Skip to content
Draft
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.d/temporal-context-retrieval-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp-temporal-context-get get` mints LineageWeave `GET /v1/temporal-context/{idempotency_key}` onto spawned `tepp-loopback` TCP (ADR 0084). Metric-free `inference_status=temporal_association_only` receipts. Event labels and actor lists never appear. Does not infer causality. Naruon refused. `NaruonLiveService` stays POST-only. Does not re-open collection GET or cancel lineages. Not GAP-010 Figma/export, not persistence.
1 change: 1 addition & 0 deletions CHANGELOG.d/temporal-context-retrieval-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `GET /v1/temporal-context/{idempotency_key}` returns one accepted LineageWeave temporal-context identity on `tepp-loopback` (ADR 0083). Metric-free `inference_status=temporal_association_only`. Event labels and actor lists never appear. Does not infer causality. Naruon refused. `NaruonLiveService` stays POST-only. Does not re-open collection GET or cancel lineages. Not GAP-010 Figma/export, not persistence.
2 changes: 2 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| naruon modular consumer contract | [`docs/connectors/naruon-artifact-consumer.md`](docs/connectors/naruon-artifact-consumer.md) |
| contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) |
| Orchestrator live HTTP doctoring | [`docs/research/orchestrator-live-http.md`](docs/research/orchestrator-live-http.md) |
| Temporal-context GET-by-id doctoring | [`docs/research/temporal-context-retrieval-get.md`](docs/research/temporal-context-retrieval-get.md) |
| Temporal-context retrieval CLI doctoring | [`docs/research/temporal-context-retrieval-cli.md`](docs/research/temporal-context-retrieval-cli.md) |
| UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) |
| Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) |
| Security policy | [`SECURITY.md`](SECURITY.md) |
Expand Down
6 changes: 6 additions & 0 deletions crates/tepp_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,11 @@ path = "src/bin/tepp_loopback.rs"
test = false
bench = false

[[bin]]
name = "tepp-temporal-context-get"
path = "src/bin/tepp_temporal_context_get.rs"
test = false
bench = false

[lints]
workspace = true
125 changes: 117 additions & 8 deletions crates/tepp_api/src/analysis_run_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ use crate::naruon_http::NARUON_ANALYSIS_RUN_PATH;
use crate::{
AnalysisRunAccepted, AnalysisRunRequest, ApiError, DEFAULT_PROJECT_HISTORY_BYTE_LIMIT,
ErrorEnvelope, NARUON_LIVE_IO_TIMEOUT, NaruonLiveResponse, PROJECT_HISTORY_PATH,
ProjectHistoryProjection, ProjectHistoryRequest, TEMPORAL_CONTEXT_PATH, TemporalContextRequest,
ProjectHistoryProjection, ProjectHistoryRequest, TEMPORAL_CONTEXT_PATH,
TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS, TemporalContextRequest, TemporalContextRetrieved,
build_temporal_context, project_history_projection, requests_are_idempotent_matches,
temporal_context_retrieval_path_id,
};

const MAX_LIVE_REQUEST_BODY_BYTES: usize = DEFAULT_PROJECT_HISTORY_BYTE_LIMIT;
Expand All @@ -41,6 +43,7 @@ pub struct AnalysisRunLiveService {
next_request_serial: u64,
accepted_runs: HashMap<String, (AnalysisRunRequest, AnalysisRunAccepted)>,
accepted_project_histories: HashMap<String, (ProjectHistoryRequest, ProjectHistoryProjection)>,
accepted_temporal_contexts: HashMap<String, TemporalContextRetrieved>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Registry lifetime matches declared scope

The registry disappears when tepp-loopback restarts. ADR 0083 explicitly leaves persistence to GAP-003B, so this is not a defect here.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

impl Default for AnalysisRunLiveService {
Expand All @@ -60,6 +63,7 @@ impl AnalysisRunLiveService {
next_request_serial: 1,
accepted_runs: HashMap::new(),
accepted_project_histories: HashMap::new(),
accepted_temporal_contexts: HashMap::new(),
}
}

Expand Down Expand Up @@ -143,33 +147,85 @@ impl AnalysisRunLiveService {
let (header_block, body) = split_request_with_limit(request, MAX_LIVE_REQUEST_BODY_BYTES)?;
let mut lines = header_block.split("\r\n");
let (method, path) = parse_request_line(lines.next().unwrap_or(""))?;
let headers = parse_headers(&mut lines)?;
if method == "GET" {
return self.get_temporal_context(path, &headers, body);
Comment on lines +151 to +152

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: GET dispatch remains route-restricted

All GETs enter one handler, but temporal_context_retrieval_path_id admits only item paths. Collection, unrelated, query, and extra-segment routes remain closed.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
if method != "POST"
|| (path != NARUON_ANALYSIS_RUN_PATH
&& path != TEMPORAL_CONTEXT_PATH
&& path != PROJECT_HISTORY_PATH)
{
return Err(ApiError::InvalidWirePayload);
}
let headers = parse_headers(&mut lines)?;
let consumer = require_headers(
&headers,
self.bound_addr,
path == NARUON_ANALYSIS_RUN_PATH || path == PROJECT_HISTORY_PATH,
)?;
if path == TEMPORAL_CONTEXT_PATH {
if consumer != LINEAGEWEAVE_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
let context_request = TemporalContextRequest::from_json(body)?;
let response = build_temporal_context(&context_request)?;
return Ok(json_response(200, "OK", response.to_json()?));
return self.accept_temporal_context(consumer, &headers, body);
}
if path == PROJECT_HISTORY_PATH {
return self.accept_project_history(consumer, &headers, body);
}
self.accept_analysis_run(consumer, &headers, body)
}

fn accept_temporal_context(
&mut self,
consumer: &str,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
if consumer != LINEAGEWEAVE_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
let context_request = TemporalContextRequest::from_json(body)?;
if let Some(idempotency_key) = headers.get("idempotency-key") {
let item = TemporalContextRetrieved::new(
idempotency_key.clone(),
context_request.knowledge_cutoff.clone(),
TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS,
)?;
let replay_key = format!("{consumer}\u{1f}{idempotency_key}");
if let Some(stored) = self.accepted_temporal_contexts.get(&replay_key) {
if stored.knowledge_cutoff != item.knowledge_cutoff {
return Err(ApiError::InvalidWirePayload);
}
Comment on lines +192 to +195

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changed retries reuse accepted keys

Reusing a key with the same cutoff but different events passes the knowledge_cutoff check. The conflicting retry is recomputed instead of rejected.

Prompt for agents
In crates/tepp_api/src/analysis_run_live.rs, accepted_temporal_contexts stores only TemporalContextRetrieved, so accept_temporal_context can compare only knowledge_cutoff on replay. Preserve the original TemporalContextRequest and successful response, as the analysis-run and project-history registries do. Return the original result for an identical retry and reject any request change under the same consumer/idempotency key. Add tests for identical replay and for changed events, subject_post_id, and other request fields with an unchanged cutoff.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

} else {
self.accepted_temporal_contexts.insert(replay_key, item);
}
Comment on lines +196 to +198

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failed submissions remain retrievable

A large valid POST can fail during response serialization after insert records its identity. A later GET then reports the failed submission as accepted.

Prompt for agents
In crates/tepp_api/src/analysis_run_live.rs, accept_temporal_context mutates accepted_temporal_contexts before build_temporal_context and response serialization have completed. A request below the 64 KiB request limit can produce a response above the 64 KiB TemporalContextResponse limit because response fields duplicate request identifiers. The POST then returns an error while GET still finds the recorded identity. Build and serialize the successful response first, then commit the registry mutation only after every fallible acceptance step succeeds. Add a regression test with a valid under-limit request whose expanded response exceeds its output limit, asserting that the failed POST does not create a retrievable key.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
let response = build_temporal_context(&context_request)?;
Ok(json_response(200, "OK", response.to_json()?))
}

fn get_temporal_context(
&self,
path: &str,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
if !body.is_empty() {
return Err(ApiError::InvalidWirePayload);
}
if headers.contains_key("idempotency-key") {
return Err(ApiError::InvalidWirePayload);
}
let idempotency_key = temporal_context_retrieval_path_id(path)?;
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != LINEAGEWEAVE_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
let replay_key = format!("{consumer}\u{1f}{idempotency_key}");
let stored = self
.accepted_temporal_contexts
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
Ok(json_response(200, "OK", stored.to_json()?))
}

fn accept_analysis_run(
&mut self,
consumer: &str,
Expand Down Expand Up @@ -320,6 +376,7 @@ mod tests {
DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, ErrorEnvelope, LINEAGEWEAVE_CONSUMER_CODE,
NARUON_ANALYSIS_RUN_PATH, NARUON_CONSUMER_CODE, NARUON_LIVE_HEADER_BYTE_LIMIT,
NARUON_LIVE_HEADER_COUNT_LIMIT, NARUON_LIVE_IO_TIMEOUT, TEMPORAL_CONTEXT_PATH,
TemporalContextRetrieved,
};

fn sample_run() -> AnalysisRunRequest {
Expand Down Expand Up @@ -734,6 +791,58 @@ mod tests {
assert_eq!(replay.body, accepted.body);
}

#[test]
fn temporal_context_get_by_id_is_metric_free_and_fail_closed() {
let temporal_body = r#"{"contract_version":1,"consumer_code":"lineageweave","knowledge_cutoff":"2026-08-20T00:00:00Z","subject_post_id":null,"events":[{"event_id":"event-1","source_post_id":"post-1","event_type_code":"order_awarded","event_label":"Order awarded","event_time":"2026-08-01T09:00:00Z","available_time":"2026-08-01T10:00:00Z","project_reference":null,"actor_references":["actor-1"]}]}"#;
let mut service = AnalysisRunLiveService::new();
let posted = format!(
"POST {TEMPORAL_CONTEXT_PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\nidempotency-key: idem-a\r\ncontent-length: {}\r\n\r\n{temporal_body}",
temporal_body.len()
);
assert_eq!(service.handle_http_request(&posted).status_code, 200);
let got = service.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/idem-a HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
),
);
assert_eq!(got.status_code, 200, "{}", got.body);
assert!(!got.body.contains("event_label"));
assert!(!got.body.contains("rmse"));
let row = TemporalContextRetrieved::from_json(&got.body).expect("row");
assert_eq!(row.idempotency_key, "idem-a");
assert_eq!(row.inference_status, "temporal_association_only");
assert_eq!(
service
.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
)
)
.status_code,
400
);
assert_eq!(
service
.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/idem-a HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {NARUON_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
)
)
.status_code,
400
);
assert_eq!(
service
.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/missing HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {LINEAGEWEAVE_CONSUMER_CODE}\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
)
)
.status_code,
400
);
}

#[test]
fn parser_helpers_cover_framing_header_and_limit_edges() {
assert_eq!(
Expand Down
30 changes: 30 additions & 0 deletions crates/tepp_api/src/bin/tepp_temporal_context_get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Operator CLI for loopback `LineageWeave` temporal-context GET-by-id.

use std::io::{self, IsTerminal};
use std::process::ExitCode;

use tepp_api::{
execute_temporal_context_retrieval_cli, read_temporal_context_retrieval_cli_stdin,
render_temporal_context_retrieval_cli_stdout, ApiError, TemporalContextRetrievalCliInvocation,
};

fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(_) => ExitCode::FAILURE,
}
}

fn run() -> Result<(), ApiError> {
let args: Vec<String> = std::env::args().skip(1).collect();
let body = read_temporal_context_retrieval_cli_stdin(io::stdin().is_terminal(), io::stdin())?;
let invocation = TemporalContextRetrievalCliInvocation::from_args(&args, body)?;
let response = execute_temporal_context_retrieval_cli(&invocation)?;
let stdout = render_temporal_context_retrieval_cli_stdout(&invocation, &response)?;
println!("{stdout}");
if response.status_code == 200 {
Ok(())
} else {
Err(ApiError::InvalidWirePayload)
}
}
34 changes: 34 additions & 0 deletions crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ mod project_history;
mod project_journey;
mod provider_payload;
mod temporal_context;
mod temporal_context_retrieval_cli;
mod temporal_context_retrieval_http;
mod wire;

/// Terminal analysis-result contract version constant.
Expand Down Expand Up @@ -282,3 +284,35 @@ pub use temporal_context::TemporalContextTimelineEvent;
pub use temporal_context::TemporalTransitionGapCandidate;
/// Build a cutoff-safe, non-causal temporal context.
pub use temporal_context::build_temporal_context;
/// Maximum opaque idempotency-key length on the retrieval path.
pub use temporal_context_retrieval_http::TEMPORAL_CONTEXT_RETRIEVAL_ID_MAX_LEN;
/// Supported temporal-context retrieval contract version.
pub use temporal_context_retrieval_http::TEMPORAL_CONTEXT_RETRIEVAL_CONTRACT_VERSION;
/// Fixed non-causal claim boundary echoed on every retrieval.
pub use temporal_context_retrieval_http::TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS;
/// One metric-free identity projection for an accepted temporal-context POST.
pub use temporal_context_retrieval_http::TemporalContextRetrieved;
/// Build a provider-owned `GET` temporal-context retrieval exchange.
pub use temporal_context_retrieval_http::lineageweave_temporal_context_retrieval_exchange;
/// Refuse retrieval JSON that already carries scientific-metric or evidence keys.
pub use temporal_context_retrieval_http::refuse_metrics_on_temporal_context_retrieval_payload;
/// Extract the opaque idempotency key from `GET /v1/temporal-context/{key}`.
pub use temporal_context_retrieval_http::temporal_context_retrieval_path_id;
/// Refuse an empty, oversized, slash, NUL, or control-bearing identity.
pub use temporal_context_retrieval_http::validate_temporal_context_registry_identity;
/// Supported operator verbs for the loopback temporal-context retrieval CLI.
pub use temporal_context_retrieval_cli::TemporalContextRetrievalCliVerb;
/// One operator CLI invocation against a loopback GET-by-id listener.
pub use temporal_context_retrieval_cli::TemporalContextRetrievalCliInvocation;
/// Compose one HTTP/1.1 retrieval GET from the typed `LineageWeave` exchange.
pub use temporal_context_retrieval_cli::compose_temporal_context_retrieval_cli_http;
/// Dispatch one retrieval CLI invocation against an in-process listener.
pub use temporal_context_retrieval_cli::dispatch_temporal_context_retrieval_cli;
/// Execute one retrieval CLI invocation over loopback TCP.
pub use temporal_context_retrieval_cli::execute_temporal_context_retrieval_cli;
/// Render a typed retrieval GET exchange as HTTP/1.1 for a loopback listener.
pub use temporal_context_retrieval_cli::loopback_http1_from_temporal_context_retrieval_exchange;
/// Read stdin leftover bytes on a non-terminal; retrieval GET admits empty.
pub use temporal_context_retrieval_cli::read_temporal_context_retrieval_cli_stdin;
/// Filter CLI stdout so retrieval never prints scientific acceptance.
pub use temporal_context_retrieval_cli::render_temporal_context_retrieval_cli_stdout;
Loading
Loading