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 @@ -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).
- `tepp_api` adaptive orchestration router (ADR 0010): versioned `direct`/`verify`/`committee`/`conductor`/`abstain` selection from CPU `f64` risk, ambiguity, evidence, and token-budget inputs; recorded stages, recursion, decomposition, access lists, and role-specific reasoning effort; fail-closed document-controlled policy/access/credentials; LLM plans remain proposals under deterministic statistical authority; comparable-budget ablation requires a direct baseline; credential-free contextual-orchestrator binding. Live NIM HTTP remains accepted-target.
- `tepp_api` purpose-bound provider-payload minimization: time-bounded `PurposeGrant` evaluation, fail-closed expired/not-yet-valid/inverted/cross-tenant/impossible-calendar denial, semantic UTC calendar validation, refusal to copy identity mappings into model-provider payloads or ordinary logs, preservation of opaque analytical identifiers and membership roles (no blanket PII mask), a separately authorized scientific re-identification path, and an internally bound FIPS 180-4 SHA-256 audit digest appended through `ReidentificationAuditSink` before disclosure.
- `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).
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 2 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,6 +14,7 @@ categories.workspace = true
publish = false

[dependencies]
jiff = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
Expand Down
46 changes: 45 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,32 @@ 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\u001fb","tenant_workspace_id":"t","snapshot_id":"s","knowledge_cutoff":"2026-08-01T00:00:00Z","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\u001fb","snapshot_id":"s","knowledge_cutoff":"2026-08-01T00:00:00Z","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":"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 orchestration;
mod provider_payload;
mod wire;
Expand Down Expand Up @@ -68,6 +70,16 @@ 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;
/// Comparable-budget ablation record.
pub use orchestration::BudgetAblationRecord;
/// Credential-free contextual-orchestrator binding.
Expand Down
77 changes: 59 additions & 18 deletions crates/tepp_api/src/naruon_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ fn compose_https_target(origin: &str, path: &str) -> Result<String, ApiError> {
|| host.contains('/')
|| host.contains('?')
|| host.contains('#')
|| host
.chars()
.any(|ch| ch.is_control() || matches!(ch, '\'' | ';' | '\\' | ' '))
|| host.chars().any(|ch| matches!(ch, '\'' | ';' | '\\' | ' '))
{
return Err(ApiError::InvalidWirePayload);
}
Expand All @@ -142,22 +140,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 All @@ -176,10 +186,10 @@ fn standard_headers(idempotency_key: &str) -> Vec<(String, String)> {
#[cfg(test)]
mod tests {
use super::{
NARUON_TEPP_INFERENCE_METHOD, compose_https_target, naruon_may_claim_tepp_inference,
refuse_credential_headers,
NARUON_TEPP_INFERENCE_METHOD, compose_https_target, naruon_export_exchange,
naruon_may_claim_tepp_inference, refuse_credential_headers,
};
use crate::ApiError;
use crate::{AnalyticalPurpose, ApiError, ExportAuthorizationRequest};

#[test]
fn compose_https_target_accepts_clean_origin_and_rejects_hostile_forms() {
Expand Down Expand Up @@ -298,6 +308,37 @@ 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]
fn naruon_export_exchange_covers_unit_test_purpose_gate() {
let allowed = ExportAuthorizationRequest {
tenant_workspace_id: "tenant-a".into(),
principal_id: "naruon-service".into(),
purpose: AnalyticalPurpose::ModularServiceConsumer,
artifact_id: "artifact-a".into(),
includes_source_text: false,
};
assert!(
naruon_export_exchange("https://tepp.example.test", &allowed, "export-idem-a").is_ok()
);

let denied = ExportAuthorizationRequest {
purpose: AnalyticalPurpose::OperationalMonitoring,
..allowed
};
assert_eq!(
naruon_export_exchange("https://tepp.example.test", &denied, "export-idem-b"),
Err(ApiError::AuthorizationDenied)
);
}

#[test]
Expand Down
Loading
Loading