diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..fbf0a7ef8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.codegraph +.git +node_modules +target diff --git a/CHANGELOG.d/lineageweave-temporal-context-service.md b/CHANGELOG.d/lineageweave-temporal-context-service.md new file mode 100644 index 000000000..e4a1fea8b --- /dev/null +++ b/CHANGELOG.d/lineageweave-temporal-context-service.md @@ -0,0 +1,3 @@ +### Added + +- Package the existing cutoff-safe `POST /v1/temporal-context` contract as the loopback-only `tepp-loopback` binary and container for trusted same-host consumers such as LineageWeave. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..ce294a142 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM rust:1.97.1-bookworm AS build +WORKDIR /src +COPY . . +RUN cargo build --locked --release -p tepp_api --bin tepp-loopback + +FROM debian:bookworm-slim +RUN apt-get update \ + && apt-get install --yes --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /src/target/release/tepp-loopback /usr/local/bin/tepp-loopback +USER 65532:65532 +HEALTHCHECK --interval=10s --timeout=3s --start-period=2s --retries=5 \ + CMD curl --fail --silent --show-error \ + --header "content-type: application/json" \ + --header "tepp-consumer: lineageweave" \ + --header "tepp-contract-version: 1" \ + --data '{"contract_version":1,"consumer_code":"lineageweave","knowledge_cutoff":"2026-08-20T00:00:00Z","subject_post_id":"health-post","events":[{"event_id":"health-event","source_post_id":"health-post","event_type_code":"health_probe","event_label":"Health probe","event_time":"2026-08-20T00:00:00Z","available_time":"2026-08-20T00:00:00Z","project_reference":null,"actor_references":["health-actor"]}]}' \ + http://127.0.0.1:18081/v1/temporal-context >/dev/null \ + || exit 1 +ENTRYPOINT ["/usr/local/bin/tepp-loopback"] diff --git a/crates/tepp_api/Cargo.toml b/crates/tepp_api/Cargo.toml index b7d27cc7d..1af51a299 100644 --- a/crates/tepp_api/Cargo.toml +++ b/crates/tepp_api/Cargo.toml @@ -20,5 +20,11 @@ serde_json = { workspace = true } sha2 = { workspace = true } temporal_core = { path = "../temporal_core", version = "0.1.0" } +[[bin]] +name = "tepp-loopback" +path = "src/bin/tepp_loopback.rs" +test = false +bench = false + [lints] workspace = true diff --git a/crates/tepp_api/src/bin/tepp_loopback.rs b/crates/tepp_api/src/bin/tepp_loopback.rs new file mode 100644 index 000000000..90800a8cb --- /dev/null +++ b/crates/tepp_api/src/bin/tepp_loopback.rs @@ -0,0 +1,24 @@ +//! Runnable loopback ingress for trusted same-host TEPP consumers. + +use std::net::SocketAddr; + +use tepp_api::AnalysisRunLiveService; + +const DEFAULT_BIND_ADDR: &str = "127.0.0.1:18081"; + +fn main() -> Result<(), Box> { + let mut arguments = std::env::args().skip(1); + let bind_addr = arguments + .next() + .unwrap_or(DEFAULT_BIND_ADDR.to_owned()) + .parse::()?; + let request_limit = arguments + .next() + .map(|value| value.parse::()) + .transpose()? + .unwrap_or(usize::MAX); + let mut service = AnalysisRunLiveService::bind(bind_addr)?; + println!("{}", service.local_addr()?); + (0..request_limit).for_each(|_| drop(service.serve_one())); + Ok(()) +} diff --git a/crates/tepp_api/tests/loopback_binary_contract.rs b/crates/tepp_api/tests/loopback_binary_contract.rs new file mode 100644 index 000000000..20e475647 --- /dev/null +++ b/crates/tepp_api/tests/loopback_binary_contract.rs @@ -0,0 +1,31 @@ +//! The packaged loopback binary serves the published temporal-context wire. + +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::TcpStream; +use std::process::{Command, Stdio}; + +#[test] +fn binary_serves_one_bounded_temporal_context_request() { + let mut child = Command::new(env!("CARGO_BIN_EXE_tepp-loopback")) + .args(["127.0.0.1:0", "1"]) + .stdout(Stdio::piped()) + .spawn() + .expect("spawn loopback service"); + let mut address = String::new(); + BufReader::new(child.stdout.take().expect("stdout")) + .read_line(&mut address) + .expect("bound address"); + let body = r#"{"contract_version":1,"consumer_code":"lineageweave","knowledge_cutoff":"2026-08-20T00:00:00Z","subject_post_id":"post-1","events":[{"event_id":"event-1","source_post_id":"post-1","event_type_code":"health_probe","event_label":"Health probe","event_time":"2026-08-20T00:00:00Z","available_time":"2026-08-20T00:00:00Z","project_reference":null,"actor_references":["actor-1"]}]}"#; + let request = format!( + "POST /v1/temporal-context HTTP/1.1\r\nHost: {}\r\ncontent-type: application/json\r\ntepp-consumer: lineageweave\r\ntepp-contract-version: 1\r\ncontent-length: {}\r\n\r\n{body}", + address.trim(), + body.len() + ); + let mut stream = TcpStream::connect(address.trim()).expect("connect"); + stream.write_all(request.as_bytes()).expect("request"); + let mut response = String::new(); + stream.read_to_string(&mut response).expect("response"); + assert!(response.starts_with("HTTP/1.1 200 OK")); + assert!(response.contains("association_not_causal")); + assert!(child.wait().expect("wait").success()); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index d1c61b872..bf370fe1f 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -7,7 +7,7 @@ TEPP must work both as a standalone product and as a modular CWL component. Integrations with `naruon`, `contextual-orchestrator`, `.github`, or other repositories use explicit versioned API/artifact contracts. Cross-service direct table access is prohibited. -Current protected main exposes Rust library/domain contracts. The active PR adds a loopback HTTP/1.1 listener for naruon analysis-run, LineageWeave temporal-context, and export POSTs. That listener is not a production TLS/`$PORT` service. Endpoint examples below that are not covered by `NaruonLiveService` remain target interface shapes. +Current protected main exposes Rust library/domain contracts. The active stack adds a loopback HTTP/1.1 listener for naruon analysis-run, LineageWeave temporal-context, and export POSTs. `tepp-loopback` runs the shared consumer listener on `127.0.0.1:18081` by default; a caller may pass another loopback socket address and an optional maximum request count as its two arguments. The container is intended for a trusted same-host or shared-network-namespace sidecar, checks readiness through a synthetic bounded temporal-context request, and deliberately cannot bind a public or bridge address. It is not a production TLS/`$PORT` service. Endpoint examples below that are not covered by `NaruonLiveService` remain target interface shapes. ## 2. Contract families