Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -9,6 +9,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang
- `persistence_postgres` event-instance SQL contracts: bitemporal insert and as-known-at lookup that refuse inverted valid/system windows and hostile type/lifecycle labels before SQL is rendered.
- `persistence_postgres` event-mention SQL contracts: mention identity cannot equal the instance it supports; confidence must be finite and in `(0, 1]`.
- `persistence_postgres` event-relation SQL contracts: closed ERD transition/provenance vocabulary bound to `transition_edge`, fail-closed unknown types and transition self-loops, live insert of `causes`/`references`.
- `persistence_postgres` source-artifact SQL contracts: append-only insert and primary-key lookup that refuse non-canonical `SHA-256` digests, negative sizes, and hostile media-type or object-store labels before SQL is rendered; identical-identity retries are `ON CONFLICT DO NOTHING` plus a stored-row match assertion, and a same-id payload change fails closed as `ConflictingSourceArtifact`.
- `persistence_postgres` typed membership assignment (migration `0006`): `entity_record`, `project_record`, and `text_segment` plus exactly-one observed-unit and target constraints that replace the polymorphic `membership_target_id` stub, with SQL insert/lookup, fail-closed inverted-window and backslash-label refusal, and live proof that one document persists two entity memberships and one project membership.
- Actions workflow fleet auditor (`scripts/actions_workflow_fleet.py`): paginated registry inventory bound to the exact default-branch SHA/tree, classification of present/orphan/disabled/GitHub-dynamic identities, and fail-closed orphan disable that confirms GitHub's official `disabled_manually` state.
- `persistence_postgres` temporal interval ordering migration (`0005`): multi-word CHECK constraints on `document_record`, `event_instance`, and `membership_assignment` that reject inverted valid/system windows and non-positive document revisions while preserving open-ended NULL upper bounds and equal point bounds; catalog validation and live inverted-window proof.
Expand Down
254 changes: 254 additions & 0 deletions crates/persistence_postgres/src/artifact_sql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
//! SQL contracts for append-only source artifacts (ADR 0008 / ADR 0013).

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

/// One append-only source artifact independent of document identity.
///
/// Maps to `source_artifact`. The identity is never the content digest:
/// identical bytes may be acquired in different tenant or provenance
/// contexts. Digests are lowercase hex `SHA-256`. Size must be non-negative.
/// Media type and optional object-store references are fail-closed labels.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SourceArtifactRecord {
/// Artifact identity (independent of the content digest).
pub source_artifact_id: Uuid,
/// Owning tenant boundary.
pub tenant_record_id: Uuid,
/// Canonical `SHA-256` of the immutable source bytes.
pub content_sha256: String,
/// Declared payload size in bytes; must be `>= 0`.
pub source_size_bytes: i64,
/// Media type token (for example `text/plain`).
pub media_type_code: String,
/// Optional protected object-store reference.
pub protected_object_ref: Option<String>,
/// System/record time when the artifact identity was asserted.
pub system_time: SystemTime,
/// Availability time of the artifact evidence.
pub available_time: AvailableTime,
}

impl SourceArtifactRecord {
/// Fail-closed digest, size, and label validation.
///
/// # Errors
///
/// Returns [`PersistenceError::InvalidSourceArtifact`] when the digest is
/// not canonical lowercase hex `SHA-256`, the size is negative, or a
/// label is empty, oversized, or hostile.
pub fn validate(&self) -> Result<(), PersistenceError> {
validate_sha256_hex(&self.content_sha256)?;
if self.source_size_bytes < 0 {
return Err(PersistenceError::InvalidSourceArtifact);
}
validate_artifact_label(&self.media_type_code)?;
if let Some(object_ref) = &self.protected_object_ref {
validate_artifact_label(object_ref)?;
}
Ok(())
}
}

/// Render insert SQL for a validated source artifact.
///
/// # Errors
///
/// Returns [`PersistenceError::InvalidSourceArtifact`] before any SQL is produced.
pub fn insert_source_artifact_sql(
record: &SourceArtifactRecord,
) -> Result<String, PersistenceError> {
record.validate()?;
let object_ref_sql = match &record.protected_object_ref {
Some(value) => format!("'{value}'"),
None => "NULL".to_owned(),
};
Ok(format!(
"INSERT INTO source_artifact (\
source_artifact_id, tenant_record_id, content_sha256, source_size_bytes, \
media_type_code, protected_object_ref, system_time, available_time\
) VALUES (\
'{artifact}'::uuid, '{tenant}'::uuid, '{digest}', {size}, \
'{media}', {object_ref_sql}, '{system}'::timestamptz, '{available}'::timestamptz\
) ON CONFLICT (source_artifact_id) DO NOTHING",
artifact = record.source_artifact_id,
tenant = record.tenant_record_id,
digest = record.content_sha256,
size = record.source_size_bytes,
media = record.media_type_code,
system = record.system_time.to_rfc3339(),
available = record.available_time.to_rfc3339(),
))
}

/// Compare two validated artifacts for ADR 0013 idempotent-retry equality.
#[must_use]
pub fn source_artifacts_are_idempotent_matches(
left: &SourceArtifactRecord,
right: &SourceArtifactRecord,
) -> bool {
left == right
}

/// Render a fail-closed assertion that the stored row matches `record`.
///
/// Used after `INSERT ... ON CONFLICT DO NOTHING` so a retry of the same
/// immutable identity succeeds and a same-id payload change raises
/// `conflicting source artifact`.
///
/// # Errors
///
/// Returns [`PersistenceError::InvalidSourceArtifact`] before any SQL is produced.
pub fn assert_source_artifact_matches_sql(
record: &SourceArtifactRecord,
) -> Result<String, PersistenceError> {
record.validate()?;
let object_ref_sql = match &record.protected_object_ref {
Some(value) => format!("'{value}'"),
None => "NULL".to_owned(),
};
Ok(format!(
"DO $tepp_source_artifact_idempotent$\n\
BEGIN\n\
IF NOT EXISTS (\n\
SELECT 1 FROM source_artifact\n\
WHERE source_artifact_id = '{artifact}'::uuid\n\
AND tenant_record_id = '{tenant}'::uuid\n\
AND content_sha256 = '{digest}'\n\
AND source_size_bytes = {size}\n\
AND media_type_code = '{media}'\n\
AND protected_object_ref IS NOT DISTINCT FROM {object_ref_sql}\n\
AND system_time = '{system}'::timestamptz\n\
AND available_time = '{available}'::timestamptz\n\
) THEN\n\
RAISE EXCEPTION 'conflicting source artifact';\n\
END IF;\n\
END\n\
$tepp_source_artifact_idempotent$",
artifact = record.source_artifact_id,
tenant = record.tenant_record_id,
digest = record.content_sha256,
size = record.source_size_bytes,
media = record.media_type_code,
system = record.system_time.to_rfc3339(),
available = record.available_time.to_rfc3339(),
))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Render selection of a source artifact by primary key.
#[must_use]
pub fn select_source_artifact_by_id_sql(source_artifact_id: Uuid) -> String {
format!(
"SELECT source_artifact_id, tenant_record_id, content_sha256, source_size_bytes, \
media_type_code, protected_object_ref, system_time, available_time \
FROM source_artifact \
WHERE source_artifact_id = '{source_artifact_id}'::uuid \
LIMIT 1"
)
}

fn is_lowercase_hex(value: &str) -> bool {
value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
}

fn validate_sha256_hex(value: &str) -> Result<(), PersistenceError> {
if value.len() != 64 || !is_lowercase_hex(value) {
return Err(PersistenceError::InvalidSourceArtifact);
}
Ok(())
}

fn validate_artifact_label(value: &str) -> Result<(), PersistenceError> {
if value.is_empty()
|| value.len() > 128
|| value
.chars()
.any(|ch| ch.is_control() || ch == '\'' || ch == ';' || ch == '\\')
{
return Err(PersistenceError::InvalidSourceArtifact);
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::{
SourceArtifactRecord, assert_source_artifact_matches_sql, insert_source_artifact_sql,
is_lowercase_hex, select_source_artifact_by_id_sql,
source_artifacts_are_idempotent_matches, validate_artifact_label, validate_sha256_hex,
};
use crate::PersistenceError;
use temporal_core::{AvailableTime, SystemTime};
use uuid::Uuid;

fn sample() -> SourceArtifactRecord {
SourceArtifactRecord {
source_artifact_id: Uuid::nil(),
tenant_record_id: Uuid::nil(),
content_sha256: "ab".repeat(32),
source_size_bytes: 4,
media_type_code: "text/plain".into(),
protected_object_ref: None,
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 artifact_sql_covers_valid_and_fail_closed_paths() {
let insert = insert_source_artifact_sql(&sample()).expect("insert");
assert!(insert.contains("INSERT INTO source_artifact"));
assert!(insert.contains("ON CONFLICT (source_artifact_id) DO NOTHING"));
assert!(insert.contains("NULL"));
assert!(source_artifacts_are_idempotent_matches(
&sample(),
&sample()
));
let assertion = assert_source_artifact_matches_sql(&sample()).expect("assert");
assert!(assertion.contains("conflicting source artifact"));
assert_eq!(
assert_source_artifact_matches_sql(&SourceArtifactRecord {
source_size_bytes: -1,
..sample()
}),
Err(PersistenceError::InvalidSourceArtifact)
);

let mut with_ref = sample();
with_ref.protected_object_ref = Some("s3://tepp/object".into());
with_ref.source_size_bytes = 0;
let referenced = insert_source_artifact_sql(&with_ref).expect("ref");
assert!(referenced.contains("s3://tepp/object"));

assert_eq!(
insert_source_artifact_sql(&SourceArtifactRecord {
content_sha256: "nope".into(),
..sample()
}),
Err(PersistenceError::InvalidSourceArtifact)
);
assert_eq!(
insert_source_artifact_sql(&SourceArtifactRecord {
source_size_bytes: -1,
..sample()
}),
Err(PersistenceError::InvalidSourceArtifact)
);
assert!(validate_sha256_hex(&"a1".repeat(32)).is_ok());
assert!(validate_sha256_hex("x").is_err());
assert!(validate_sha256_hex(&"AB".repeat(32)).is_err());
assert!(validate_artifact_label("text/plain").is_ok());
assert!(validate_artifact_label("").is_err());
assert!(validate_artifact_label("text/plain';x").is_err());
assert!(validate_artifact_label("text/plain;x").is_err());
assert!(validate_artifact_label("text/plain\\").is_err());
assert!(validate_artifact_label("text/plain\n").is_err());
assert!(validate_artifact_label(&"x".repeat(129)).is_err());
assert!(is_lowercase_hex("a1"));
assert!(!is_lowercase_hex("AB"));
assert!(select_source_artifact_by_id_sql(Uuid::nil()).contains("FROM source_artifact"));
}
}
14 changes: 14 additions & 0 deletions crates/persistence_postgres/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub enum PersistenceError {
InvalidEventMention,
/// An event instance had inverted windows or a hostile label.
InvalidEventInstance,
/// A source-artifact identity already exists with different immutable fields.
ConflictingSourceArtifact,
/// A source artifact had a non-canonical digest, negative size, or hostile label.
InvalidSourceArtifact,
}

impl fmt::Display for PersistenceError {
Expand All @@ -50,6 +54,8 @@ impl fmt::Display for PersistenceError {
Self::InvalidEventRelation => "invalid event relation",
Self::InvalidEventMention => "invalid event mention",
Self::InvalidEventInstance => "invalid event instance",
Self::ConflictingSourceArtifact => "conflicting source artifact",
Self::InvalidSourceArtifact => "invalid source artifact",
};
formatter.write_str(message)
}
Expand Down Expand Up @@ -161,6 +167,14 @@ mod tests {
PersistenceError::InvalidEventInstance.to_string(),
"invalid event instance"
);
assert_eq!(
PersistenceError::ConflictingSourceArtifact.to_string(),
"conflicting source artifact"
);
assert_eq!(
PersistenceError::InvalidSourceArtifact.to_string(),
"invalid source artifact"
);
assert_eq!(
MigrationContractError::SingleWordObjectName.to_string(),
"single-word database object name"
Expand Down
11 changes: 11 additions & 0 deletions crates/persistence_postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//! membership-assignment SQL (migration `0006`) replaces the polymorphic 0001 stub so documents
//! can belong to multiple entities and projects without atomistic collapse.

mod artifact_sql;
mod cutoff;
mod document_sql;
mod document_store;
Expand All @@ -36,6 +37,16 @@ mod sqlx_gate;
mod sqlx_live;
mod tenant_session;

/// Append-only source artifact row.
pub use artifact_sql::SourceArtifactRecord;
/// Render a fail-closed stored-row match assertion for a source artifact.
pub use artifact_sql::assert_source_artifact_matches_sql;
/// Render insert SQL for a validated source artifact.
pub use artifact_sql::insert_source_artifact_sql;
/// Render selection SQL for a source artifact by primary key.
pub use artifact_sql::select_source_artifact_by_id_sql;
/// Compare two source artifacts for idempotent-retry equality.
pub use artifact_sql::source_artifacts_are_idempotent_matches;
/// Knowledge-cutoff eligibility for historical analytical reads.
pub use cutoff::is_cutoff_eligible;
/// Render append-only audit insert SQL.
Expand Down
Loading
Loading