use std::collections::{BTreeMap, BTreeSet};
use serde::Deserialize;
use crate::setup_core::digest;
use crate::provider_v3::error::{Error, Result};
use crate::provider_v3::info::ProjectionProfile;
use crate::provider_v3::reason::WireReason;
use crate::provider_v3::zip;
pub const BUNDLE_DOMAIN: &str = "ai-stp:bundle:v1";
pub const BUNDLE_FORMAT: &str = "ai-stp-bundle/2";
#[cfg(test)]
const RETIRED_BUNDLE_FORMAT_V1: &str = "ai-stp-bundle/1";
pub const BUNDLE_PROTOCOL_VERSION: u32 = 1;
pub const MANIFEST_MEMBER: &str = "bundle.json";
pub const FILES_PREFIX: &str = "files/";
pub const REQUIRED_MEMBERS: &[&str] = &[
"bundle.json",
"setup-passport.json",
"composition-report.json",
"conversion-report.json",
];
const ALLOWED_MODES: &[u32] = &[0o644, 0o755];
pub const CONTRACT_MAX_FILES: u64 = 2000;
pub const CONTRACT_MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;
pub const CONTRACT_MAX_BUNDLE_BYTES: u64 = 64 * 1024 * 1024;
const SECRET_NAMES: &[&str] = &[
".env",
".netrc",
".npmrc",
"credentials",
"id_rsa",
"id_ed25519",
".pgpass",
];
const SECRET_PREFIXES: &[&str] = &[".env."];
const SECRET_SUFFIXES: &[&str] = &[".pem", ".key", ".p12", ".pfx", ".keystore"];
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum FileKind {
#[default]
File,
Symlink,
Hardlink,
Special,
#[serde(other)]
Unknown,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
pub struct ConversionReport {
#[serde(default)]
pub complete: bool,
#[serde(default)]
pub entries: Vec<ConversionEntry>,
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)]
pub struct ConversionEntry {
#[serde(default)]
pub stable_id: String,
#[serde(default)]
pub component_type: String,
#[serde(default)]
pub native_surface: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct BundleFile {
pub path: String,
pub digest: String,
pub byte_length: u64,
pub mode: u32,
#[serde(default)]
pub owner: String,
#[serde(default)]
pub kind: FileKind,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
pub struct Limits {
#[serde(default)]
pub max_files: Option<u64>,
#[serde(default)]
pub max_file_bytes: Option<u64>,
#[serde(default)]
pub max_bundle_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ProjectionProfileBinding {
pub profile_id: String,
pub profile_digest: String,
pub target_scope: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ProjectionArtifactBinding {
pub digest: String,
pub size_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ComponentAdaptationBinding {
pub stable_id: String,
pub version: String,
pub passport_digest: String,
pub adaptation_id: String,
pub projection_artifact: ProjectionArtifactBinding,
pub provider_component_kind: String,
pub projection_kind: String,
#[serde(default)]
pub member_paths: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Manifest {
pub schema_version: u32,
pub bundle_format: String,
pub protocol_version: u32,
pub harness_id: String,
#[serde(default)]
pub target_scope: Option<String>,
#[serde(default)]
pub projection_profile: Option<ProjectionProfileBinding>,
#[serde(default)]
pub component_adaptations: Vec<ComponentAdaptationBinding>,
#[serde(default)]
pub builder_version: String,
pub bundle_digest: String,
#[serde(default)]
pub input_digest: String,
#[serde(default)]
pub conversion_report: ConversionReport,
#[serde(default)]
pub managed_paths: Vec<String>,
#[serde(default)]
pub files: Vec<BundleFile>,
#[serde(default)]
pub limits: Limits,
}
impl Manifest {
#[must_use]
pub fn effective_max_files(&self) -> u64 {
self.limits
.max_files
.map_or(CONTRACT_MAX_FILES, |c| c.min(CONTRACT_MAX_FILES))
}
#[must_use]
pub fn effective_max_file_bytes(&self) -> u64 {
self.limits
.max_file_bytes
.map_or(CONTRACT_MAX_FILE_BYTES, |c| c.min(CONTRACT_MAX_FILE_BYTES))
}
#[must_use]
pub fn effective_max_bundle_bytes(&self) -> u64 {
self.limits
.max_bundle_bytes
.map_or(CONTRACT_MAX_BUNDLE_BYTES, |c| {
c.min(CONTRACT_MAX_BUNDLE_BYTES)
})
}
}
#[derive(Debug, Clone)]
pub struct Bundle {
pub manifest: Manifest,
pub passport: SetupPassport,
pub files: BTreeMap<String, (Vec<u8>, u32)>,
}
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
pub struct SetupPassport {
#[serde(default)]
pub stable_id: String,
#[serde(default)]
pub version: String,
}
#[derive(Debug, Clone, Copy)]
pub struct Claim<'a> {
pub bundle_format: &'a str,
pub bundle_digest: &'a str,
pub artifact_digest: &'a str,
pub bundle_size: u64,
pub harness_id: &'a str,
}
impl Bundle {
pub fn read(bytes: &[u8], claim: Claim<'_>) -> Result<Self> {
if claim.bundle_format != BUNDLE_FORMAT {
return Err(Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("{:?} is not a supported bundle format", claim.bundle_format),
));
}
let actual_length = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
if actual_length != claim.bundle_size {
return Err(Error::refuse(
WireReason::DigestMismatch,
format!(
"the artifact is {actual_length} bytes, not {}",
claim.bundle_size
),
));
}
if digest::of_bytes(bytes) != claim.artifact_digest {
return Err(Error::refuse(
WireReason::DigestMismatch,
"the artifact bytes do not hash to the digest that named them",
));
}
let members = zip::read(bytes)?;
let manifest_bytes = require_members(&members)?;
let manifest = parse_manifest(manifest_bytes)?;
if manifest.bundle_format != claim.bundle_format {
return Err(Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("the manifest declares {:?}", manifest.bundle_format),
));
}
if manifest.protocol_version != BUNDLE_PROTOCOL_VERSION {
return Err(Error::refuse(
WireReason::UnsupportedProtocolVersion,
format!(
"the manifest declares bundle protocol {}, and this reader speaks {BUNDLE_PROTOCOL_VERSION}",
manifest.protocol_version
),
));
}
if manifest.harness_id != claim.harness_id {
return Err(Error::refuse(
WireReason::ProjectionProfileMismatch,
format!(
"the bundle is for harness {:?}, and this provider configures {:?}",
manifest.harness_id, claim.harness_id
),
));
}
check_adaptation_bindings(&manifest)?;
let files = check_files(&manifest, &members, actual_length)?;
check_manifest_digest(manifest_bytes, &manifest.bundle_digest)?;
if manifest.bundle_digest != claim.bundle_digest {
return Err(Error::refuse(
WireReason::DigestMismatch,
"the manifest's identity is not the one the caller named",
));
}
let passport = members
.get(1)
.and_then(|member| serde_json::from_slice::<SetupPassport>(&member.data).ok())
.unwrap_or_default();
Ok(Self {
manifest,
passport,
files,
})
}
pub fn require_projection_profile(&self, expected: &ProjectionProfile) -> Result<()> {
let Some(binding) = self.manifest.projection_profile.as_ref() else {
return Err(Error::refuse(
WireReason::AdaptationBindingMissing,
"bundle v2 has no projection_profile",
));
};
let expected_scope = expected.target_scope.as_deref().unwrap_or("global");
if binding.profile_id != expected.profile_id
|| binding.profile_digest != expected.digest
|| binding.target_scope != expected_scope
{
return Err(Error::refuse(
WireReason::ProjectionProfileMismatch,
"bundle v2 was compiled for a different provider projection profile",
));
}
for adaptation in &self.manifest.component_adaptations {
if !expected
.component_kinds
.contains(&adaptation.provider_component_kind)
|| !expected
.projection_kinds
.contains(&adaptation.projection_kind)
{
return Err(Error::refuse(
WireReason::AdaptationBindingMismatch,
format!(
"adaptation for {:?} names a kind outside the selected profile",
adaptation.stable_id
),
));
}
}
Ok(())
}
}
fn canonical_digest(value: &str) -> bool {
value.len() == 71
&& value.starts_with("sha256:")
&& value[7..]
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn canonical_adaptation_id(value: &str) -> bool {
value.len() == 75
&& value.starts_with("adaptation_")
&& value[11..]
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
fn check_adaptation_bindings(manifest: &Manifest) -> Result<()> {
if manifest.bundle_format != BUNDLE_FORMAT {
return Err(Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("the manifest declares {:?}", manifest.bundle_format),
));
}
let Some(profile) = manifest.projection_profile.as_ref() else {
return Err(Error::refuse(
WireReason::AdaptationBindingMissing,
"bundle v2 has no projection_profile",
));
};
if manifest.component_adaptations.is_empty() {
return Err(Error::refuse(
WireReason::AdaptationBindingMissing,
"bundle v2 has no component_adaptations",
));
}
let manifest_scope = manifest.target_scope.as_deref().unwrap_or("global");
if profile.profile_id.is_empty()
|| !canonical_digest(&profile.profile_digest)
|| profile.target_scope != manifest_scope
{
return Err(Error::refuse(
WireReason::ProjectionProfileMismatch,
"bundle v2's profile binding is incomplete or names another scope",
));
}
let mut prior = None;
let mut owners = BTreeSet::new();
let mut bound_paths = BTreeSet::new();
for binding in &manifest.component_adaptations {
if prior.is_some_and(|value: &str| value >= binding.stable_id.as_str()) {
return Err(Error::refuse(
WireReason::AdaptationBindingMismatch,
"component_adaptations are not strictly sorted by stable_id",
));
}
prior = Some(binding.stable_id.as_str());
if binding.stable_id.is_empty()
|| binding.version.is_empty()
|| !canonical_digest(&binding.passport_digest)
|| !canonical_adaptation_id(&binding.adaptation_id)
|| !canonical_digest(&binding.projection_artifact.digest)
|| binding.projection_artifact.size_bytes == 0
|| binding.provider_component_kind.is_empty()
|| binding.projection_kind.is_empty()
|| binding.member_paths.is_empty()
|| !owners.insert(binding.stable_id.as_str())
{
return Err(Error::refuse(
WireReason::AdaptationBindingMismatch,
format!("adaptation for {:?} is not canonical", binding.stable_id),
));
}
let mut previous_path = None;
for path in &binding.member_paths {
check_path(path)?;
if previous_path.is_some_and(|value: &str| value >= path.as_str()) {
return Err(Error::refuse(
WireReason::AdaptationBindingMismatch,
format!(
"member_paths for {:?} are not strictly sorted",
binding.stable_id
),
));
}
previous_path = Some(path.as_str());
bound_paths.insert((binding.stable_id.as_str(), path.as_str()));
}
}
let file_owners: BTreeSet<&str> = manifest
.files
.iter()
.map(|file| file.owner.as_str())
.collect();
if file_owners != owners
|| manifest
.files
.iter()
.any(|file| !bound_paths.contains(&(file.owner.as_str(), file.path.as_str())))
{
return Err(Error::refuse(
WireReason::AdaptationBindingMismatch,
"bundle files do not close over the declared component adaptations",
));
}
Ok(())
}
fn require_members(members: &[zip::Member]) -> Result<&[u8]> {
for (index, required) in REQUIRED_MEMBERS.iter().enumerate() {
match members.get(index) {
Some(member) if member.name == *required => {}
Some(member) => {
return Err(Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("member {index} is {:?}, not {required:?}", member.name),
));
}
None => {
return Err(Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("the bundle has no {required:?}"),
));
}
}
}
let mut seen = BTreeSet::new();
for member in members {
if !seen.insert(member.name.as_str()) {
return Err(Error::refuse(
WireReason::PathDuplicate,
format!("member {:?} appears twice", member.name),
));
}
}
members
.first()
.map(|member| member.data.as_slice())
.ok_or_else(|| Error::refuse(WireReason::UnsupportedBundleFormat, "the bundle is empty"))
}
fn parse_manifest(bytes: &[u8]) -> Result<Manifest> {
serde_json::from_slice(bytes).map_err(|source| {
Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("{MANIFEST_MEMBER} does not parse: {source}"),
)
})
}
fn check_manifest_digest(bytes: &[u8], declared: &str) -> Result<()> {
let mut value: serde_json::Value = serde_json::from_slice(bytes).map_err(|source| {
Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("{MANIFEST_MEMBER} does not parse: {source}"),
)
})?;
let Some(object) = value.as_object_mut() else {
return Err(Error::refuse(
WireReason::UnsupportedBundleFormat,
format!("{MANIFEST_MEMBER} is not an object"),
));
};
object.remove("bundle_digest");
let computed = digest::of_domain_canonical_json(BUNDLE_DOMAIN, &value)?;
if computed != declared {
return Err(Error::refuse(
WireReason::DigestMismatch,
"the manifest does not hash to the identity it declares",
));
}
Ok(())
}
fn check_files(
manifest: &Manifest,
members: &[zip::Member],
artifact_length: u64,
) -> Result<BTreeMap<String, (Vec<u8>, u32)>> {
let declared = u64::try_from(manifest.files.len()).unwrap_or(u64::MAX);
let max_files = manifest.effective_max_files();
if declared > max_files {
return Err(Error::refuse(
WireReason::LimitExceeded,
format!("{declared} files declared against a limit of {max_files}"),
));
}
let max_bundle = manifest.effective_max_bundle_bytes();
if artifact_length > max_bundle {
return Err(Error::refuse(
WireReason::LimitExceeded,
format!("the artifact is {artifact_length} bytes against a limit of {max_bundle}"),
));
}
let present: BTreeMap<&str, &zip::Member> = members
.iter()
.filter_map(|member| {
member
.name
.strip_prefix(FILES_PREFIX)
.map(|rest| (rest, member))
})
.filter(|(rest, _)| !rest.is_empty() && !rest.ends_with('/'))
.collect();
let mut files = BTreeMap::new();
let mut lowercase: BTreeMap<String, String> = BTreeMap::new();
for record in &manifest.files {
check_record(record, manifest, &mut lowercase)?;
let Some(member) = present.get(record.path.as_str()) else {
return Err(Error::refuse(
WireReason::DigestMismatch,
format!(
"the manifest declares {:?}, which the archive does not carry",
record.path
),
));
};
let actual = u64::try_from(member.data.len()).unwrap_or(u64::MAX);
if actual != record.byte_length {
return Err(Error::refuse(
WireReason::DigestMismatch,
format!(
"{:?} is {actual} bytes, not {}",
record.path, record.byte_length
),
));
}
if digest::of_bytes(&member.data) != record.digest {
return Err(Error::refuse(
WireReason::DigestMismatch,
format!(
"{:?} does not hash to the digest that declares it",
record.path
),
));
}
if files
.insert(record.path.clone(), (member.data.clone(), record.mode))
.is_some()
{
return Err(Error::refuse(
WireReason::PathDuplicate,
format!("{:?} is declared twice", record.path),
));
}
}
for name in present.keys() {
if !files.contains_key(*name) {
return Err(Error::refuse(
WireReason::UnsupportedNativeSurface,
format!("the archive carries {name:?}, which the manifest does not declare"),
));
}
}
Ok(files)
}
fn check_record(
record: &BundleFile,
manifest: &Manifest,
lowercase: &mut BTreeMap<String, String>,
) -> Result<()> {
check_path(&record.path)?;
match record.kind {
FileKind::File => {}
FileKind::Symlink | FileKind::Hardlink => {
return Err(Error::refuse(
WireReason::LinkNotAllowed,
format!(
"{:?} is declared as a link, which is never materialized",
record.path
),
));
}
FileKind::Special | FileKind::Unknown => {
return Err(Error::refuse(
WireReason::SpecialFileNotAllowed,
format!("{:?} is declared as neither a file nor a link", record.path),
));
}
}
if !ALLOWED_MODES.contains(&record.mode) {
return Err(Error::refuse(
WireReason::UnsupportedNativeSurface,
format!(
"{:?} declares mode {:o}, which is not allowed",
record.path, record.mode
),
));
}
if record.byte_length > manifest.effective_max_file_bytes() {
return Err(Error::refuse(
WireReason::LimitExceeded,
format!("{:?} is larger than the declared file limit", record.path),
));
}
if let Some(other) = lowercase.insert(record.path.to_lowercase(), record.path.clone())
&& other != record.path
{
return Err(Error::refuse(
WireReason::PathDuplicate,
format!("{:?} and {other:?} differ only in case", record.path),
));
}
Ok(())
}
const RESERVED_STEMS: &[&str] = &[
"con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", "com5", "com6", "com7", "com8",
"com9", "com¹", "com²", "com³", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8",
"lpt9", "lpt¹", "lpt²", "lpt³",
];
const RESERVED_IN_A_NAME: &[char] = &['<', '>', '"', '|', '?', '*'];
fn names_a_device(segment: &str) -> Option<&'static str> {
let stem = segment.split_once('.').map_or(segment, |(head, _)| head);
let folded = stem.to_lowercase();
RESERVED_STEMS.iter().copied().find(|name| folded == *name)
}
fn check_segment(path: &str, segment: &str) -> Result<()> {
if segment.is_empty() || segment == "." {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!("{path:?} has an empty or bare-dot segment"),
));
}
if segment == ".." {
return Err(Error::refuse(
WireReason::PathEscapesTarget,
format!("{path:?} climbs out of the target"),
));
}
if segment.len() > 255 {
return Err(Error::refuse(
WireReason::LimitExceeded,
format!("{path:?} has a segment longer than 255 bytes"),
));
}
if let Some(device) = names_a_device(segment) {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!(
"{path:?} has the segment {segment:?}, which names the reserved device \
{device} on Windows -- with or without an extension, and in any case"
),
));
}
if let Some(bad) = segment.chars().find(|c| RESERVED_IN_A_NAME.contains(c)) {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!(
"{path:?} has the segment {segment:?}, which carries {bad:?}, one of the \
characters Windows reserves inside a name"
),
));
}
if segment.contains(':') {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!(
"{path:?} has the segment {segment:?}, which carries a colon; on Windows \
that opens an alternate data stream rather than naming a file, wherever \
in the segment it appears"
),
));
}
if segment.ends_with(' ') || segment.ends_with('.') {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!(
"{path:?} has the segment {segment:?}, which ends in a space or a period; \
Windows does not give a file or directory such a name"
),
));
}
Ok(())
}
fn check_path(path: &str) -> Result<()> {
if path.is_empty() {
return Err(Error::refuse(
WireReason::PathNotRelative,
"a bundle path is not empty",
));
}
if path.len() > 1024 {
return Err(Error::refuse(
WireReason::LimitExceeded,
format!("{path:?} is longer than 1024 bytes"),
));
}
if path.starts_with('/') || path.starts_with('~') || path.starts_with('\\') {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!("{path:?} is not relative"),
));
}
if path.as_bytes().get(1) == Some(&b':') {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!("{path:?} carries a drive letter"),
));
}
for segment in path.split('/') {
check_segment(path, segment)?;
}
if path.chars().any(char::is_control) {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!("{path:?} contains a control character"),
));
}
if path.contains('\\') {
return Err(Error::refuse(
WireReason::PathNotRelative,
format!("{path:?} uses a backslash separator"),
));
}
let name = path.rsplit('/').next().unwrap_or(path);
if SECRET_NAMES.contains(&name)
|| SECRET_PREFIXES
.iter()
.any(|prefix| name.starts_with(prefix))
|| SECRET_SUFFIXES.iter().any(|suffix| name.ends_with(suffix))
{
return Err(Error::refuse(
WireReason::SpecialFileNotAllowed,
format!("{path:?} is named like a credential and is refused by name"),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use crate::provider_v3::zip::build::{Entry, write};
fn document(name: &str) -> Entry {
Entry {
name: name.to_owned(),
data: b"{}".to_vec(),
mode: 0o644,
}
}
struct Built {
bytes: Vec<u8>,
claim_digest: String,
artifact_digest: String,
length: u64,
}
#[test]
fn a_bundle_may_lower_its_limits_and_may_not_raise_them() {
let hostile: Manifest = serde_json::from_value(serde_json::json!({
"schema_version": 1,
"bundle_format": BUNDLE_FORMAT,
"protocol_version": BUNDLE_PROTOCOL_VERSION,
"harness_id": "test",
"bundle_digest": "sha256:aa",
"limits": {
"max_files": 999_999,
"max_file_bytes": 8_u64 * 1024 * 1024 * 1024,
"max_bundle_bytes": 8_u64 * 1024 * 1024 * 1024,
}
}))
.unwrap();
assert_eq!(hostile.effective_max_files(), CONTRACT_MAX_FILES);
assert_eq!(hostile.effective_max_file_bytes(), CONTRACT_MAX_FILE_BYTES);
assert_eq!(
hostile.effective_max_bundle_bytes(),
CONTRACT_MAX_BUNDLE_BYTES
);
let stricter: Manifest = serde_json::from_value(serde_json::json!({
"schema_version": 1,
"bundle_format": BUNDLE_FORMAT,
"protocol_version": BUNDLE_PROTOCOL_VERSION,
"harness_id": "test",
"bundle_digest": "sha256:aa",
"limits": {"max_files": 3, "max_file_bytes": 16, "max_bundle_bytes": 32}
}))
.unwrap();
assert_eq!(stricter.effective_max_files(), 3);
assert_eq!(stricter.effective_max_file_bytes(), 16);
assert_eq!(stricter.effective_max_bundle_bytes(), 32);
let silent: Manifest = serde_json::from_value(serde_json::json!({
"schema_version": 1,
"bundle_format": BUNDLE_FORMAT,
"protocol_version": BUNDLE_PROTOCOL_VERSION,
"harness_id": "test",
"bundle_digest": "sha256:aa",
}))
.unwrap();
assert_eq!(silent.effective_max_files(), CONTRACT_MAX_FILES);
assert_eq!(silent.effective_max_file_bytes(), CONTRACT_MAX_FILE_BYTES);
assert_eq!(
silent.effective_max_bundle_bytes(),
CONTRACT_MAX_BUNDLE_BYTES
);
}
fn v2_manifest() -> Manifest {
serde_json::from_value(serde_json::json!({
"schema_version": 1,
"bundle_format": BUNDLE_FORMAT,
"protocol_version": BUNDLE_PROTOCOL_VERSION,
"harness_id": "test",
"bundle_digest": "sha256:aa",
"projection_profile": {
"profile_id": "test/native-files/2",
"profile_digest": "sha256:".to_owned() + &"4".repeat(64),
"target_scope": "global"
},
"component_adaptations": [{
"stable_id": "component_a",
"version": "1.0",
"passport_digest": "sha256:".to_owned() + &"1".repeat(64),
"adaptation_id": "adaptation_".to_owned() + &"2".repeat(64),
"projection_artifact": {
"digest": "sha256:".to_owned() + &"3".repeat(64),
"size_bytes": 128
},
"provider_component_kind": "skill",
"projection_kind": "native_files",
"member_paths": ["skills/a.md"]
}],
"files": [{
"path": "skills/a.md",
"digest": "sha256:".to_owned() + &"5".repeat(64),
"byte_length": 1,
"mode": 420,
"owner": "component_a"
}]
}))
.unwrap()
}
#[test]
fn bundle_v2_requires_complete_sorted_adaptation_bindings() {
let manifest = v2_manifest();
check_adaptation_bindings(&manifest).unwrap();
let mut missing = manifest.clone();
missing.component_adaptations.clear();
assert_eq!(
check_adaptation_bindings(&missing).unwrap_err().reason(),
Some(WireReason::AdaptationBindingMissing)
);
let mut unbound = manifest;
unbound.files[0].owner = "component_b".to_owned();
assert_eq!(
check_adaptation_bindings(&unbound).unwrap_err().reason(),
Some(WireReason::AdaptationBindingMismatch)
);
}
#[test]
fn bundle_v2_is_bound_to_the_exact_provider_profile() {
let manifest = v2_manifest();
let profile = ProjectionProfile::new(
"test/native-files/2",
&[crate::provider_v3::ComponentKind::Skill],
&[crate::provider_v3::ProjectionKind::NativeFiles],
&["skills"],
&[BUNDLE_FORMAT],
CONTRACT_MAX_FILES,
CONTRACT_MAX_BUNDLE_BYTES,
)
.unwrap();
let bundle = Bundle {
manifest: manifest.clone(),
passport: SetupPassport::default(),
files: BTreeMap::new(),
};
assert_eq!(
bundle
.require_projection_profile(&profile)
.unwrap_err()
.reason(),
Some(WireReason::ProjectionProfileMismatch),
"the manifest carries a deliberately different profile digest"
);
let mut exact = manifest;
exact.projection_profile.as_mut().unwrap().profile_digest = profile.digest.clone();
Bundle {
manifest: exact,
passport: SetupPassport::default(),
files: BTreeMap::new(),
}
.require_projection_profile(&profile)
.unwrap();
}
fn build(files: &[(&str, &str, u32)]) -> Built {
let mut member_paths = files.iter().map(|(path, _, _)| *path).collect::<Vec<_>>();
member_paths.sort_unstable();
let records: Vec<serde_json::Value> = files
.iter()
.map(|(path, body, mode)| {
serde_json::json!({
"schema_version": 1,
"path": path,
"digest": digest::of_bytes(body.as_bytes()),
"byte_length": body.len(),
"mode": mode,
"owner": "component_a",
})
})
.collect();
let mut manifest = serde_json::json!({
"schema_version": 1,
"bundle_format": BUNDLE_FORMAT,
"protocol_version": BUNDLE_PROTOCOL_VERSION,
"harness_id": "test",
"builder_version": "0.1.0",
"input_digest": "sha256:".to_owned() + &"3".repeat(64),
"projection_profile": {
"profile_id": "test/native-files/2",
"profile_digest": "sha256:".to_owned() + &"4".repeat(64),
"target_scope": "global"
},
"component_adaptations": [{
"stable_id": "component_a",
"version": "1.0",
"passport_digest": "sha256:".to_owned() + &"1".repeat(64),
"adaptation_id": "adaptation_".to_owned() + &"2".repeat(64),
"projection_artifact": {
"digest": "sha256:".to_owned() + &"3".repeat(64),
"size_bytes": 128
},
"provider_component_kind": "instruction",
"projection_kind": "native_files",
"member_paths": member_paths
}],
"managed_paths": files.iter().map(|(path, _, _)| *path).collect::<Vec<_>>(),
"files": records,
"limits": {
"max_files": 2000,
"max_file_bytes": 4 * 1024 * 1024,
"max_bundle_bytes": 64 * 1024 * 1024,
},
});
let bundle_digest = digest::of_domain_canonical_json(BUNDLE_DOMAIN, &manifest).unwrap();
manifest["bundle_digest"] = serde_json::json!(bundle_digest);
let manifest_bytes = crate::setup_core::canonical::to_canonical_bytes(&manifest).unwrap();
let mut entries = vec![Entry {
name: MANIFEST_MEMBER.to_owned(),
data: manifest_bytes,
mode: 0o644,
}];
for name in REQUIRED_MEMBERS.iter().skip(1) {
entries.push(document(name));
}
for (path, body, mode) in files {
entries.push(Entry {
name: format!("{FILES_PREFIX}{path}"),
data: body.as_bytes().to_vec(),
mode: *mode,
});
}
let bytes = write(&entries);
let artifact_digest = digest::of_bytes(&bytes);
let length = bytes.len() as u64;
Built {
bytes,
claim_digest: bundle_digest,
artifact_digest,
length,
}
}
fn build_declaring(
files: &[(&str, &str, u32)],
bend: impl Fn(&mut serde_json::Value),
) -> Built {
let plain = build(files);
let mut manifest: serde_json::Value = serde_json::from_slice(
&zip::read(&plain.bytes)
.unwrap()
.into_iter()
.find(|member| member.name == MANIFEST_MEMBER)
.unwrap()
.data,
)
.unwrap();
bend(&mut manifest);
manifest["bundle_digest"] = serde_json::Value::Null;
let mut without = manifest.clone();
without.as_object_mut().unwrap().remove("bundle_digest");
let bundle_digest = digest::of_domain_canonical_json(BUNDLE_DOMAIN, &without).unwrap();
manifest["bundle_digest"] = serde_json::json!(bundle_digest);
let manifest_bytes = crate::setup_core::canonical::to_canonical_bytes(&manifest).unwrap();
let mut entries: Vec<Entry> = Vec::new();
for member in zip::read(&plain.bytes).unwrap() {
let data = if member.name == MANIFEST_MEMBER {
manifest_bytes.clone()
} else {
member.data
};
entries.push(Entry {
name: member.name,
data,
mode: member.mode.unwrap_or(0o644),
});
}
let bytes = write(&entries);
let artifact_digest = digest::of_bytes(&bytes);
let length = bytes.len() as u64;
Built {
bytes,
claim_digest: bundle_digest,
artifact_digest,
length,
}
}
fn claim(built: &Built) -> Claim<'_> {
Claim {
bundle_format: BUNDLE_FORMAT,
bundle_digest: &built.claim_digest,
artifact_digest: &built.artifact_digest,
bundle_size: built.length,
harness_id: "test",
}
}
#[test]
fn a_consistent_bundle_is_read_and_carries_its_files() {
let built = build(&[
("AGENTS.md", "# hello\n", 0o644),
("skills/a.md", "skill", 0o644),
]);
let bundle = Bundle::read(&built.bytes, claim(&built)).unwrap();
assert_eq!(bundle.manifest.harness_id, "test");
assert_eq!(bundle.files.len(), 2);
assert_eq!(bundle.files["AGENTS.md"].0, b"# hello\n");
assert_eq!(bundle.files["skills/a.md"].1, 0o644);
}
#[test]
fn an_executable_mode_is_carried_because_some_surfaces_are_executable() {
let built = build(&[("hooks/run.sh", "#!/bin/sh\n", 0o755)]);
let bundle = Bundle::read(&built.bytes, claim(&built)).unwrap();
assert_eq!(bundle.files["hooks/run.sh"].1, 0o755);
}
#[test]
fn the_raw_bytes_are_checked_before_the_parser_ever_runs() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut claim = claim(&built);
let wrong = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
claim.artifact_digest = wrong;
let error = Bundle::read(&built.bytes, claim).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::DigestMismatch));
assert!(error.detail().contains("do not hash"), "{error}");
}
#[test]
fn a_size_that_disagrees_with_the_bytes_is_refused_first() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut claim = claim(&built);
claim.bundle_size = built.length + 1;
assert!(
Bundle::read(&built.bytes, claim)
.unwrap_err()
.detail()
.contains("bytes, not")
);
}
#[test]
fn a_bundle_for_another_harness_is_a_profile_mismatch_not_a_generic_refusal() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut claim = claim(&built);
claim.harness_id = "other";
let error = Bundle::read(&built.bytes, claim).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::ProjectionProfileMismatch));
}
#[test]
fn a_manifest_identity_the_caller_did_not_name_is_refused() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut claim = claim(&built);
let other = "sha256:1111111111111111111111111111111111111111111111111111111111111111";
claim.bundle_digest = other;
let error = Bundle::read(&built.bytes, claim).unwrap_err();
assert!(
error.detail().contains("not the one the caller named"),
"{error}"
);
}
#[test]
fn a_file_whose_bytes_do_not_match_its_record_is_refused() {
let mut built = build(&[("AGENTS.md", "hello", 0o644)]);
let position = built.bytes.windows(5).position(|w| w == b"hello").unwrap();
built.bytes[position] = b'j';
built.artifact_digest = digest::of_bytes(&built.bytes);
let error = Bundle::read(&built.bytes, claim(&built)).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::DigestMismatch));
}
#[test]
fn every_hostile_path_shape_is_refused_by_its_own_reason() {
for (path, reason) in [
("/etc/passwd", WireReason::PathNotRelative),
("~/.ssh/config", WireReason::PathNotRelative),
("C:/Windows/system32", WireReason::PathNotRelative),
("../outside", WireReason::PathEscapesTarget),
("a/../../b", WireReason::PathEscapesTarget),
("a//b", WireReason::PathNotRelative),
("./a", WireReason::PathNotRelative),
("a\\b", WireReason::PathNotRelative),
("", WireReason::PathNotRelative),
] {
let error = check_path(path).unwrap_err();
assert_eq!(error.reason(), Some(reason), "{path:?} gave {error}");
}
}
#[test]
fn a_path_windows_cannot_write_is_refused_here_rather_than_discovered_there() {
for path in [
"NUL",
"nul",
"CON.txt",
"nul.tar.gz",
"com9.md",
"LPT3",
"skills/aux/SKILL.md",
"commands/build./x.md",
"a ",
"plugins/a:b/c.md",
"stream.md:hidden",
"COM\u{b9}",
"lpt\u{b3}.txt",
"a<b.md",
"a>b.md",
"a\"b.md",
"a|b.md",
"a?b.md",
"a*b.md",
] {
let error = check_path(path).unwrap_err();
assert_eq!(
error.reason(),
Some(WireReason::PathNotRelative),
"{path:?} gave {error}"
);
}
}
#[test]
fn a_name_that_merely_starts_like_a_device_is_kept() {
for path in [
"console.md",
"commands/connect.json",
"auxiliary.md",
"skills/nullable/SKILL.md",
"com10.txt",
"prnt.md",
"a.con",
] {
assert!(check_path(path).is_ok(), "{path:?} was refused");
}
}
#[test]
fn a_path_named_like_a_credential_is_refused_without_opening_it() {
for path in [
".env",
".env.production",
".netrc",
".npmrc",
"credentials",
"keys/id_rsa",
"keys/id_ed25519",
".pgpass",
"certs/server.pem",
"private.key",
"bundle.p12",
"store.pfx",
"keys/ring.keystore",
] {
let error = check_path(path).unwrap_err();
assert_eq!(
error.reason(),
Some(WireReason::SpecialFileNotAllowed),
"{path}"
);
}
}
#[test]
fn a_path_only_shaped_like_a_credential_installs() {
for path in [
"id_rsa.pub",
"auth.json",
"auth.json.sample",
".credentials.json",
"docs/secrets.md",
"not-credentials.txt",
"ID_RSA",
"certs/pembroke.pub",
] {
assert!(check_path(path).is_ok(), "{path:?} was refused");
}
}
#[test]
fn a_control_character_in_a_path_is_refused() {
assert!(check_path("a\u{1}b").is_err());
}
#[test]
fn an_over_long_path_or_segment_is_refused() {
for path in [&"a".repeat(1025), &format!("dir/{}", "a".repeat(256))] {
let error = check_path(path).unwrap_err();
assert_eq!(
error.reason(),
Some(WireReason::LimitExceeded),
"{}",
error.detail()
);
}
}
#[test]
fn two_paths_differing_only_in_case_are_a_duplicate() {
let built = build(&[("skills/a.md", "one", 0o644), ("skills/A.md", "two", 0o644)]);
let error = Bundle::read(&built.bytes, claim(&built)).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::PathDuplicate), "{error}");
}
#[test]
fn a_manifest_record_that_is_a_link_is_refused_as_one() {
let built = build_declaring(&[("AGENTS.md", "x", 0o644)], |manifest| {
manifest["files"][0]["kind"] = serde_json::json!("symlink");
});
let error = Bundle::read(&built.bytes, claim(&built)).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::LinkNotAllowed), "{error}");
}
#[test]
fn an_archive_member_the_manifest_never_declared_is_refused() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut entries: Vec<Entry> = Vec::new();
for member in zip::read(&built.bytes).unwrap() {
entries.push(Entry {
name: member.name,
data: member.data,
mode: 0o644,
});
}
entries.push(Entry {
name: format!("{FILES_PREFIX}undeclared.md"),
data: b"sneaky".to_vec(),
mode: 0o644,
});
let bytes = write(&entries);
let artifact = digest::of_bytes(&bytes);
let claim = Claim {
bundle_format: BUNDLE_FORMAT,
bundle_digest: &built.claim_digest,
artifact_digest: &artifact,
bundle_size: bytes.len() as u64,
harness_id: "test",
};
let error = Bundle::read(&bytes, claim).unwrap_err();
assert!(error.detail().contains("undeclared.md"), "{error}");
}
#[test]
fn the_four_documents_are_required_in_order() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut members = zip::read(&built.bytes).unwrap();
members.swap(1, 2);
let entries: Vec<Entry> = members
.into_iter()
.map(|m| Entry {
name: m.name,
data: m.data,
mode: 0o644,
})
.collect();
let bytes = write(&entries);
let artifact = digest::of_bytes(&bytes);
let claim = Claim {
bundle_format: BUNDLE_FORMAT,
bundle_digest: &built.claim_digest,
artifact_digest: &artifact,
bundle_size: bytes.len() as u64,
harness_id: "test",
};
let error = Bundle::read(&bytes, claim).unwrap_err();
assert!(error.detail().contains("not"), "{error}");
}
#[test]
fn a_format_tag_this_reader_does_not_know_is_refused_before_anything_else() {
let built = build(&[("AGENTS.md", "x", 0o644)]);
let mut claim = claim(&built);
claim.bundle_format = RETIRED_BUNDLE_FORMAT_V1;
let error = Bundle::read(&built.bytes, claim).unwrap_err();
assert_eq!(error.reason(), Some(WireReason::UnsupportedBundleFormat));
}
}