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-stored-request-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp-temporal-context-request get` mints LineageWeave `GET /v1/temporal-context/{idempotency_key}/request` onto spawned `tepp-loopback` TCP (ADR 0092). Empty stdin admitted. Metric-free of RMSE/`tepp.scientific_acceptance.v1`. `inference_status` on the live projection remains `temporal_association_only`. Naruon refused. `NaruonLiveService` stays POST-only. Dedicated binary so it does not collide with `tepp-temporal-context-get` (#452). 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-stored-request-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `GET /v1/temporal-context/{idempotency_key}/request` returns the stored LineageWeave temporal-context create request on `tepp-loopback` (ADR 0091). Metric-free of RMSE/`tepp.scientific_acceptance.v1`. `inference_status` on the live projection remains `temporal_association_only`. 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 @@ -14,6 +14,8 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| 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 stored-request GET doctoring | [`docs/research/temporal-context-stored-request-get.md`](docs/research/temporal-context-stored-request-get.md) |
| Temporal-context stored-request CLI doctoring | [`docs/research/temporal-context-stored-request-cli.md`](docs/research/temporal-context-stored-request-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-request"
path = "src/bin/tepp_temporal_context_request.rs"
test = false
bench = false

[lints]
workspace = true
78 changes: 70 additions & 8 deletions crates/tepp_api/src/analysis_run_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ use crate::{
ErrorEnvelope, NARUON_LIVE_IO_TIMEOUT, NaruonLiveResponse, PROJECT_HISTORY_PATH,
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,
build_temporal_context, project_history_projection, refuse_metrics_on_temporal_context_stored_request_payload,
requests_are_idempotent_matches, temporal_context_retrieval_path_id,
temporal_context_stored_request_path_id,
};

const MAX_LIVE_REQUEST_BODY_BYTES: usize = DEFAULT_PROJECT_HISTORY_BYTE_LIMIT;
Expand All @@ -43,7 +44,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>,
accepted_temporal_contexts: HashMap<String, (TemporalContextRequest, TemporalContextRetrieved)>,
}

impl Default for AnalysisRunLiveService {
Expand Down Expand Up @@ -149,6 +150,12 @@ impl AnalysisRunLiveService {
let (method, path) = parse_request_line(lines.next().unwrap_or(""))?;
let headers = parse_headers(&mut lines)?;
if method == "GET" {
if matches!(
temporal_context_stored_request_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
) {
return self.get_temporal_context_stored_request(path, &headers, body);
}
return self.get_temporal_context(path, &headers, body);
}
if method != "POST"
Expand Down Expand Up @@ -189,12 +196,16 @@ impl AnalysisRunLiveService {
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 {
if let Some((stored_request, stored)) = self.accepted_temporal_contexts.get(&replay_key)
{
if stored_request != &context_request
|| stored.knowledge_cutoff != item.knowledge_cutoff
{
return Err(ApiError::InvalidWirePayload);
}
} else {
self.accepted_temporal_contexts.insert(replay_key, item);
self.accepted_temporal_contexts
.insert(replay_key, (context_request.clone(), item));
}
}
let response = build_temporal_context(&context_request)?;
Expand All @@ -219,13 +230,44 @@ impl AnalysisRunLiveService {
return Err(ApiError::InvalidWirePayload);
}
let replay_key = format!("{consumer}\u{1f}{idempotency_key}");
let stored = self
let (_, stored) = self
.accepted_temporal_contexts
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
Ok(json_response(200, "OK", stored.to_json()?))
}

fn get_temporal_context_stored_request(
&self,
path: &str,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
if !body.is_empty() {
return Err(ApiError::InvalidWirePayload);
}
refuse_metrics_on_temporal_context_stored_request_payload(body)?;
if headers.contains_key("idempotency-key") {
return Err(ApiError::InvalidWirePayload);
}
let idempotency_key = temporal_context_stored_request_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_request, projection) = self
.accepted_temporal_contexts
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
Comment on lines +254 to +262

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Stored requests lack caller authentication

Any local process knowing an idempotency key can retrieve the original request. Event labels and actor references are exposed without caller authentication.

Devin Review

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

if projection.inference_status != TEMPORAL_CONTEXT_RETRIEVAL_INFERENCE_STATUS {
return Err(ApiError::InvalidWirePayload);
}
let response_body = stored_request.to_json()?;
refuse_metrics_on_temporal_context_stored_request_payload(&response_body)?;
Ok(json_response(200, "OK", response_body))
}

fn accept_analysis_run(
&mut self,
consumer: &str,
Expand Down Expand Up @@ -376,7 +418,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,
TemporalContextRequest, TemporalContextRetrieved,
};

fn sample_run() -> AnalysisRunRequest {
Expand Down Expand Up @@ -841,6 +883,26 @@ mod tests {
.status_code,
400
);
let stored = service.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/idem-a/request 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!(stored.status_code, 200, "{}", stored.body);
let request = TemporalContextRequest::from_json(&stored.body).expect("stored request");
assert_eq!(request.knowledge_cutoff, "2026-08-20T00:00:00Z");
assert!(!stored.body.contains("rmse"));
assert!(!stored.body.contains("tepp.scientific_acceptance.v1"));
assert_eq!(
service
.handle_http_request(
&format!(
"GET {TEMPORAL_CONTEXT_PATH}/idem-a/cancel 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]
Expand Down
35 changes: 35 additions & 0 deletions crates/tepp_api/src/bin/tepp_temporal_context_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Operator CLI for loopback `LineageWeave` temporal-context stored-request GET.

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

use tepp_api::{
ApiError, TemporalContextStoredRequestCliInvocation,
execute_temporal_context_stored_request_cli, read_temporal_context_stored_request_cli_stdin,
render_temporal_context_stored_request_cli_stdout,
};

fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("tepp-temporal-context-request: {error}");
ExitCode::FAILURE
}
}
}

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

/// Terminal analysis-result contract version constant.
Expand Down Expand Up @@ -299,3 +301,27 @@ pub use temporal_context_retrieval_http::refuse_metrics_on_temporal_context_retr
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;
/// Whether `path` is the stored-request extra-segment resource.
pub use temporal_context_stored_request_http::is_temporal_context_stored_request_path;
/// Build a credential-free `LineageWeave` stored-request GET exchange.
pub use temporal_context_stored_request_http::lineageweave_temporal_context_stored_request_exchange;
/// Extract the opaque idempotency key from `GET /v1/temporal-context/{key}/request`.
pub use temporal_context_stored_request_http::temporal_context_stored_request_path_id;
/// Refuse stored-request JSON that already carries scientific-metric keys.
pub use temporal_context_stored_request_http::refuse_metrics_on_temporal_context_stored_request_payload;
/// Supported operator verbs for the loopback temporal-context stored-request CLI.
pub use temporal_context_stored_request_cli::TemporalContextStoredRequestCliVerb;
/// One operator CLI invocation against a loopback stored-request listener.
pub use temporal_context_stored_request_cli::TemporalContextStoredRequestCliInvocation;
/// Render a typed stored-request GET exchange as HTTP/1.1 for a loopback listener.
pub use temporal_context_stored_request_cli::loopback_http1_from_temporal_context_stored_request_exchange;
/// Compose one HTTP/1.1 stored-request GET from the typed `LineageWeave` exchange.
pub use temporal_context_stored_request_cli::compose_temporal_context_stored_request_cli_http;
/// Dispatch one stored-request CLI invocation against an in-process listener.
pub use temporal_context_stored_request_cli::dispatch_temporal_context_stored_request_cli;
/// Execute one stored-request CLI invocation over loopback TCP.
pub use temporal_context_stored_request_cli::execute_temporal_context_stored_request_cli;
/// Filter CLI stdout so stored-request GET never prints scientific acceptance.
pub use temporal_context_stored_request_cli::render_temporal_context_stored_request_cli_stdout;
/// Read stdin leftover bytes on a non-terminal; stored-request GET admits empty.
pub use temporal_context_stored_request_cli::read_temporal_context_stored_request_cli_stdin;
Loading
Loading