use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::shim::cli::TerraformCommands;
use crate::shim::mcp;
macro_rules! string_enum {
($name:ident { $($variant:ident => $value:literal),+ $(,)? }) => {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum $name {
$(#[serde(rename = $value)] $variant),+
}
};
}
string_enum!(EvidenceProtocol {
ExitCode => "exit_code",
StdoutSha256 => "stdout_sha256",
ControllerReportV1 => "controller_report_v1",
ControllerReportV2 => "controller_report_v2",
SparkProof => "spark_proof",
TerraformPlanV1 => "terraform_plan_v1",
});
#[allow(clippy::derivable_impls)]
impl Default for EvidenceProtocol {
fn default() -> Self {
Self::ExitCode
}
}
string_enum!(TerraformAction {
NoOp => "no-op",
Create => "create",
Read => "read",
Update => "update",
Delete => "delete",
Replace => "replace",
});
string_enum!(TerraformRawAction {
NoOp => "no-op",
Create => "create",
Read => "read",
Update => "update",
Delete => "delete",
});
string_enum!(MaterialChanges {
Any => "any",
Required => "required",
Forbidden => "forbidden",
});
#[allow(clippy::derivable_impls)]
impl Default for MaterialChanges {
fn default() -> Self {
Self::Any
}
}
string_enum!(ProviderSource {
HashicorpAws => "registry.terraform.io/hashicorp/aws",
});
string_enum!(AwsProviderVersion {
V6_10_0 => "6.10.0",
});
string_enum!(LinuxPlatform {
Amd64 => "linux_amd64",
});
string_enum!(AwsProviderPackageDigest {
V6_10_0LinuxAmd64 => "3c92efebaf635372bf7283e04fc667d59b0ff3cf1aacd011fc484a11f70954d9",
});
string_enum!(LocalStackRegion {
UsEast1 => "us-east-1",
});
string_enum!(CredentialIdentity {
SyntheticV1 => "falsegreen-synthetic-localstack-v1",
});
string_enum!(LocalStackImage {
Pinned => "localstack/localstack@sha256:08e57a2147bda1ae1b21c0b571b410ea38a855b0ba4f8b2e2fa2d3a247dc3d2b",
});
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformExpectedAction {
pub address: String,
pub action: TerraformAction,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformForbiddenAction {
pub address_pattern: String,
pub actions: BTreeSet<TerraformAction>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformPlanAssertions {
#[serde(default)]
pub no_deletions: bool,
#[serde(default)]
pub no_replacements: bool,
#[serde(default)]
pub maximum_deletions: Option<u64>,
#[serde(default)]
pub maximum_replacements: Option<u64>,
#[serde(default)]
pub expected_resource_addresses: Vec<String>,
#[serde(default)]
pub expected_resource_types: Vec<String>,
#[serde(default)]
pub expected_actions: Vec<TerraformExpectedAction>,
#[serde(default)]
pub forbidden_actions: Vec<TerraformForbiddenAction>,
#[serde(default)]
pub allowed_changed_resource_patterns: Vec<String>,
#[serde(default)]
pub forbidden_changed_resource_patterns: Vec<String>,
#[serde(default)]
pub expected_outputs: Vec<String>,
#[serde(default)]
pub expected_changed_outputs: Vec<String>,
#[serde(default)]
pub material_changes: MaterialChanges,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformProviderAuthoritySpec {
pub source: ProviderSource,
pub version: AwsProviderVersion,
#[serde(default = "linux_amd64")]
pub platform: LinuxPlatform,
pub lockfile_sha256: String,
pub package_sha256: AwsProviderPackageDigest,
pub mirror_manifest_sha256: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
fn linux_amd64() -> LinuxPlatform {
LinuxPlatform::Amd64
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformLocalStackTarget {
#[serde(default = "target_schema")]
pub schema_version: String,
pub environment_id: String,
pub endpoint: String,
#[serde(default = "us_east_1")]
pub region: LocalStackRegion,
#[serde(default = "synthetic_credentials")]
pub credential_identity: CredentialIdentity,
#[serde(default = "pinned_localstack")]
pub emulator_image: LocalStackImage,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
fn target_schema() -> String {
"falsegreen.terraform-localstack-target/v1".to_string()
}
fn us_east_1() -> LocalStackRegion {
LocalStackRegion::UsEast1
}
fn synthetic_credentials() -> CredentialIdentity {
CredentialIdentity::SyntheticV1
}
fn pinned_localstack() -> LocalStackImage {
LocalStackImage::Pinned
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformExpectedS3Bucket {
pub resource_address: String,
pub bucket: String,
#[serde(default)]
pub tags: BTreeMap<String, String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformDeploymentSpec {
#[serde(default = "deployment_spec_schema")]
pub schema_version: String,
pub target: TerraformLocalStackTarget,
pub expected_s3_buckets: Vec<TerraformExpectedS3Bucket>,
#[serde(default = "authorization_max_seconds")]
pub authorization_max_seconds: u64,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
fn deployment_spec_schema() -> String {
"falsegreen.terraform-deployment-spec/v1".to_string()
}
fn authorization_max_seconds() -> u64 {
900
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformPlanSpec {
pub assertions: TerraformPlanAssertions,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_authority: Option<TerraformProviderAuthoritySpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deployment: Option<TerraformDeploymentSpec>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ContractCommand {
pub argv: Vec<String>,
#[serde(default)]
pub evidence_protocol: EvidenceProtocol,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terraform_plan: Option<TerraformPlanSpec>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ContractCriterion {
pub id: String,
#[serde(default)]
pub requires_process_provenance: bool,
pub commands: Vec<ContractCommand>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ContractTransport {
pub schema_version: u64,
pub task_id: String,
pub criteria: Vec<ContractCriterion>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
pub fn validate_contract_argument(arguments: &Value) -> Result<(), String> {
let Some(contract) = arguments.get("contract") else {
return Ok(());
};
let has_terraform = contract
.get("criteria")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|criterion| criterion.get("commands").and_then(Value::as_array))
.flatten()
.any(|command| {
command.get("evidence_protocol").and_then(Value::as_str) == Some("terraform_plan_v1")
});
if !has_terraform {
return Ok(());
}
let typed: ContractTransport = serde_json::from_value(contract.clone())
.map_err(|error| format!("incompatible Terraform contract schema: {error}"))?;
if typed.schema_version != 1 {
return Err(format!(
"unsupported contract schema version: {}",
typed.schema_version
));
}
for criterion in typed.criteria {
for command in criterion.commands {
if command.evidence_protocol != EvidenceProtocol::TerraformPlanV1 {
continue;
}
let plan = command
.terraform_plan
.ok_or_else(|| "terraform_plan_v1 requires terraform_plan".to_string())?;
if let Some(deployment) = plan.deployment {
if plan.provider_authority.is_none() {
return Err(
"Terraform deployment requires frozen provider authority".to_string()
);
}
if deployment.schema_version != "falsegreen.terraform-deployment-spec/v1" {
return Err(format!(
"unsupported Terraform deployment schema: {}",
deployment.schema_version
));
}
if deployment.target.schema_version != "falsegreen.terraform-localstack-target/v1" {
return Err(format!(
"unsupported Terraform target schema: {}",
deployment.target.schema_version
));
}
}
}
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformPlanValue {
pub present: bool,
#[serde(default)]
pub value: Value,
#[serde(default)]
pub unknown: Value,
#[serde(default)]
pub sensitive: Value,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformResourceChange {
pub address: String,
pub previous_address: Option<String>,
pub module_address: Option<String>,
pub mode: String,
pub resource_type: String,
pub resource_name: String,
pub provider_name: Option<String>,
pub action: TerraformAction,
pub actions: Vec<TerraformRawAction>,
pub action_reason: Option<String>,
pub before: TerraformPlanValue,
pub after: TerraformPlanValue,
#[serde(default)]
pub replace_paths: Vec<Vec<Value>>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformOutputChange {
pub name: String,
pub action: TerraformAction,
pub actions: Vec<TerraformRawAction>,
pub before: TerraformPlanValue,
pub after: TerraformPlanValue,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformActionCounts {
#[serde(default)]
pub no_op: u64,
#[serde(default)]
pub create: u64,
#[serde(default)]
pub read: u64,
#[serde(default)]
pub update: u64,
#[serde(default)]
pub delete: u64,
#[serde(default)]
pub replace: u64,
#[serde(default)]
pub material: u64,
#[serde(default)]
pub total: u64,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformParsedPlan {
pub schema_version: String,
pub terraform_version: String,
pub format_version: String,
#[serde(default)]
pub errored: bool,
#[serde(default)]
pub provider_configurations: Vec<Value>,
#[serde(default)]
pub configuration_resources: Vec<Value>,
#[serde(default)]
pub configuration_outputs: Vec<String>,
#[serde(default)]
pub resource_changes: Vec<TerraformResourceChange>,
#[serde(default)]
pub resource_drift: Vec<TerraformResourceChange>,
#[serde(default)]
pub output_changes: Vec<TerraformOutputChange>,
pub action_counts: TerraformActionCounts,
pub output_action_counts: TerraformActionCounts,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformAssertionResult {
pub assertion: String,
pub subject: Option<String>,
pub passed: bool,
pub message: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformInvocation {
pub phase: String,
pub argv: Vec<String>,
pub exit_code: i32,
pub timed_out: bool,
pub duration_ms: u64,
pub stdout_sha256: String,
pub stderr_sha256: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformToolIdentity {
pub executable_requested: String,
pub executable_resolved: String,
pub executable_sha256: String,
pub version: Option<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformProviderAuthorityEvidence {
pub schema_version: String,
pub source: String,
pub version: String,
pub platform: String,
pub lockfile_source_path: String,
pub lockfile_sha256: String,
pub package_filename: String,
pub package_sha256: String,
pub mirror_manifest_sha256: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformInputStateIdentity {
pub source_path: String,
pub present: bool,
pub authoritative_sha256: Option<String>,
pub retained_copy_sha256: Option<String>,
pub working_copy_sha256_after_plan: Option<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformPlanArtifacts {
pub input_state: TerraformInputStateIdentity,
pub provider_authority: Option<TerraformProviderAuthorityEvidence>,
pub configuration_sha256: Option<String>,
pub plan_binary_sha256: Option<String>,
pub canonical_plan_json_sha256: Option<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformPlanEvidence {
pub schema_version: String,
pub tool: TerraformToolIdentity,
pub invocations: Vec<TerraformInvocation>,
pub artifacts: TerraformPlanArtifacts,
pub plan: Option<TerraformParsedPlan>,
#[serde(default)]
pub assertions: Vec<TerraformAssertionResult>,
pub complete: bool,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProcessObserverIdentity {
pub schema_version: String,
pub observer_name: String,
pub observer_version: String,
pub observer_program_sha256: String,
pub bpftrace_version: String,
pub bpftrace_sha256: String,
pub kernel_release: String,
pub boot_id: String,
pub cgroup_v2_path: String,
pub cgroup_v2_inode: u64,
pub container_id: String,
pub tracepoints: Vec<String>,
pub limits: BTreeMap<String, u64>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ProcessProvenance {
pub schema_version: String,
pub collector: String,
pub run_id: String,
pub command_index: u64,
pub criterion_id: String,
pub capture_started_monotonic_ns: u64,
pub capture_completed_monotonic_ns: u64,
pub observer: Option<ProcessObserverIdentity>,
#[serde(default)]
pub roots: Vec<Value>,
#[serde(default)]
pub processes: Vec<Value>,
pub complete: bool,
#[serde(default)]
pub lost_events: u64,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
string_enum!(ApprovalMode {
DevelopmentHumanConfirmed => "development_human_confirmed",
EnforcedSigned => "enforced_signed",
});
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformApplyAuthorization {
pub schema_version: String,
pub authorization_id: String,
pub task_id: String,
pub run_id: String,
pub criterion_id: String,
pub command_index: u64,
pub contract_sha256: String,
pub source_artifact_sha256: String,
pub terraform_configuration_sha256: String,
pub plan_binary_sha256: String,
pub canonical_plan_json_sha256: String,
pub plan_evidence_sha256: String,
pub input_state_source_path: String,
pub input_state_present: bool,
pub authoritative_input_state_sha256: Option<String>,
pub working_state_sha256_after_plan: Option<String>,
pub provider: TerraformProviderAuthorityEvidence,
pub terraform: TerraformToolIdentity,
pub target: TerraformLocalStackTarget,
pub plan_process_provenance_sha256: String,
pub plan_process_provenance_complete: bool,
pub permission: String,
pub created_at: String,
pub expires_at: String,
pub approval_mode: ApprovalMode,
pub approval_key_id: Option<String>,
pub approval_signature: Option<String>,
pub authorization_payload_sha256: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerraformProcessProvenanceClaim {
pub artifact_sha256: Option<String>,
pub observer_identity_sha256: Option<String>,
pub observer: Option<ProcessObserverIdentity>,
pub complete: bool,
pub lost_events: u64,
#[serde(default)]
pub errors: Vec<String>,
pub process_count: u64,
#[serde(default)]
pub phases: Vec<String>,
pub terraform_process_observed: bool,
pub provider_child_process_observed: bool,
#[serde(default)]
pub argv_truncated: bool,
#[serde(default)]
pub canonical_executable_truncated: bool,
#[serde(default)]
pub capture_limit_exceeded: bool,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformDeploymentInvocation {
pub phase: String,
pub argv: Vec<String>,
pub exit_code: i32,
pub timed_out: bool,
pub duration_ms: u64,
pub stdout_sha256: String,
pub stderr_sha256: String,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformApplyEvidence {
pub exact_plan_sha256: String,
pub authorization_id: String,
pub invocations: Vec<TerraformDeploymentInvocation>,
pub pre_apply_state_sha256: Option<String>,
pub post_apply_state_sha256: Option<String>,
pub process_provenance: TerraformProcessProvenanceClaim,
pub succeeded: bool,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformObservedValue {
pub state: String,
#[serde(default)]
pub value: Value,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformProviderResourceObservation {
pub provider: String,
pub resource_address: String,
pub external_id: String,
pub existence: TerraformObservedValue,
#[serde(default)]
pub properties: BTreeMap<String, TerraformObservedValue>,
pub observed_at: String,
pub endpoint: String,
pub complete: bool,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformProviderStateEvidence {
pub schema_version: String,
pub provider: String,
pub target: TerraformLocalStackTarget,
#[serde(default)]
pub resources: Vec<TerraformProviderResourceObservation>,
pub complete: bool,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformRefreshedStateEvidence {
pub invocations: Vec<TerraformDeploymentInvocation>,
pub process_provenance: Option<TerraformProcessProvenanceClaim>,
pub plan: Option<TerraformParsedPlan>,
pub canonical_plan_json_sha256: Option<String>,
pub complete: bool,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
string_enum!(DeploymentStatus {
NotApplied => "not_applied",
Accepted => "accepted",
Incomplete => "incomplete",
Invalid => "invalid",
});
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformDeploymentEvidence {
pub schema_version: String,
pub verification_sequence: u64,
pub authorization: TerraformApplyAuthorization,
pub configuration_source_sha256: String,
pub terraform_configuration_sha256: String,
pub provider: TerraformProviderAuthorityEvidence,
pub apply: TerraformApplyEvidence,
pub apply_evidence_sha256: String,
pub provider_state: TerraformProviderStateEvidence,
pub provider_state_sha256: String,
pub refreshed_state: TerraformRefreshedStateEvidence,
pub refreshed_state_sha256: String,
#[serde(default)]
pub assertions: Vec<TerraformAssertionResult>,
pub evidence_sha256: String,
pub status: DeploymentStatus,
pub complete: bool,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
string_enum!(PlanStatus {
Accepted => "accepted",
Incomplete => "incomplete",
Invalid => "invalid",
});
string_enum!(AuthorizationStatus {
Unavailable => "unavailable",
NotAuthorized => "not_authorized",
Authorized => "authorized",
Consumed => "consumed",
Expired => "expired",
});
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformPlanState {
pub status: PlanStatus,
pub criterion_id: String,
pub command_index: u64,
pub evidence: Option<TerraformPlanEvidence>,
pub process_provenance: Option<ProcessProvenance>,
pub process_provenance_sha256: Option<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformAuthorizationState {
pub status: AuthorizationStatus,
pub authorization: Option<TerraformApplyAuthorization>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformDeployedOutcome {
pub status: DeploymentStatus,
pub evidence: Option<TerraformDeploymentEvidence>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TerraformProductStatus {
pub schema_version: String,
pub task_id: String,
pub run_id: String,
pub contract_sha256: String,
pub source_artifact_sha256: String,
pub plan: TerraformPlanState,
pub apply_authorization: TerraformAuthorizationState,
pub deployed_outcome: TerraformDeployedOutcome,
#[serde(default)]
pub durable_run_id: Option<String>,
#[serde(default)]
pub next_action: Option<String>,
#[serde(default, flatten)]
pub extra: BTreeMap<String, Value>,
}
impl TerraformProductStatus {
fn parse(value: Value) -> Result<Self, String> {
let status: Self = serde_json::from_value(value)
.map_err(|error| format!("incompatible Terraform result schema: {error}"))?;
if status.schema_version != "falsegreen.terraform-product-status/v1" {
return Err(format!(
"unsupported Terraform result schema: {}",
status.schema_version
));
}
status.validate_schema_versions()?;
let has_authorization = status.apply_authorization.authorization.is_some();
let authorization_requires_record = matches!(
status.apply_authorization.status,
AuthorizationStatus::Authorized
| AuthorizationStatus::Consumed
| AuthorizationStatus::Expired
);
if has_authorization != authorization_requires_record {
return Err("Terraform authorization status/record mismatch".to_string());
}
let has_deployment = status.deployed_outcome.evidence.is_some();
if has_deployment != !matches!(status.deployed_outcome.status, DeploymentStatus::NotApplied)
{
return Err("Terraform deployment status/evidence mismatch".to_string());
}
if let Some(evidence) = &status.deployed_outcome.evidence
&& (evidence.status != status.deployed_outcome.status
|| evidence.complete != matches!(evidence.status, DeploymentStatus::Accepted))
{
return Err("Terraform deployment completeness/status mismatch".to_string());
}
Ok(status)
}
fn validate_schema_versions(&self) -> Result<(), String> {
if let Some(plan) = &self.plan.evidence {
require_schema(
"Terraform plan evidence",
&plan.schema_version,
"falsegreen.terraform-plan-evidence/v1",
)?;
if let Some(parsed) = &plan.plan {
require_schema(
"Terraform parsed plan",
&parsed.schema_version,
"falsegreen.terraform-plan/v1",
)?;
}
if let Some(provider) = &plan.artifacts.provider_authority {
validate_provider_evidence(provider)?;
}
}
if let Some(provenance) = &self.plan.process_provenance {
require_schema(
"process provenance",
&provenance.schema_version,
"falsegreen.process-provenance/v1",
)?;
if let Some(observer) = &provenance.observer {
validate_observer(observer)?;
}
}
if let Some(authorization) = &self.apply_authorization.authorization {
validate_authorization(authorization)?;
}
if let Some(deployment) = &self.deployed_outcome.evidence {
require_schema(
"Terraform deployment evidence",
&deployment.schema_version,
"falsegreen.terraform-deployment-evidence/v1",
)?;
validate_authorization(&deployment.authorization)?;
validate_provider_evidence(&deployment.provider)?;
require_schema(
"Terraform provider-state evidence",
&deployment.provider_state.schema_version,
"falsegreen.terraform-provider-state/v1",
)?;
validate_target(&deployment.provider_state.target)?;
validate_provenance_claim(&deployment.apply.process_provenance)?;
if let Some(provenance) = &deployment.refreshed_state.process_provenance {
validate_provenance_claim(provenance)?;
}
}
Ok(())
}
fn render(&self) -> String {
let mut lines = vec![
format!("PLAN: {}", uppercase_json_enum(&self.plan.status)),
format!(
"APPLY AUTHORIZATION: {}",
uppercase_json_enum(&self.apply_authorization.status)
),
format!(
"DEPLOYED OUTCOME: {}",
uppercase_json_enum(&self.deployed_outcome.status)
),
format!("Task: {}", self.task_id),
format!("Run: {}", self.run_id),
format!("Contract digest: sha256:{}", self.contract_sha256),
];
if let Some(evidence) = &self.plan.evidence {
if let Some(digest) = &evidence.artifacts.plan_binary_sha256 {
lines.push(format!("Plan binary digest: sha256:{digest}"));
}
lines.push(format!("Plan evidence complete: {}", evidence.complete));
if !evidence.errors.is_empty() {
lines.push(format!("Plan errors: {}", evidence.errors.join(", ")));
}
}
if let Some(provenance) = &self.plan.process_provenance {
lines.push(format!("Plan provenance complete: {}", provenance.complete));
lines.push(format!(
"Plan provenance lost events: {}",
provenance.lost_events
));
if !provenance.errors.is_empty() {
lines.push(format!(
"Plan provenance errors: {}",
provenance.errors.join(", ")
));
}
}
if let Some(authorization) = &self.apply_authorization.authorization {
lines.push(format!(
"Authorization ID: {}",
authorization.authorization_id
));
lines.push(format!(
"Authorization expires: {}",
authorization.expires_at
));
}
if let Some(deployment) = &self.deployed_outcome.evidence {
lines.push(format!(
"Deployment evidence digest: sha256:{}",
deployment.evidence_sha256
));
lines.push(format!("Deployment complete: {}", deployment.complete));
lines.push(format!(
"Apply provenance complete: {}",
deployment.apply.process_provenance.complete
));
lines.push(format!(
"Provider observation complete: {}",
deployment.provider_state.complete
));
lines.push(format!(
"Refreshed state complete: {}",
deployment.refreshed_state.complete
));
if !deployment.errors.is_empty() {
lines.push(format!(
"Deployment errors: {}",
deployment.errors.join(", ")
));
}
}
lines.join("\n")
}
}
fn require_schema(label: &str, observed: &str, expected: &str) -> Result<(), String> {
if observed == expected {
Ok(())
} else {
Err(format!("unsupported {label} schema: {observed}"))
}
}
fn validate_observer(observer: &ProcessObserverIdentity) -> Result<(), String> {
require_schema(
"process observer identity",
&observer.schema_version,
"falsegreen.process-observer-identity/v1",
)
}
fn validate_target(target: &TerraformLocalStackTarget) -> Result<(), String> {
require_schema(
"Terraform target",
&target.schema_version,
"falsegreen.terraform-localstack-target/v1",
)
}
fn validate_provider_evidence(provider: &TerraformProviderAuthorityEvidence) -> Result<(), String> {
require_schema(
"Terraform provider authority",
&provider.schema_version,
"falsegreen.terraform-provider-authority/v1",
)
}
fn validate_authorization(authorization: &TerraformApplyAuthorization) -> Result<(), String> {
require_schema(
"Terraform apply authorization",
&authorization.schema_version,
"falsegreen.terraform-apply-authorization/v1",
)?;
validate_provider_evidence(&authorization.provider)?;
validate_target(&authorization.target)?;
if authorization.permission != "terraform_apply_exact_saved_plan"
|| !authorization.plan_process_provenance_complete
{
return Err("Terraform authorization permission/provenance mismatch".to_string());
}
Ok(())
}
fn validate_provenance_claim(claim: &TerraformProcessProvenanceClaim) -> Result<(), String> {
if let Some(observer) = &claim.observer {
validate_observer(observer)?;
}
Ok(())
}
fn uppercase_json_enum<T: Serialize>(value: &T) -> String {
serde_json::to_value(value)
.ok()
.and_then(|item| item.as_str().map(str::to_uppercase))
.unwrap_or_else(|| "UNKNOWN".to_string())
}
#[derive(Debug, Deserialize)]
struct AttestationSignature {
algorithm: String,
key_id: Option<String>,
value: Option<String>,
}
#[derive(Debug, Deserialize)]
struct IacAttestationView {
#[serde(rename = "schema")]
schema_id: String,
status: String,
task_id: String,
run_id: String,
terraform_plans: Vec<Value>,
terraform_deployments: Vec<TerraformDeploymentEvidence>,
process_provenance: Value,
signature: AttestationSignature,
#[serde(default, flatten)]
extra: BTreeMap<String, Value>,
}
fn call_status(name: &str, arguments: Value) -> Result<TerraformProductStatus, String> {
TerraformProductStatus::parse(mcp::invoke_tool(name, arguments)?)
}
fn identity_arguments(task_id: String, run_id: String, command_index: Option<u64>) -> Value {
let mut arguments = json!({"task_id": task_id, "run_id": run_id});
if let Some(index) = command_index {
arguments["command_index"] = json!(index);
}
arguments
}
pub fn run(command: TerraformCommands) -> Result<String, String> {
match command {
TerraformCommands::Inspect {
task_id,
run_id,
command_index,
} => {
let status = call_status(
"falsegreen_get_terraform_result",
identity_arguments(task_id, run_id, command_index),
)?;
Ok(status.render())
}
TerraformCommands::AuthorizationPayload {
task_id,
run_id,
command_index,
valid_for_seconds,
} => {
let mut arguments = identity_arguments(task_id, run_id, command_index);
if let Some(seconds) = valid_for_seconds {
arguments["valid_for_seconds"] = json!(seconds);
}
let payload = mcp::invoke_tool(
"falsegreen_get_terraform_apply_authorization_payload",
arguments,
)?;
serde_json::to_string_pretty(&payload)
.map_err(|error| format!("cannot render authorization payload: {error}"))
}
TerraformCommands::Authorize {
task_id,
run_id,
actor,
confirm,
command_index,
valid_for_seconds,
approval_file,
} => {
if !confirm {
return Err("refusing to authorize Terraform apply without --confirm".to_string());
}
let mut arguments = identity_arguments(task_id, run_id, command_index);
arguments["actor"] = json!(actor);
arguments["confirmed"] = json!(true);
if let Some(seconds) = valid_for_seconds {
arguments["valid_for_seconds"] = json!(seconds);
}
if let Some(path) = approval_file {
let raw = fs::read(&path).map_err(|error| {
format!("cannot read approval file {}: {error}", path.display())
})?;
arguments["approval"] = serde_json::from_slice(&raw).map_err(|error| {
format!(
"approval file {} is not valid JSON: {error}",
path.display()
)
})?;
}
Ok(call_status("falsegreen_authorize_terraform_apply", arguments)?.render())
}
TerraformCommands::Apply {
task_id,
run_id,
authorization_id,
command_index,
} => {
let mut arguments = identity_arguments(task_id, run_id, command_index);
arguments["authorization_id"] = json!(authorization_id);
Ok(call_status("falsegreen_apply_authorized_terraform_plan", arguments)?.render())
}
TerraformCommands::VerifyDeployed {
task_id,
run_id,
command_index,
} => {
let identity = identity_arguments(task_id.clone(), run_id.clone(), command_index);
let status = call_status("falsegreen_verify_terraform_deployment", identity)?;
let artifacts = mcp::invoke_tool(
"falsegreen_get_verification_artifacts",
json!({"task_id": task_id, "run_id": run_id}),
)?;
let raw_attestation = artifacts
.get("attestation_json")
.and_then(Value::as_str)
.ok_or_else(|| "verification artifacts omitted attestation_json".to_string())?;
let attestation: IacAttestationView = serde_json::from_str(raw_attestation)
.map_err(|error| format!("incompatible signed attestation schema: {error}"))?;
if attestation.schema_id != "falsegreen.attestation/v1"
|| attestation.task_id != status.task_id
|| attestation.run_id != status.run_id
{
return Err(
"signed attestation identity does not match Terraform result".to_string(),
);
}
if attestation.terraform_plans.is_empty()
|| attestation.terraform_deployments.is_empty()
|| attestation.process_provenance.is_null()
{
return Err("signed attestation omitted Terraform/provenance claims".to_string());
}
let signature = if attestation.signature.algorithm == "Ed25519"
&& attestation.signature.value.is_some()
{
format!(
"SIGNED ({})",
attestation
.signature
.key_id
.as_deref()
.unwrap_or("unknown key")
)
} else {
"UNSIGNED".to_string()
};
let _future_fields = attestation.extra.len();
Ok(format!(
"{}\nATTESTATION: {}\nATTESTED OUTCOME: {}",
status.render(),
signature,
attestation.status.to_uppercase()
))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn terraform_contract() -> Value {
json!({
"schema_version": 1,
"task_id": "task_iac",
"goal": "apply one exact LocalStack outcome",
"constraints": [],
"exclusions": ["no real AWS"],
"uncertainties": [],
"criteria": [{
"id": "TF-1",
"title": "exact plan",
"description": "one bounded change",
"role": "release_requirement",
"provenance": ["human"],
"required": true,
"requires_process_provenance": true,
"commands": [{
"argv": ["terraform", "plan"],
"evidence_protocol": "terraform_plan_v1",
"terraform_plan": {
"assertions": {
"no_deletions": true,
"no_replacements": true,
"maximum_deletions": 0,
"maximum_replacements": 0,
"expected_resource_addresses": ["aws_s3_bucket.staging"],
"expected_resource_types": ["aws_s3_bucket"],
"expected_actions": [{"address": "aws_s3_bucket.staging", "action": "create"}],
"forbidden_actions": [{"address_pattern": "*", "actions": ["delete", "replace"]}],
"allowed_changed_resource_patterns": ["aws_s3_bucket.staging"],
"forbidden_changed_resource_patterns": ["aws_s3_bucket.unrelated"],
"expected_outputs": ["bucket_name"],
"expected_changed_outputs": ["bucket_name"],
"material_changes": "required"
},
"provider_authority": {
"source": "registry.terraform.io/hashicorp/aws",
"version": "6.10.0",
"platform": "linux_amd64",
"lockfile_sha256": "1".repeat(64),
"package_sha256": "3c92efebaf635372bf7283e04fc667d59b0ff3cf1aacd011fc484a11f70954d9",
"mirror_manifest_sha256": "2".repeat(64)
},
"deployment": {
"schema_version": "falsegreen.terraform-deployment-spec/v1",
"target": {
"schema_version": "falsegreen.terraform-localstack-target/v1",
"environment_id": "localstack-mvp",
"endpoint": "http://127.0.0.1:4566",
"region": "us-east-1",
"credential_identity": "falsegreen-synthetic-localstack-v1",
"emulator_image": "localstack/localstack@sha256:08e57a2147bda1ae1b21c0b571b410ea38a855b0ba4f8b2e2fa2d3a247dc3d2b"
},
"expected_s3_buckets": [{
"resource_address": "aws_s3_bucket.staging",
"bucket": "falsegreen-iac-mvp-staging",
"tags": {"ManagedBy": "FalseGreen"}
}],
"authorization_max_seconds": 900
}
},
"timeout_seconds": 1200
}]
}],
"max_repair_cycles": 1,
"created_at": "2026-01-01T00:00:00Z",
"created_by": "human"
})
}
#[test]
fn terraform_contract_round_trip_retains_assertions_and_provenance_requirement() {
let original = terraform_contract();
let contract: ContractTransport = serde_json::from_value(original.clone()).unwrap();
let criterion = &contract.criteria[0];
assert!(criterion.requires_process_provenance);
assert_eq!(
criterion.commands[0].evidence_protocol,
EvidenceProtocol::TerraformPlanV1
);
let assertions = &criterion.commands[0]
.terraform_plan
.as_ref()
.unwrap()
.assertions;
assert_eq!(
assertions.expected_actions[0].action,
TerraformAction::Create
);
assert_eq!(assertions.forbidden_actions[0].actions.len(), 2);
assert_eq!(assertions.material_changes, MaterialChanges::Required);
assert_eq!(serde_json::to_value(contract).unwrap(), original);
}
#[test]
fn enum_mismatch_fails_instead_of_coercing() {
let mut contract = terraform_contract();
contract["criteria"][0]["commands"][0]["terraform_plan"]["assertions"]["expected_actions"]
[0]["action"] = json!("recreate");
assert!(serde_json::from_value::<ContractTransport>(contract).is_err());
}
#[test]
fn additive_future_fields_are_preserved_by_typed_contract_round_trip() {
let mut contract = terraform_contract();
contract["criteria"][0]["commands"][0]["terraform_plan"]["future_optional"] =
json!({"mode": "future"});
let decoded: ContractTransport = serde_json::from_value(contract.clone()).unwrap();
assert_eq!(serde_json::to_value(decoded).unwrap(), contract);
}
#[test]
fn legacy_non_terraform_contract_stays_on_raw_compatible_path() {
let arguments = json!({
"task_id": "task_old",
"contract": {
"schema_version": 1,
"task_id": "task_old",
"criteria": [{
"id": "OLD-1",
"commands": [{
"argv": ["python", "-c", "print('ok')"],
"evidence_protocol": "controller_report_v2",
"legacy_optional_shape": {"preserve": true}
}]
}]
}
});
assert!(validate_contract_argument(&arguments).is_ok());
}
#[test]
fn typed_contract_boundary_rejects_unknown_iac_schema_version() {
let mut contract = terraform_contract();
contract["criteria"][0]["commands"][0]["terraform_plan"]["deployment"]["schema_version"] =
json!("falsegreen.terraform-deployment-spec/v2");
let error = validate_contract_argument(&json!({"contract": contract})).unwrap_err();
assert!(error.contains("unsupported Terraform deployment schema"));
}
#[test]
fn plan_acceptance_without_authorization_or_apply_renders_distinct_states() {
let status = minimal_status("accepted", "not_authorized", "not_applied", true);
let parsed = TerraformProductStatus::parse(status).unwrap();
let rendered = parsed.render();
assert!(rendered.contains("PLAN: ACCEPTED"));
assert!(rendered.contains("APPLY AUTHORIZATION: NOT_AUTHORIZED"));
assert!(rendered.contains("DEPLOYED OUTCOME: NOT_APPLIED"));
}
#[test]
fn incomplete_provenance_and_deployment_are_never_rendered_as_success() {
let status = minimal_status("incomplete", "unavailable", "not_applied", false);
let parsed = TerraformProductStatus::parse(status).unwrap();
let rendered = parsed.render();
assert!(rendered.contains("PLAN: INCOMPLETE"));
assert!(!rendered.contains("Accepted-plan digest:"));
assert!(rendered.contains("Plan provenance complete: false"));
assert!(!rendered.contains("PLAN: ACCEPTED"));
}
#[test]
fn exact_authorization_identity_and_deployment_evidence_deserialize() {
let authorization = authorization_value();
let deployment = deployment_value(true);
let mut status = minimal_status("accepted", "consumed", "accepted", true);
status["apply_authorization"]["authorization"] = authorization.clone();
status["deployed_outcome"]["evidence"] = deployment.clone();
let parsed = TerraformProductStatus::parse(status).unwrap();
let authorized = parsed.apply_authorization.authorization.unwrap();
assert_eq!(authorized.task_id, "task_iac");
assert_eq!(authorized.run_id, "run_iac");
assert_eq!(authorized.contract_sha256, "c".repeat(64));
assert_eq!(authorized.plan_binary_sha256, "b".repeat(64));
assert_eq!(authorized.permission, "terraform_apply_exact_saved_plan");
let deployed = parsed.deployed_outcome.evidence.unwrap();
assert!(deployed.complete);
assert!(deployed.apply.process_provenance.complete);
assert!(deployed.apply.process_provenance.terraform_process_observed);
assert!(
deployed
.apply
.process_provenance
.provider_child_process_observed
);
assert!(deployed.provider_state.complete);
assert!(deployed.refreshed_state.complete);
assert_eq!(deployed.evidence_sha256, "e".repeat(64));
let attestation: IacAttestationView = serde_json::from_value(json!({
"schema": "falsegreen.attestation/v1",
"status": "accepted",
"task_id": "task_iac",
"run_id": "run_iac",
"terraform_plans": [{"complete": true}],
"terraform_deployments": [deployment],
"process_provenance": {"commands": [{"complete": true}]},
"signature": {"algorithm": "Ed25519", "key_id": "sha256:key", "value": "signature"},
"future_optional": {"retained": true}
}))
.unwrap();
assert_eq!(attestation.signature.algorithm, "Ed25519");
assert_eq!(attestation.terraform_deployments.len(), 1);
assert!(attestation.extra.contains_key("future_optional"));
}
#[test]
fn incomplete_deployment_evidence_remains_incomplete_with_errors() {
let mut status = minimal_status("accepted", "consumed", "incomplete", true);
status["apply_authorization"]["authorization"] = authorization_value();
status["deployed_outcome"]["evidence"] = deployment_value(false);
let parsed = TerraformProductStatus::parse(status).unwrap();
let evidence = parsed.deployed_outcome.evidence.unwrap();
assert!(!evidence.complete);
assert_eq!(evidence.status, DeploymentStatus::Incomplete);
assert!(
evidence
.errors
.contains(&"observer_capture_incomplete".to_string())
);
assert!(!evidence.apply.process_provenance.complete);
}
#[test]
fn unsupported_returned_schema_version_fails_explicitly() {
let mut status = minimal_status("accepted", "not_authorized", "not_applied", true);
status["plan"]["process_provenance"]["schema_version"] =
json!("falsegreen.process-provenance/v2");
assert!(
TerraformProductStatus::parse(status)
.unwrap_err()
.contains("unsupported process provenance schema")
);
}
fn target_value() -> Value {
json!({
"schema_version": "falsegreen.terraform-localstack-target/v1",
"environment_id": "localstack-mvp",
"endpoint": "http://127.0.0.1:4566",
"region": "us-east-1",
"credential_identity": "falsegreen-synthetic-localstack-v1",
"emulator_image": "localstack/localstack@sha256:08e57a2147bda1ae1b21c0b571b410ea38a855b0ba4f8b2e2fa2d3a247dc3d2b"
})
}
fn provider_value() -> Value {
json!({
"schema_version": "falsegreen.terraform-provider-authority/v1",
"source": "registry.terraform.io/hashicorp/aws",
"version": "6.10.0",
"platform": "linux_amd64",
"lockfile_source_path": ".terraform.lock.hcl",
"lockfile_sha256": "l".repeat(64),
"package_filename": "registry.terraform.io/hashicorp/aws/provider.zip",
"package_sha256": "p".repeat(64),
"mirror_manifest_sha256": "m".repeat(64)
})
}
fn authorization_value() -> Value {
json!({
"schema_version": "falsegreen.terraform-apply-authorization/v1",
"authorization_id": format!("tfapply_{}", "a".repeat(32)),
"task_id": "task_iac",
"run_id": "run_iac",
"criterion_id": "TF-1",
"command_index": 1,
"contract_sha256": "c".repeat(64),
"source_artifact_sha256": "s".repeat(64),
"terraform_configuration_sha256": "f".repeat(64),
"plan_binary_sha256": "b".repeat(64),
"canonical_plan_json_sha256": "j".repeat(64),
"plan_evidence_sha256": "v".repeat(64),
"input_state_source_path": "terraform.tfstate",
"input_state_present": true,
"authoritative_input_state_sha256": "i".repeat(64),
"working_state_sha256_after_plan": "w".repeat(64),
"provider": provider_value(),
"terraform": {
"executable_requested": "terraform",
"executable_resolved": "/usr/bin/terraform",
"executable_sha256": "t".repeat(64),
"version": "1.13.5"
},
"target": target_value(),
"plan_process_provenance_sha256": "r".repeat(64),
"plan_process_provenance_complete": true,
"permission": "terraform_apply_exact_saved_plan",
"created_at": "2026-01-01T00:00:00Z",
"expires_at": "2026-01-01T00:10:00Z",
"approval_mode": "development_human_confirmed",
"approval_key_id": null,
"approval_signature": null,
"authorization_payload_sha256": "a".repeat(64)
})
}
fn provenance_claim(complete: bool) -> Value {
json!({
"artifact_sha256": "q".repeat(64),
"observer_identity_sha256": null,
"observer": null,
"complete": complete,
"lost_events": if complete { 0 } else { 1 },
"errors": if complete { Vec::<String>::new() } else { vec!["observer_capture_incomplete".to_string()] },
"process_count": 2,
"phases": ["terraform_apply"],
"terraform_process_observed": true,
"provider_child_process_observed": true,
"argv_truncated": false,
"canonical_executable_truncated": false,
"capture_limit_exceeded": false
})
}
fn deployment_value(complete: bool) -> Value {
let status = if complete { "accepted" } else { "incomplete" };
json!({
"schema_version": "falsegreen.terraform-deployment-evidence/v1",
"verification_sequence": 1,
"authorization": authorization_value(),
"configuration_source_sha256": "s".repeat(64),
"terraform_configuration_sha256": "f".repeat(64),
"provider": provider_value(),
"apply": {
"exact_plan_sha256": "b".repeat(64),
"authorization_id": format!("tfapply_{}", "a".repeat(32)),
"invocations": [],
"pre_apply_state_sha256": "w".repeat(64),
"post_apply_state_sha256": "x".repeat(64),
"process_provenance": provenance_claim(complete),
"succeeded": true
},
"apply_evidence_sha256": "1".repeat(64),
"provider_state": {
"schema_version": "falsegreen.terraform-provider-state/v1",
"provider": "aws-localstack-s3-v1",
"target": target_value(),
"resources": [{
"provider": "aws-localstack-s3-v1",
"resource_address": "aws_s3_bucket.staging",
"external_id": "falsegreen-iac-mvp-staging",
"existence": {"state": "known", "value": true},
"properties": {"tags": {"state": "known", "value": {"ManagedBy": "FalseGreen"}}},
"observed_at": "2026-01-01T00:01:00Z",
"endpoint": "http://127.0.0.1:4566",
"complete": complete,
"errors": []
}],
"complete": complete,
"errors": []
},
"provider_state_sha256": "2".repeat(64),
"refreshed_state": {
"invocations": [],
"process_provenance": provenance_claim(complete),
"plan": null,
"canonical_plan_json_sha256": null,
"complete": complete,
"errors": []
},
"refreshed_state_sha256": "3".repeat(64),
"assertions": [],
"evidence_sha256": "e".repeat(64),
"status": status,
"complete": complete,
"errors": if complete { Vec::<String>::new() } else { vec!["observer_capture_incomplete".to_string()] }
})
}
fn minimal_status(
plan_status: &str,
authorization_status: &str,
deployment_status: &str,
provenance_complete: bool,
) -> Value {
json!({
"schema_version": "falsegreen.terraform-product-status/v1",
"task_id": "task_iac",
"run_id": "run_iac",
"contract_sha256": "c".repeat(64),
"source_artifact_sha256": "s".repeat(64),
"plan": {
"status": plan_status,
"criterion_id": "TF-1",
"command_index": 1,
"evidence": null,
"process_provenance": {
"schema_version": "falsegreen.process-provenance/v1",
"collector": "linux-ebpf-tracepoints",
"run_id": "run_iac",
"command_index": 1,
"criterion_id": "TF-1",
"capture_started_monotonic_ns": 1,
"capture_completed_monotonic_ns": 2,
"observer": null,
"roots": [],
"processes": [],
"complete": provenance_complete,
"lost_events": 1,
"errors": ["fixture_incomplete"]
},
"process_provenance_sha256": null
},
"apply_authorization": {"status": authorization_status, "authorization": null},
"deployed_outcome": {"status": deployment_status, "evidence": null}
})
}
}