Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang

### Added

- `persistence_postgres` append-only reproducibility-manifest SQL contracts and live repository methods (`insert_reproducibility_manifest`, digest/id lookup) with fail-closed SHA-256 and commit identity validation for `reproducibility_manifest`.
- `persistence_postgres` tenant row-level security: migration `0002_tenant_row_level_security`, `tepp_app_runtime` role, session GUC `tepp.current_tenant_record_id`, multi-word isolation policies with FORCE RLS, session helpers, contract validation, and live isolation proof under `TEPP_LIVE_POSTGRES=1`.
- `persistence_postgres` live PostgreSQL CI: `live-postgres` job with Postgres 16 service, `TEPP_LIVE_POSTGRES=1` gate, and integration coverage for pool open, foundation+RLS migrations, document insert/revise/as-of, audit SQL, and tenant isolation.
- Repository release evidence tooling: `scripts/release_evidence.py` generates CycloneDX 1.5 SBOM, exact-head provenance, and SHA-256 checksums from `Cargo.lock`/`Cargo.toml`, with fail-closed validation and CI generation on every quality gate.
Expand Down
13 changes: 12 additions & 1 deletion crates/persistence_postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
//! path with validated sizing options (ADR 0013). In-process transports keep
//! default CI deterministic; optional `live-sqlx` feature compiles a real
//! `PgPool` driver behind validated URL/options, with a gated live `PostgreSQL`
//! CI job (`TEPP_LIVE_POSTGRES=1`).
//! CI job (`TEPP_LIVE_POSTGRES=1`). Append-only reproducibility-manifest SQL
//! contracts bind evidence digests, code commit, dependency lock, and knowledge
//! cutoff for run provenance (ADR 0013).

mod cutoff;
mod document_sql;
mod document_store;
mod error;
mod live_pool;
mod live_repository;
mod manifest_sql;
mod migration;
mod naming;
mod sql_session;
Expand Down Expand Up @@ -61,6 +64,14 @@ pub use live_pool::open_live_sqlx_pool;
pub use live_repository::LiveDocumentRepository;
/// Migration application failures on the live path.
pub use live_repository::LiveMigrationError;
/// Append-only reproducibility manifest row.
pub use manifest_sql::ReproducibilityManifestRecord;
/// Render insert SQL for a reproducibility manifest.
pub use manifest_sql::insert_reproducibility_manifest_sql;
/// Render selection SQL by digest triple.
pub use manifest_sql::select_reproducibility_manifest_by_digests_sql;
/// Render selection SQL by primary key.
pub use manifest_sql::select_reproducibility_manifest_by_id_sql;
/// Embedded and ad-hoc migration catalogs.
pub use migration::MigrationCatalog;
/// Validate migration SQL against TEPP contracts.
Expand Down
77 changes: 77 additions & 0 deletions crates/persistence_postgres/src/live_repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use crate::document_sql::{
append_audit_sql, as_known_at_sql, as_valid_at_sql, insert_document_sql, revise_document_sqls,
};
use crate::document_store::{AuditEvent, DocumentRecord};
use crate::manifest_sql::{
ReproducibilityManifestRecord, insert_reproducibility_manifest_sql,
select_reproducibility_manifest_by_digests_sql, select_reproducibility_manifest_by_id_sql,
};
use crate::migration::{MigrationCatalog, validate_migration_catalog};
use crate::sql_session::{SqlSession, apply_sql_batch};
use crate::{MigrationContractError, PersistenceError};
Expand Down Expand Up @@ -123,6 +127,51 @@ impl<S: SqlSession> LiveDocumentRepository<S> {
let sql = append_audit_sql(event);
self.session.execute(&sql)
}

/// Insert an append-only reproducibility manifest under the active tenant.
///
/// # Errors
///
/// Returns digest validation or transport failures.
pub fn insert_reproducibility_manifest(
&mut self,
record: &ReproducibilityManifestRecord,
) -> Result<(), PersistenceError> {
let sql = insert_reproducibility_manifest_sql(record)?;
self.session.execute(&sql)
}

/// Look up a reproducibility manifest by the unique digest triple.
///
/// # Errors
///
/// Returns transport failures.
pub fn submit_reproducibility_manifest_by_digests(
&mut self,
evidence_digest: &str,
code_commit_sha: &str,
dependency_lock_digest: &str,
) -> Result<(), PersistenceError> {
let sql = select_reproducibility_manifest_by_digests_sql(
evidence_digest,
code_commit_sha,
dependency_lock_digest,
);
self.session.execute(&sql)
}

/// Look up a reproducibility manifest by primary key.
///
/// # Errors
///
/// Returns transport failures.
pub fn submit_reproducibility_manifest_by_id(
&mut self,
reproducibility_manifest_id: Uuid,
) -> Result<(), PersistenceError> {
let sql = select_reproducibility_manifest_by_id_sql(reproducibility_manifest_id);
self.session.execute(&sql)
}
}

/// Migration application failures distinguishing contract vs transport errors.
Expand All @@ -149,6 +198,7 @@ impl std::error::Error for LiveMigrationError {}
mod tests {
use super::{LiveDocumentRepository, LiveMigrationError};
use crate::document_store::{AuditEvent, DocumentRecord};
use crate::manifest_sql::ReproducibilityManifestRecord;
use crate::migration::MigrationCatalog;
use crate::sql_session::RecordingSqlSession;
use crate::{MigrationContractError, PersistenceError};
Expand Down Expand Up @@ -193,6 +243,33 @@ mod tests {
&SystemTime::parse_rfc3339("2026-02-01T00:00:00Z").expect("k"),
)
.expect("valid");
let manifest = ReproducibilityManifestRecord {
reproducibility_manifest_id: uuid::Uuid::nil(),
tenant_record_id: uuid::Uuid::nil(),
knowledge_cutoff: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("k"),
evidence_digest: "ab".repeat(32),
code_commit_sha: "c".repeat(40),
dependency_lock_digest: "de".repeat(32),
system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"),
available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"),
};
repo.insert_reproducibility_manifest(&manifest)
.expect("manifest insert");
repo.submit_reproducibility_manifest_by_digests(
&manifest.evidence_digest,
&manifest.code_commit_sha,
&manifest.dependency_lock_digest,
)
.expect("manifest by digests");
repo.submit_reproducibility_manifest_by_id(manifest.reproducibility_manifest_id)
.expect("manifest by id");
assert!(
repo.session()
.executed()
.iter()
.any(|sql| sql.contains("INSERT INTO reproducibility_manifest"))
);

let audit = AuditEvent {
audit_event_id: uuid::Uuid::nil(),
tenant_record_id: uuid::Uuid::nil(),
Expand Down
209 changes: 209 additions & 0 deletions crates/persistence_postgres/src/manifest_sql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
//! SQL contracts for append-only reproducibility manifests (ADR 0013).

use crate::PersistenceError;
use temporal_core::{AvailableTime, SystemTime};
use uuid::Uuid;

/// Immutable reproducibility manifest row bound to a tenant and digests.
///
/// Maps to `reproducibility_manifest` in migration `0001`. Digests are
/// lowercase hex `SHA-256` strings; `code_commit_sha` is a non-empty Git
/// commit identity (typically 40-character lowercase hex).
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReproducibilityManifestRecord {
/// Primary key for this manifest identity.
pub reproducibility_manifest_id: Uuid,
/// Owning tenant boundary.
pub tenant_record_id: Uuid,
/// Knowledge cutoff applied when the bound run was estimated.
pub knowledge_cutoff: AvailableTime,
/// Hex-encoded evidence/content digest for the primary analytical payload.
pub evidence_digest: String,
/// Git commit identity of the producing code revision.
pub code_commit_sha: String,
/// Hex-encoded dependency-lock digest bound into the run.
pub dependency_lock_digest: String,
/// System/record time when the manifest was asserted.
pub system_time: SystemTime,
/// Availability time of the manifest evidence.
pub available_time: AvailableTime,
}

impl ReproducibilityManifestRecord {
/// Fail-closed field validation for digests and commit identity.
///
/// # Errors
///
/// Returns [`PersistenceError::InvalidContentDigest`] for non-hex digests or
/// empty commit identities.
pub fn validate(&self) -> Result<(), PersistenceError> {
validate_sha256_hex(&self.evidence_digest)?;
validate_sha256_hex(&self.dependency_lock_digest)?;
validate_commit_sha(&self.code_commit_sha)?;
Ok(())
}
}

/// Render append-only insert SQL for a validated reproducibility manifest.
///
/// # Errors
///
/// Returns digest/commit validation failures before any SQL is produced.
pub fn insert_reproducibility_manifest_sql(
record: &ReproducibilityManifestRecord,
) -> Result<String, PersistenceError> {
record.validate()?;
Ok(format!(
"INSERT INTO reproducibility_manifest (\
reproducibility_manifest_id, tenant_record_id, knowledge_cutoff, \
evidence_digest, code_commit_sha, dependency_lock_digest, \
system_time, available_time\
) VALUES (\
'{manifest_id}'::uuid, '{tenant_id}'::uuid, '{cutoff}'::timestamptz, \
'{evidence}', '{commit}', '{lock_digest}', \
'{system}'::timestamptz, '{available}'::timestamptz\
)",
manifest_id = record.reproducibility_manifest_id,
tenant_id = record.tenant_record_id,
cutoff = record.knowledge_cutoff.to_rfc3339(),
evidence = record.evidence_digest,
commit = escape_literal(&record.code_commit_sha),
lock_digest = record.dependency_lock_digest,
system = record.system_time.to_rfc3339(),
available = record.available_time.to_rfc3339(),
))
}

/// Render selection by the unique digest triple used for idempotent lookup.
#[must_use]
pub fn select_reproducibility_manifest_by_digests_sql(
evidence_digest: &str,
code_commit_sha: &str,
dependency_lock_digest: &str,
) -> String {
format!(
"SELECT reproducibility_manifest_id, tenant_record_id, knowledge_cutoff, \
evidence_digest, code_commit_sha, dependency_lock_digest, \
system_time, available_time \
FROM reproducibility_manifest \
WHERE evidence_digest = '{evidence}' \
AND code_commit_sha = '{commit}' \
AND dependency_lock_digest = '{lock_digest}' \
LIMIT 1",
evidence = escape_literal(evidence_digest),
commit = escape_literal(code_commit_sha),
lock_digest = escape_literal(dependency_lock_digest),
)
}

/// Render selection by primary key under the active tenant RLS context.
#[must_use]
pub fn select_reproducibility_manifest_by_id_sql(reproducibility_manifest_id: Uuid) -> String {
format!(
"SELECT reproducibility_manifest_id, tenant_record_id, knowledge_cutoff, \
evidence_digest, code_commit_sha, dependency_lock_digest, \
system_time, available_time \
FROM reproducibility_manifest \
WHERE reproducibility_manifest_id = '{reproducibility_manifest_id}'::uuid \
LIMIT 1"
)
}

fn validate_sha256_hex(value: &str) -> Result<(), PersistenceError> {
if value.len() != 64 || !value.chars().all(|ch| ch.is_ascii_hexdigit()) {
return Err(PersistenceError::InvalidContentDigest);
}
Ok(())
}

fn validate_commit_sha(value: &str) -> Result<(), PersistenceError> {
if value.is_empty()
|| value.len() > 64
|| !value
.chars()
.all(|ch| ch.is_ascii_hexdigit() || ch == '_' || ch == '-')
{
return Err(PersistenceError::InvalidContentDigest);
}
Ok(())
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

fn escape_literal(value: &str) -> String {
value.replace('\'', "''")
}

#[cfg(test)]
mod tests {
use super::{
ReproducibilityManifestRecord, insert_reproducibility_manifest_sql,
select_reproducibility_manifest_by_digests_sql, select_reproducibility_manifest_by_id_sql,
validate_commit_sha, validate_sha256_hex,
};
use crate::PersistenceError;
use temporal_core::{AvailableTime, SystemTime};
use uuid::Uuid;

fn sample() -> ReproducibilityManifestRecord {
ReproducibilityManifestRecord {
reproducibility_manifest_id: Uuid::nil(),
tenant_record_id: Uuid::nil(),
knowledge_cutoff: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("k"),
evidence_digest: "ab".repeat(32),
code_commit_sha: "c".repeat(40),
dependency_lock_digest: "de".repeat(32),
system_time: SystemTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("s"),
available_time: AvailableTime::parse_rfc3339("2026-01-01T00:00:00Z").expect("a"),
}
}

#[test]
fn manifest_sql_insert_and_select_fail_closed() {
let record = sample();
let insert = insert_reproducibility_manifest_sql(&record).expect("insert");
assert!(insert.contains("INSERT INTO reproducibility_manifest"));
assert!(insert.contains(&record.evidence_digest));
assert!(insert.contains(&record.code_commit_sha));

let by_digest = select_reproducibility_manifest_by_digests_sql(
&record.evidence_digest,
&record.code_commit_sha,
&record.dependency_lock_digest,
);
assert!(by_digest.contains("evidence_digest ="));
assert!(by_digest.contains("LIMIT 1"));

let by_id = select_reproducibility_manifest_by_id_sql(Uuid::nil());
assert!(by_id.contains("reproducibility_manifest_id"));

let mut bad = record.clone();
bad.evidence_digest = "short".into();
assert_eq!(
insert_reproducibility_manifest_sql(&bad),
Err(PersistenceError::InvalidContentDigest)
);
bad = record.clone();
bad.dependency_lock_digest = "nope".into();
assert_eq!(
insert_reproducibility_manifest_sql(&bad),
Err(PersistenceError::InvalidContentDigest)
);
bad = record;
bad.code_commit_sha.clear();
assert_eq!(
insert_reproducibility_manifest_sql(&bad),
Err(PersistenceError::InvalidContentDigest)
);

assert!(validate_sha256_hex(&"ff".repeat(32)).is_ok());
assert!(validate_sha256_hex("zz").is_err());
// Length-correct but non-hex must fail closed (branch vs wrong length).
assert!(validate_sha256_hex(&"g".repeat(64)).is_err());
assert!(validate_commit_sha("abc123").is_ok());
assert!(validate_commit_sha("deadbeef-cafe_01").is_ok());
assert!(validate_commit_sha("").is_err());
assert!(validate_commit_sha(&"x".repeat(65)).is_err());
// Invalid character outside hex/_/-.
assert!(validate_commit_sha("abc!def").is_err());
assert_eq!(super::escape_literal("a'b"), "a''b");
}
}
Loading
Loading