Skip to content

Commit 0b19f0a

Browse files
Bound auxiliary artifact verification state
Signed-off-by: Nelson Spence <nelson@projectnavi.ai>
1 parent 5bd7a2a commit 0b19f0a

6 files changed

Lines changed: 206 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Added bounded parser/report defaults to `ordvec-manifest` verification for
1313
manifest JSON size, row-identity JSONL line length, row count,
14-
duplicate-tracking memory, report issue count, and SQLite cached report size.
14+
duplicate-tracking memory, auxiliary artifact declaration count, report issue
15+
count, and SQLite cached report size.
1516

1617
### Added
1718

ordvec-manifest/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,21 @@ Verification uses bounded parser/report defaults on both CLI and library paths:
3232
- row-identity JSONL line: 64 KiB;
3333
- row-identity JSONL rows: 10,000,000;
3434
- row-identity duplicate-tracking `db_id` bytes: 64 MiB;
35+
- auxiliary artifact declarations: 1,024;
3536
- collected report issues: 1,024, after which a
3637
`verification_report_issue_limit_exceeded` issue is emitted;
3738
- SQLite cached report JSON: 4 MiB.
3839

3940
The CLI exposes matching override flags on `inspect`, `verify`, `create`,
4041
`sqlite verify`, and `sqlite activate`: `--max-manifest-bytes`,
4142
`--max-row-map-line-bytes`, `--max-row-map-rows`,
42-
`--max-row-map-tracked-id-bytes`, `--max-report-issues`, and
43-
`--max-cached-report-bytes`. Library callers can override the same ceilings
44-
via `VerifyOptions::limits`. Stable limit codes exposed through verification
45-
reports are `row_identity_line_too_large`,
43+
`--max-row-map-tracked-id-bytes`, `--max-auxiliary-artifacts`,
44+
`--max-report-issues`, and `--max-cached-report-bytes`. Library callers can
45+
override the same ceilings via `VerifyOptions::limits`. Stable limit codes
46+
exposed through verification reports are `row_identity_line_too_large`,
4647
`row_identity_row_count_limit_exceeded`,
47-
`row_identity_duplicate_tracking_limit_exceeded`, and
48+
`row_identity_duplicate_tracking_limit_exceeded`,
49+
`auxiliary_artifact_count_limit_exceeded`, and
4850
`verification_report_issue_limit_exceeded`. `ManifestError::code()` reports
4951
`manifest_file_too_large`, `row_identity_line_too_large`,
5052
`row_identity_row_count_limit_exceeded`,

ordvec-manifest/src/lib.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub const DEFAULT_MAX_MANIFEST_BYTES: u64 = 1024 * 1024;
1818
pub const DEFAULT_MAX_ROW_IDENTITY_JSONL_LINE_BYTES: usize = 64 * 1024;
1919
pub const DEFAULT_MAX_ROW_IDENTITY_ROWS: usize = 10_000_000;
2020
pub const DEFAULT_MAX_ROW_IDENTITY_TRACKED_DB_ID_BYTES: usize = 64 * 1024 * 1024;
21+
pub const DEFAULT_MAX_AUXILIARY_ARTIFACTS: usize = 1024;
2122
pub const DEFAULT_MAX_REPORT_ISSUES: usize = 1024;
2223
pub const DEFAULT_MAX_CACHED_REPORT_BYTES: u64 = 4 * 1024 * 1024;
2324

@@ -157,7 +158,7 @@ pub fn verify_index_manifest(
157158

158159
pub fn verify_manifest(document: &ManifestDocument, options: VerifyOptions) -> VerificationReport {
159160
let mut report = VerificationReport::new(Some(document.manifest.manifest_id.clone()));
160-
validate_manifest_shape(&document.manifest, &mut report);
161+
validate_manifest_shape(&document.manifest, &options.limits, &mut report);
161162

162163
let artifact_display_path = document.manifest.artifact.path.clone();
163164
report.artifact.manifest_path = Some(artifact_display_path.clone());
@@ -228,7 +229,11 @@ pub fn verify_manifest(document: &ManifestDocument, options: VerifyOptions) -> V
228229
report
229230
}
230231

231-
fn validate_manifest_shape(manifest: &IndexManifest, report: &mut VerificationReport) {
232+
fn validate_manifest_shape(
233+
manifest: &IndexManifest,
234+
limits: &ResourceLimits,
235+
report: &mut VerificationReport,
236+
) {
232237
if manifest.schema_version != SCHEMA_VERSION {
233238
report.error(
234239
"schema_version_unsupported",
@@ -321,7 +326,7 @@ fn validate_manifest_shape(manifest: &IndexManifest, report: &mut VerificationRe
321326
}
322327
}
323328

324-
validate_auxiliary_artifact_shape(manifest, report);
329+
validate_auxiliary_artifact_shape(manifest, limits, report);
325330

326331
validate_optional_non_empty(
327332
"embedding_model_revision_empty",
@@ -401,7 +406,14 @@ fn validate_manifest_shape(manifest: &IndexManifest, report: &mut VerificationRe
401406
}
402407
}
403408

404-
fn validate_auxiliary_artifact_shape(manifest: &IndexManifest, report: &mut VerificationReport) {
409+
fn validate_auxiliary_artifact_shape(
410+
manifest: &IndexManifest,
411+
limits: &ResourceLimits,
412+
report: &mut VerificationReport,
413+
) {
414+
if !check_auxiliary_artifact_count(manifest, limits, report) {
415+
return;
416+
}
405417
let mut names = HashSet::new();
406418
for artifact in &manifest.auxiliary_artifacts {
407419
let name = artifact.name.trim();
@@ -1176,6 +1188,9 @@ fn verify_auxiliary_artifacts(
11761188
options: &VerifyOptions,
11771189
report: &mut VerificationReport,
11781190
) {
1191+
if !check_auxiliary_artifact_count(&document.manifest, &options.limits, report) {
1192+
return;
1193+
}
11791194
for artifact in auxiliary_artifacts_in_report_order(&document.manifest) {
11801195
let mut entry = AuxiliaryArtifactReport {
11811196
name: artifact.name.clone(),
@@ -1264,6 +1279,33 @@ fn verify_auxiliary_artifacts(
12641279
}
12651280
}
12661281

1282+
fn check_auxiliary_artifact_count(
1283+
manifest: &IndexManifest,
1284+
limits: &ResourceLimits,
1285+
report: &mut VerificationReport,
1286+
) -> bool {
1287+
let count = manifest.auxiliary_artifacts.len();
1288+
if count <= limits.max_auxiliary_artifacts {
1289+
return true;
1290+
}
1291+
if !report
1292+
.errors
1293+
.iter()
1294+
.any(|issue| issue.code == "auxiliary_artifact_count_limit_exceeded")
1295+
{
1296+
push_report_issue_bounded(
1297+
&mut report.errors,
1298+
limits,
1299+
"auxiliary_artifact_count_limit_exceeded",
1300+
format!(
1301+
"auxiliary_artifacts has {count} entries, exceeding max_auxiliary_artifacts={}",
1302+
limits.max_auxiliary_artifacts
1303+
),
1304+
);
1305+
}
1306+
false
1307+
}
1308+
12671309
fn auxiliary_artifacts_in_report_order(manifest: &IndexManifest) -> Vec<&AuxiliaryArtifact> {
12681310
let mut artifacts: Vec<_> = manifest.auxiliary_artifacts.iter().collect();
12691311
artifacts.sort_by(|left, right| {
@@ -1473,6 +1515,7 @@ pub struct ResourceLimits {
14731515
pub max_row_identity_jsonl_line_bytes: usize,
14741516
pub max_row_identity_rows: usize,
14751517
pub max_row_identity_tracked_db_id_bytes: usize,
1518+
pub max_auxiliary_artifacts: usize,
14761519
pub max_report_issues: usize,
14771520
pub max_cached_report_bytes: u64,
14781521
}
@@ -1484,6 +1527,7 @@ impl Default for ResourceLimits {
14841527
max_row_identity_jsonl_line_bytes: DEFAULT_MAX_ROW_IDENTITY_JSONL_LINE_BYTES,
14851528
max_row_identity_rows: DEFAULT_MAX_ROW_IDENTITY_ROWS,
14861529
max_row_identity_tracked_db_id_bytes: DEFAULT_MAX_ROW_IDENTITY_TRACKED_DB_ID_BYTES,
1530+
max_auxiliary_artifacts: DEFAULT_MAX_AUXILIARY_ARTIFACTS,
14871531
max_report_issues: DEFAULT_MAX_REPORT_ISSUES,
14881532
max_cached_report_bytes: DEFAULT_MAX_CACHED_REPORT_BYTES,
14891533
}
@@ -1863,6 +1907,7 @@ pub struct VerificationReport {
18631907
pub checked_at: String,
18641908
pub manifest_id: Option<String>,
18651909
pub artifact: ArtifactReport,
1910+
#[serde(default)]
18661911
pub auxiliary_artifacts: Vec<AuxiliaryArtifactReport>,
18671912
pub row_identity: RowIdentityReport,
18681913
pub calibration: CalibrationReport,

ordvec-manifest/src/main.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ struct LimitArgs {
131131
#[arg(long)]
132132
max_row_map_tracked_id_bytes: Option<usize>,
133133
#[arg(long)]
134+
max_auxiliary_artifacts: Option<usize>,
135+
#[arg(long)]
134136
max_report_issues: Option<usize>,
135137
#[arg(long)]
136138
max_cached_report_bytes: Option<u64>,
@@ -151,6 +153,9 @@ impl LimitArgs {
151153
if let Some(value) = self.max_row_map_tracked_id_bytes {
152154
limits.max_row_identity_tracked_db_id_bytes = value;
153155
}
156+
if let Some(value) = self.max_auxiliary_artifacts {
157+
limits.max_auxiliary_artifacts = value;
158+
}
154159
if let Some(value) = self.max_report_issues {
155160
limits.max_report_issues = value;
156161
}

ordvec-manifest/src/sqlite.rs

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -465,9 +465,6 @@ fn current_auxiliary_artifacts_sha256(
465465
}
466466
let mut report = VerificationReport::new(None);
467467
verify_auxiliary_artifacts(document, options, &mut report);
468-
if !report.errors.is_empty() {
469-
return Ok(None);
470-
}
471468
auxiliary_artifacts_sha256_from_report(document, &report)
472469
}
473470

@@ -484,35 +481,27 @@ fn auxiliary_artifacts_sha256_from_report(
484481

485482
let mut entries = Vec::with_capacity(report.auxiliary_artifacts.len());
486483
for entry in &report.auxiliary_artifacts {
487-
match entry.state {
484+
let state = match entry.state {
488485
AuxiliaryArtifactState::Verified => {
489486
let (Some(sha256), Some(size_bytes)) = (entry.sha256.as_ref(), entry.size_bytes)
490487
else {
491488
return Ok(None);
492489
};
493-
entries.push(AuxiliaryArtifactCacheEntry {
494-
name: entry.name.clone(),
495-
path: entry.manifest_path.clone(),
496-
required: entry.required,
497-
state: "verified",
498-
sha256: Some(sha256.clone()),
499-
size_bytes: Some(size_bytes),
500-
});
490+
("verified", Some(sha256.clone()), Some(size_bytes))
501491
}
502-
AuxiliaryArtifactState::OptionalAbsent => {
503-
entries.push(AuxiliaryArtifactCacheEntry {
504-
name: entry.name.clone(),
505-
path: entry.manifest_path.clone(),
506-
required: entry.required,
507-
state: "optional_absent",
508-
sha256: None,
509-
size_bytes: None,
510-
});
511-
}
512-
AuxiliaryArtifactState::MissingRequired | AuxiliaryArtifactState::Failed => {
513-
return Ok(None);
514-
}
515-
}
492+
AuxiliaryArtifactState::OptionalAbsent => ("optional_absent", None, None),
493+
AuxiliaryArtifactState::MissingRequired => ("missing_required", None, None),
494+
AuxiliaryArtifactState::Failed => ("failed", entry.sha256.clone(), entry.size_bytes),
495+
};
496+
entries.push(AuxiliaryArtifactCacheEntry {
497+
name: entry.name.clone(),
498+
path: entry.manifest_path.clone(),
499+
required: entry.required,
500+
state: state.0,
501+
reason_code: entry.reason_code.clone(),
502+
sha256: state.1,
503+
size_bytes: state.2,
504+
});
516505
}
517506

518507
let json = serde_json::to_vec(&entries)?;
@@ -525,6 +514,7 @@ struct AuxiliaryArtifactCacheEntry {
525514
path: String,
526515
required: bool,
527516
state: &'static str,
517+
reason_code: Option<String>,
528518
sha256: Option<String>,
529519
size_bytes: Option<u64>,
530520
}

ordvec-manifest/tests/manifest.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,46 @@ fn auxiliary_artifact_schema_rejects_unknown_fields_and_duplicate_names() {
14131413
assert!(parsed.is_err());
14141414
}
14151415

1416+
#[test]
1417+
fn auxiliary_artifact_count_limit_is_enforced_before_verification() {
1418+
let root = tempfile::tempdir().unwrap();
1419+
let (temp, mut manifest, _manifest_path) = identity_manifest(root.path());
1420+
fs::write(temp.path().join("a.bin"), b"a").unwrap();
1421+
fs::write(temp.path().join("b.bin"), b"b").unwrap();
1422+
let a_hash = sha256_file(temp.path().join("a.bin")).unwrap();
1423+
let b_hash = sha256_file(temp.path().join("b.bin")).unwrap();
1424+
manifest.auxiliary_artifacts = vec![
1425+
auxiliary_artifact("a", "a.bin", a_hash, true),
1426+
auxiliary_artifact("b", "b.bin", b_hash, true),
1427+
];
1428+
1429+
let report = verify_manifest_with_base(
1430+
manifest,
1431+
temp.path(),
1432+
VerifyOptions {
1433+
limits: ResourceLimits {
1434+
max_auxiliary_artifacts: 1,
1435+
..ResourceLimits::default()
1436+
},
1437+
..VerifyOptions::default()
1438+
},
1439+
);
1440+
assert!(error_codes(&report).contains(&"auxiliary_artifact_count_limit_exceeded"));
1441+
assert!(report.auxiliary_artifacts.is_empty());
1442+
}
1443+
1444+
#[test]
1445+
fn verification_report_deserializes_missing_auxiliary_artifacts_field() {
1446+
let root = tempfile::tempdir().unwrap();
1447+
let (temp, manifest, _manifest_path) = identity_manifest(root.path());
1448+
let report = verify_manifest_with_base(manifest, temp.path(), VerifyOptions::default());
1449+
let mut value = serde_json::to_value(&report).unwrap();
1450+
value.as_object_mut().unwrap().remove("auxiliary_artifacts");
1451+
1452+
let parsed: ordvec_manifest::VerificationReport = serde_json::from_value(value).unwrap();
1453+
assert!(parsed.auxiliary_artifacts.is_empty());
1454+
}
1455+
14161456
#[test]
14171457
fn attestation_shape_requires_matching_subject_sha256() {
14181458
let root = tempfile::tempdir().unwrap();
@@ -1929,6 +1969,70 @@ fn sqlite_cache_key_includes_auxiliary_artifact_bytes() {
19291969
assert!(error_codes(&cached).contains(&"auxiliary_artifact_sha256_mismatch"));
19301970
}
19311971

1972+
#[cfg(feature = "sqlite")]
1973+
#[test]
1974+
fn sqlite_cache_key_includes_failed_auxiliary_artifact_observed_bytes() {
1975+
let temp = tempfile::tempdir().unwrap();
1976+
let index = write_index(temp.path());
1977+
let manifest_path = temp.path().join("manifest.json");
1978+
let mut manifest = create_manifest_for_index(
1979+
&index,
1980+
CreateRowIdentity::RowIdIdentity,
1981+
"test-embedding",
1982+
&manifest_path,
1983+
)
1984+
.unwrap();
1985+
let sidecar_path = temp.path().join("sidecar.json");
1986+
fs::write(&sidecar_path, b"{\"version\":1}\n").unwrap();
1987+
let expected_hash = sha256_file(&sidecar_path).unwrap();
1988+
manifest.auxiliary_artifacts = vec![auxiliary_artifact(
1989+
"sidecar",
1990+
"sidecar.json",
1991+
expected_hash,
1992+
true,
1993+
)];
1994+
fs::write(
1995+
&manifest_path,
1996+
serde_json::to_string_pretty(&manifest).unwrap(),
1997+
)
1998+
.unwrap();
1999+
let document = load_manifest_file(&manifest_path).unwrap();
2000+
let db = temp.path().join("registry.sqlite");
2001+
2002+
fs::write(&sidecar_path, b"{\"version\":2}\n").unwrap();
2003+
let first_observed = sha256_file(&sidecar_path).unwrap();
2004+
let report = ordvec_manifest::sqlite::verify_with_registry(
2005+
&db,
2006+
&document,
2007+
&manifest_path,
2008+
VerifyOptions::default(),
2009+
true,
2010+
)
2011+
.unwrap();
2012+
assert!(!report.ok);
2013+
assert_eq!(
2014+
report.auxiliary_artifacts[0].sha256.as_deref(),
2015+
Some(first_observed.sha256.as_str())
2016+
);
2017+
2018+
fs::write(&sidecar_path, b"{\"version\":3}\n").unwrap();
2019+
let second_observed = sha256_file(&sidecar_path).unwrap();
2020+
let cached = ordvec_manifest::sqlite::verify_with_registry(
2021+
&db,
2022+
&document,
2023+
&manifest_path,
2024+
VerifyOptions::default(),
2025+
true,
2026+
)
2027+
.unwrap();
2028+
assert!(!cached.ok);
2029+
assert_eq!(
2030+
cached.auxiliary_artifacts[0].sha256.as_deref(),
2031+
Some(second_observed.sha256.as_str())
2032+
);
2033+
assert_ne!(first_observed.sha256, second_observed.sha256);
2034+
}
2035+
19322036
#[cfg(feature = "sqlite")]
19332037
#[test]
19342038
fn sqlite_cache_key_distinguishes_optional_auxiliary_absent_and_present() {
@@ -2041,6 +2145,29 @@ fn sqlite_cache_key_includes_limits_and_bounds_cached_report_size() {
20412145
assert!(cached.ok, "{:?}", cached.errors);
20422146

20432147
let conn = Connection::open(&db).unwrap();
2148+
let count: i64 = conn
2149+
.query_row("SELECT COUNT(*) FROM verification_reports", [], |row| {
2150+
row.get(0)
2151+
})
2152+
.unwrap();
2153+
assert_eq!(count, 1, "same limits should reuse the cached report");
2154+
2155+
let options_b = VerifyOptions {
2156+
limits: ResourceLimits {
2157+
max_report_issues: 18,
2158+
..ResourceLimits::default()
2159+
},
2160+
..VerifyOptions::default()
2161+
};
2162+
let report = ordvec_manifest::sqlite::verify_with_registry(
2163+
&db,
2164+
&document,
2165+
&manifest_path,
2166+
options_b,
2167+
true,
2168+
)
2169+
.unwrap();
2170+
assert!(report.ok, "{:?}", report.errors);
20442171
let count: i64 = conn
20452172
.query_row("SELECT COUNT(*) FROM verification_reports", [], |row| {
20462173
row.get(0)

0 commit comments

Comments
 (0)