use super::Check;
use crate::core::{manifest::ProjectEntry, static_analysis::workspace_diagnostics};
use std::{
fs,
path::{Component, Path},
};
pub(super) fn inspect(root: &Path, modules: &str, project: &ProjectEntry) -> Check {
let mut check = Check {
kind: "http_provenance_declared_scope",
project: project.name.clone(),
command: vec![],
status: "not_checked",
exit_code: None,
message: None,
};
let result = (|| -> anyhow::Result<bool> {
let artifact = project
.metadata
.as_ref()
.and_then(|m| m.contract_artifact_path.as_deref())
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
Path::new(modules)
.join("client-sdk/typescript")
.join(&project.name)
.join("contract.json")
});
anyhow::ensure!(
!artifact.is_absolute()
&& !artifact.components().any(|c| matches!(
c,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)),
"Contract artifact must be application-relative"
);
let receipt = root.join(artifact).with_file_name("provenance.json");
if !receipt.exists() {
return Ok(false);
}
anyhow::ensure!(
receipt.canonicalize()?.starts_with(root.canonicalize()?),
"Provenance path leaves application"
);
anyhow::ensure!(
fs::metadata(&receipt)?.len() <= 4 * 1024 * 1024,
"Provenance manifest exceeds size limit"
);
let manifest: forklaunch_core::build::provenance::Manifest =
serde_json::from_slice(&fs::read(receipt)?)?;
anyhow::ensure!(
manifest.service == project.name,
"Provenance service does not match selected project"
);
let directory = workspace_diagnostics::project_dir(root, modules, project)
.ok_or_else(|| anyhow::anyhow!("Invalid service directory"))?;
manifest.verify_service_scope(&directory, &manifest.context)?;
Ok(true)
})();
match result {
Ok(true) => {
check.status = "passed";
check.message = Some("Declared source/configuration scope, HTTP contract, both SDKs and explicitly recorded additional asset digests match the recorded build context. Independent target/features, complete dependency graph and runtime binding/security behavior are not established.".into());
}
Ok(false) => check.message = Some("No build-owned HTTP provenance manifest; rebuild with a provenance-enabled framework hook".into()),
Err(error) => {
check.status = "failed";
check.message = Some(format!("{error:#}"));
}
}
check
}
pub(super) fn inspect_channels(
root: &Path,
modules: &str,
projects: &[ProjectEntry],
filter: Option<&str>,
) -> Option<Check> {
inspect_channel_at(
root,
modules,
projects,
filter,
&root.join(modules).join("client-sdk/events"),
)
}
pub(super) fn inspect_local_channels(
root: &Path,
modules: &str,
projects: &[ProjectEntry],
filter: Option<&str>,
) -> Vec<Check> {
projects
.iter()
.filter(|project| filter.is_none_or(|name| name == project.name))
.filter_map(|project| {
let directory =
workspace_diagnostics::project_dir(root, modules, project)?.join("events");
if !directory.join("provenance.json").exists()
&& project.metadata.as_ref().and_then(|m| m.contract_export) != Some(false)
{
return None;
}
inspect_channel_at(root, modules, projects, Some(&project.name), &directory)
})
.collect()
}
fn inspect_channel_at(
root: &Path,
modules: &str,
projects: &[ProjectEntry],
filter: Option<&str>,
directory: &Path,
) -> Option<Check> {
if !directory.join("event-contract.json").exists()
&& !directory.join("provenance.json").exists()
{
return None;
}
let mut check = Check {
kind: "channel_provenance_declared_scope",
project: filter.unwrap_or("workspace").into(),
command: vec![],
status: "not_checked",
exit_code: None,
message: None,
};
let result = (|| -> anyhow::Result<bool> {
let receipt = directory.join("provenance.json");
if !receipt.exists() {
return Ok(false);
}
anyhow::ensure!(
receipt.canonicalize()?.starts_with(root.canonicalize()?),
"Channel provenance path leaves application"
);
anyhow::ensure!(
fs::metadata(&receipt)?.len() <= 4 * 1024 * 1024,
"Channel provenance manifest exceeds size limit"
);
let manifest: forklaunch_core::build::provenance::AssetManifest =
serde_json::from_slice(&fs::read(receipt)?)?;
if filter.is_some_and(|name| name != manifest.service) {
return Ok(false);
}
let project = projects
.iter()
.find(|project| project.name == manifest.service)
.ok_or_else(|| {
anyhow::anyhow!("Channel provenance owner is not a registered project")
})?;
anyhow::ensure!(
project
.metadata
.as_ref()
.and_then(|m| m.language.as_deref())
== Some("rust"),
"Channel provenance owner must be Rust"
);
check.project = project.name.clone();
let manifest_dir = workspace_diagnostics::project_dir(root, modules, project)
.ok_or_else(|| anyhow::anyhow!("Invalid channel owner directory"))?;
manifest.verify_service_scope(&manifest_dir, &manifest.context)?;
Ok(true)
})();
match result {
Ok(true) => {
check.status = "passed";
check.message = Some("Declared channel input and output digests match the recorded build context; runtime encryption, authorization and independent target/features are not attested.".into());
}
Ok(false) => {
check.message = Some("No channel provenance receipt for the selected owner; channel freshness is not established".into());
}
Err(error) => {
check.status = "failed";
check.message = Some(format!("{error:#}"));
}
}
Some(check)
}
#[cfg(test)]
mod tests {
use super::*;
fn project(artifact: &str) -> ProjectEntry {
serde_json::from_value(serde_json::json!({
"name":"notes", "description":"", "type":"Service",
"metadata":{"language":"rust", "contractArtifactPath":artifact}
}))
.unwrap()
}
#[test]
fn project_local_channel_inventory_checks_standalone_and_respects_filter() {
let root = tempfile::tempdir().unwrap();
let mut worker = project("src/notes/sdk/typescript/contract.json");
worker.metadata.as_mut().unwrap().contract_export = Some(false);
let directory = root.path().join("src/notes/events");
fs::create_dir_all(&directory).unwrap();
fs::write(directory.join("event-contract.json"), "{}").unwrap();
let checks = inspect_local_channels(root.path(), "src", &[worker.clone()], None);
assert_eq!(checks.len(), 1);
assert_eq!(checks[0].status, "not_checked");
fs::write(directory.join("provenance.json"), "{}").unwrap();
assert_eq!(
inspect_local_channels(root.path(), "src", &[worker.clone()], None)[0].status,
"failed"
);
assert!(inspect_local_channels(root.path(), "src", &[worker], Some("other")).is_empty());
}
#[test]
fn absent_evidence_is_unknown_but_malformed_evidence_fails() {
let root = tempfile::tempdir().unwrap();
let project = project("src/client-sdk/typescript/notes/contract.json");
assert_eq!(inspect(root.path(), "src", &project).status, "not_checked");
let directory = root.path().join("src/client-sdk/typescript/notes");
fs::create_dir_all(&directory).unwrap();
fs::write(directory.join("provenance.json"), "{}").unwrap();
assert_eq!(inspect(root.path(), "src", &project).status, "failed");
}
#[test]
fn channel_evidence_is_optional_until_assets_exist_and_malformed_fails() {
let root = tempfile::tempdir().unwrap();
assert!(inspect_channels(root.path(), "src", &[], None).is_none());
let directory = root.path().join("src/client-sdk/events");
fs::create_dir_all(&directory).unwrap();
fs::write(directory.join("event-contract.json"), "{}").unwrap();
assert_eq!(
inspect_channels(root.path(), "src", &[], None)
.unwrap()
.status,
"not_checked"
);
fs::write(directory.join("provenance.json"), "{}").unwrap();
assert_eq!(
inspect_channels(root.path(), "src", &[], None)
.unwrap()
.status,
"failed"
);
}
#[test]
#[cfg(unix)]
fn channel_evidence_cannot_follow_an_external_receipt() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("provenance.json"), "{}").unwrap();
let directory = root.path().join("src/client-sdk");
fs::create_dir_all(&directory).unwrap();
std::os::unix::fs::symlink(outside.path(), directory.join("events")).unwrap();
let check = inspect_channels(root.path(), "src", &[], None).unwrap();
assert_eq!(check.status, "failed");
assert!(check.message.unwrap().contains("leaves application"));
}
#[test]
fn evidence_path_cannot_escape_application() {
let root = tempfile::tempdir().unwrap();
for path in ["../outside/contract.json", "/tmp/outside/contract.json"] {
assert_eq!(inspect(root.path(), "src", &project(path)).status, "failed");
}
#[cfg(unix)]
{
let outside = tempfile::tempdir().unwrap();
fs::write(outside.path().join("provenance.json"), "{}").unwrap();
std::os::unix::fs::symlink(outside.path(), root.path().join("outside")).unwrap();
let result = inspect(root.path(), "src", &project("outside/contract.json"));
assert_eq!(result.status, "failed");
assert!(result.message.unwrap().contains("leaves application"));
}
}
}
pub(super) fn inspect_bindings(root: &Path, modules: &str, project: &ProjectEntry) -> Check {
let mut check = Check {
kind: "managed_http_syntactic_assembly",
project: project.name.clone(),
command: vec![],
status: "not_checked",
exit_code: None,
message: None,
};
let result = (|| -> anyhow::Result<Option<String>> {
let artifact = project
.metadata
.as_ref()
.and_then(|metadata| metadata.contract_artifact_path.as_deref())
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
Path::new(modules)
.join("client-sdk/typescript")
.join(&project.name)
.join("contract.json")
});
anyhow::ensure!(
!artifact.is_absolute()
&& !artifact.components().any(|part| matches!(
part,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)),
"Contract path must stay inside workspace"
);
let receipt = root.join(&artifact).with_file_name("provenance.json");
if !receipt.exists() {
return Ok(None);
}
anyhow::ensure!(
fs::metadata(&receipt)?.len() <= 4 * 1024 * 1024,
"Provenance exceeds size bound"
);
let manifest: forklaunch_core::build::provenance::Manifest =
serde_json::from_slice(&fs::read(&receipt)?)?;
let directory = workspace_diagnostics::project_dir(root, modules, project)
.ok_or_else(|| anyhow::anyhow!("Invalid service directory"))?;
anyhow::ensure!(
manifest.service == project.name,
"Provenance owner mismatch"
);
manifest.verify_service_scope(&directory, &manifest.context)?;
let binding_path = artifact.with_file_name("bindings.json");
let binding_name = binding_path.to_string_lossy();
if !manifest.outputs.contains_key(binding_name.as_ref()) {
return Ok(None);
}
let path = root.join(&binding_path);
anyhow::ensure!(
path.canonicalize()?.starts_with(root.canonicalize()?),
"Binding manifest leaves workspace"
);
anyhow::ensure!(
fs::metadata(&path)?.len() <= 4 * 1024 * 1024,
"Binding manifest exceeds bound"
);
let bindings: forklaunch_core::build::provenance::BindingManifest =
serde_json::from_slice(&fs::read(path)?)?;
bindings.verify_contract(&serde_json::from_slice(&fs::read(root.join(artifact))?)?)?;
Ok(Some(super::assembly::verify(
root,
&directory,
&project.name,
&bindings,
)?))
})();
match result {
Ok(Some(message)) => {
check.status = "passed";
check.message = Some(message)
}
Ok(None) => {
check.message = Some(
"No digest-bound compiler binding manifest; managed assembly coverage is unknown"
.into(),
)
}
Err(error) => {
if error
.downcast_ref::<super::assembly::UnmountedOperation>()
.is_some()
{
check.status = "failed";
}
check.message = Some(format!("{error:#}"));
}
}
check
}