Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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

- `tepp_api` naruon live loopback HTTP/1.1 listener: `serve_one` installs a read/write deadline, requires a loopback `Host`, refuses `Transfer-Encoding` and NIM/proxy credential headers, parses `knowledge_cutoff` as RFC 3339 and refuses a future cutoff, keys analysis-run idempotency by tenant plus key, and proves both analysis-run and export POSTs over a real `TcpStream`. Not a production TLS/`$PORT` service (ADR 0011).
- `persistence_postgres` backup/restore integrity: restored snapshots stay unusable until tenant, canonical `SHA-256`, knowledge-cutoff eligibility, temporal window order, and append-only triggers revalidate; SQL probes raise `restore integrity failed` (ADR 0013).
- `persistence_postgres` concurrent document-write stress: atomic revise `DO` block that requires exactly one open `system_to` close, SQLSTATE mapping onto `ConcurrentWriteConflict` / `DuplicateDocumentRecord`, and live multi-session insert/revise/append-only proofs. No new migration number.
- `tepp_api` naruon HTTP interchange: versioned `https` POST contracts for analysis-run create and modular export authorization that refuse table-access URLs, review/Copilot credential headers, reserved standard-header redefinition, principal-only export idempotency keys, and lexical inference claims (ADR 0011).
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/tepp_api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tepp_api"
description = "Versioned service DTOs, schemas, and export contracts."
description = "Versioned service DTOs, schemas, export contracts, and loopback naruon HTTP."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
Expand All @@ -14,8 +14,10 @@ categories.workspace = true
publish = false

[dependencies]
jiff = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
temporal_core = { path = "../temporal_core" }

[lints]
workspace = true
33 changes: 32 additions & 1 deletion crates/tepp_api/src/analysis_run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use crate::ApiError;
use crate::wire::{
from_json, require_byte_limit, require_contract_version, require_nonempty, to_json,
};
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use temporal_core::KnowledgeCutoff;

/// Supported analysis-run contract version.
pub const ANALYSIS_RUN_CONTRACT_VERSION: u16 = 1;
Expand Down Expand Up @@ -83,13 +85,29 @@ impl AnalysisRunRequest {
require_nonempty(&self.idempotency_key)?;
require_nonempty(&self.tenant_workspace_id)?;
require_nonempty(&self.snapshot_id)?;
require_nonempty(&self.knowledge_cutoff)?;
require_rfc3339_knowledge_cutoff(&self.knowledge_cutoff)?;
require_nonempty(&self.model_contract_version)?;
require_nonempty(&self.output_profile)?;
Ok(())
}
}

/// Parse `knowledge_cutoff` as a TEPP clock and refuse a cutoff after now.
///
/// A buyer cannot claim analysis of evidence that is not yet available. The
/// request receipt instant is treated as availability of the command itself.
fn require_rfc3339_knowledge_cutoff(knowledge_cutoff: &str) -> Result<(), ApiError> {
require_nonempty(knowledge_cutoff)?;
let cutoff = KnowledgeCutoff::parse_rfc3339(knowledge_cutoff)
.map_err(|_| ApiError::InvalidWirePayload)?;
let receipt = KnowledgeCutoff::parse_rfc3339(&Timestamp::now().to_string())
.map_err(|_| ApiError::InvalidWirePayload)?;
if cutoff.instant() > receipt.instant() {
return Err(ApiError::InvalidWirePayload);
}
Ok(())
}

impl AnalysisRunAccepted {
/// Construct a validated accepted-run response.
///
Expand Down Expand Up @@ -223,6 +241,19 @@ mod tests {
bad.output_profile.clear();
assert_eq!(bad.to_json(), Err(ApiError::InvalidWirePayload));

assert_eq!(
AnalysisRunRequest::from_json(
r#"{"contract_version":1,"idempotency_key":"a","tenant_workspace_id":"t","snapshot_id":"s","knowledge_cutoff":"k","model_contract_version":"m","output_profile":"o"}"#
),
Err(ApiError::InvalidWirePayload)
);
assert_eq!(
AnalysisRunRequest::from_json(
r#"{"contract_version":1,"idempotency_key":"a","tenant_workspace_id":"t","snapshot_id":"s","knowledge_cutoff":"2099-01-01T00:00:00Z","model_contract_version":"m","output_profile":"o"}"#
),
Err(ApiError::InvalidWirePayload)
);

let accepted = AnalysisRunAccepted::new("run-1", "accepted", "idem-1").expect("acc");
let accepted_json = accepted.to_json().expect("aj");
assert_eq!(
Expand Down
16 changes: 14 additions & 2 deletions crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
//! component without sharing application tables. Domain estimation remains in
//! scientific crates; this crate only defines fail-closed interchange shapes.
//! naruon HTTP interchange is a versioned `https` POST to analysis-run and
//! export paths; table-access URLs, review/Copilot headers, and lexical
//! inference claims fail closed (ADR 0011).
//! export paths; table-access URLs, review/Copilot/NIM/proxy headers, and
//! lexical inference claims fail closed. A loopback live listener proves
//! those POSTs over TCP without claiming production TLS (ADR 0011).

mod analysis_run;
mod authorization;
mod envelope;
mod error;
mod export;
mod naruon_http;
mod naruon_live;
mod wire;

/// Analysis-run contract version constant.
Expand Down Expand Up @@ -66,3 +68,13 @@ pub use naruon_http::naruon_analysis_run_exchange_with_headers;
pub use naruon_http::naruon_export_exchange;
/// Refuse lexical heuristics as TEPP inference claims.
pub use naruon_http::naruon_may_claim_tepp_inference;
/// Maximum live HTTP header-block bytes.
pub use naruon_live::NARUON_LIVE_HEADER_BYTE_LIMIT;
/// Maximum live HTTP header count.
pub use naruon_live::NARUON_LIVE_HEADER_COUNT_LIMIT;
/// Accepted-stream read/write deadline.
pub use naruon_live::NARUON_LIVE_IO_TIMEOUT;
/// HTTP/1.1 response from the naruon live listener.
pub use naruon_live::NaruonLiveResponse;
/// Loopback live HTTP/1.1 service for naruon POSTs.
pub use naruon_live::NaruonLiveService;
44 changes: 32 additions & 12 deletions crates/tepp_api/src/naruon_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,34 @@ fn compose_https_target(origin: &str, path: &str) -> Result<String, ApiError> {
Ok(format!("{origin}{path}"))
}

/// Return whether `name` is a reserved naruon interchange header.
pub(crate) fn header_is_reserved_standard(name: &str) -> bool {
matches!(
name.to_ascii_lowercase().as_str(),
"content-type" | "tepp-consumer" | "tepp-contract-version" | "idempotency-key"
)
}

/// Return whether `name` is a review, model, proxy, or bearer credential header.
pub(crate) fn header_is_credential(name: &str) -> bool {
let lowered = name.to_ascii_lowercase();
lowered == "authorization"
|| lowered == "proxy-authorization"
|| lowered == "cookie"
|| lowered == "x-api-key"
|| lowered.contains("token")
|| lowered.contains("copilot")
|| lowered.contains("github")
|| lowered.contains("nim")
|| lowered.contains("nvidia")
}

fn refuse_credential_headers(extra_headers: &[(&str, &str)]) -> Result<(), ApiError> {
for (name, _) in extra_headers {
let lowered = name.to_ascii_lowercase();
if matches!(
lowered.as_str(),
"content-type" | "tepp-consumer" | "tepp-contract-version" | "idempotency-key"
) {
if header_is_reserved_standard(name) {
return Err(ApiError::InvalidWirePayload);
}
if lowered == "authorization"
|| lowered == "cookie"
|| lowered == "x-api-key"
|| lowered.contains("token")
|| lowered.contains("copilot")
|| lowered.contains("github")
{
if header_is_credential(name) {
return Err(ApiError::AuthorizationDenied);
}
}
Expand Down Expand Up @@ -298,6 +310,14 @@ mod tests {
refuse_credential_headers(&[("x-copilot-session", "t")]),
Err(ApiError::AuthorizationDenied)
);
assert_eq!(
refuse_credential_headers(&[("Proxy-Authorization", "Basic x")]),
Err(ApiError::AuthorizationDenied)
);
assert_eq!(
refuse_credential_headers(&[("x-nvidia-nim-key", "nvapi-x")]),
Err(ApiError::AuthorizationDenied)
);
}

#[test]
Expand Down
Loading
Loading