Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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/project-history-stored-request-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp-project-history-request get` mints LineageWeave stored-request GET onto spawned `tepp-loopback` TCP (ADR 0088). Metric-free; stored projection `inference_status` remains `temporal_association_only`. Does not infer causality. Naruon refused. `NaruonLiveService` stays POST-only. Does not re-open cancel lineages. Not GAP-010 Figma/export, not persistence.
1 change: 1 addition & 0 deletions CHANGELOG.d/project-history-stored-request-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `GET /v1/project-histories/{idempotency_key}/request` returns the accepted LineageWeave create request on `tepp-loopback` (ADR 0087). Metric-free; stored projection `inference_status` remains `temporal_association_only`. Does not infer causality. Naruon refused. `NaruonLiveService` stays POST-only. Does not re-open 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) |
| Project-history collection GET doctoring | [`docs/research/project-history-collection-http.md`](docs/research/project-history-collection-http.md) |
| Project-history GET-by-id doctoring | [`docs/research/project-history-retrieval-http.md`](docs/research/project-history-retrieval-http.md) |
| Project-history stored-request GET doctoring | [`docs/research/project-history-stored-request-get.md`](docs/research/project-history-stored-request-get.md) |
| Project-history stored-request CLI doctoring | [`docs/research/project-history-stored-request-cli.md`](docs/research/project-history-stored-request-cli.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) |
| UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.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-project-history-request"
path = "src/bin/tepp_project_history_request.rs"
test = false
bench = false

[lints]
workspace = true
88 changes: 87 additions & 1 deletion crates/tepp_api/src/analysis_run_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ use crate::{
is_project_history_collection_path, page_project_history_collection_items,
parse_project_history_collection_page_cursor, parse_project_history_collection_page_limit,
project_history_projection, project_history_retrieval_path_id,
refuse_metrics_on_project_history_retrieval_payload, requests_are_idempotent_matches,
project_history_stored_request_path_id, refuse_metrics_on_project_history_retrieval_payload,
refuse_metrics_on_project_history_stored_request_payload, requests_are_idempotent_matches,
};

const MAX_LIVE_REQUEST_BODY_BYTES: usize = DEFAULT_PROJECT_HISTORY_BYTE_LIMIT;
Expand Down Expand Up @@ -153,6 +154,12 @@ impl AnalysisRunLiveService {
if is_project_history_collection_path(path) {
return self.list_project_histories(&headers, body);
}
if matches!(
project_history_stored_request_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
) {
return self.get_project_history_stored_request(path, &headers, body);
}
if matches!(
project_history_retrieval_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
Expand Down Expand Up @@ -321,6 +328,40 @@ impl AnalysisRunLiveService {
Ok(json_response(200, "OK", response_body))
}

fn get_project_history_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_project_history_stored_request_payload(body)?;
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != LINEAGEWEAVE_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
if headers.contains_key("tepp-page-limit") || headers.contains_key("tepp-page-cursor") {
return Err(ApiError::InvalidWirePayload);
}
let tenant_workspace_id = header_value(headers, PROJECT_HISTORY_RETRIEVAL_TENANT_HEADER)?;
crate::project_history::validate_project_history_registry_identity(tenant_workspace_id)?;
let idempotency_key = project_history_stored_request_path_id(path)?;
let replay_key =
consumer_tenant_idempotency_key(consumer, tenant_workspace_id, &idempotency_key);
let (stored_request, projection) = self
.accepted_project_histories
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
Comment on lines +348 to +356

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: Stored lookup preserves tenant scope

The registry key combines consumer, required tenant, and decoded caller key. Identical keys in different tenants remain isolated.

Devin Review

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

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

fn response_from_error(&mut self, error: ApiError) -> NaruonLiveResponse {
let request_id = format!("analysis-run-live-{}", self.next_request_serial);
self.next_request_serial += 1;
Expand Down Expand Up @@ -1237,6 +1278,51 @@ mod tests {
assert!(!collection.body.contains("evidence_text"));
}

#[test]
fn project_history_stored_request_get_returns_create_request_and_fails_closed() {
let mut service = AnalysisRunLiveService::new();
let first = sample_project_history("idem-a", "project-a");
assert_eq!(
service
.handle_http_request(&project_history_post(&first))
.status_code,
200
);
let got = service.handle_http_request(&format!(
"GET {PROJECT_HISTORY_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\ntepp-tenant-workspace-id: history-tenant\r\ncontent-length: 0\r\n\r\n"
));
assert_eq!(got.status_code, 200, "{}", got.body);
let stored = ProjectHistoryRequest::from_json(&got.body).expect("stored");
assert_eq!(stored, first);
assert!(!got.body.contains("rmse"));
assert!(!got.body.contains("tepp.scientific_acceptance.v1"));
assert!(!got.body.contains("causal_score"));
assert_eq!(
service
.handle_http_request(&format!(
"GET {PROJECT_HISTORY_PATH}/idem-a/request 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\ntepp-tenant-workspace-id: history-tenant\r\ncontent-length: 0\r\n\r\n"
))
.status_code,
400
);
assert_eq!(
service
.handle_http_request(&format!(
"GET {PROJECT_HISTORY_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\ntepp-tenant-workspace-id: history-tenant\r\ncontent-length: 0\r\n\r\n"
))
.status_code,
400
);
assert_eq!(
service
.handle_http_request(&format!(
"GET {PROJECT_HISTORY_PATH}/missing/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\ntepp-tenant-workspace-id: history-tenant\r\ncontent-length: 0\r\n\r\n"
))
.status_code,
400
);
}

struct ScriptedRead {
reader: Cursor<Vec<u8>>,
first_error: Option<std::io::ErrorKind>,
Expand Down
35 changes: 35 additions & 0 deletions crates/tepp_api/src/bin/tepp_project_history_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Operator CLI for loopback `LineageWeave` project-history stored-request GET.

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

use tepp_api::{
ApiError, ProjectHistoryStoredRequestCliInvocation, execute_project_history_stored_request_cli,
read_project_history_stored_request_cli_stdin,
render_project_history_stored_request_cli_stdout,
};

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

fn run() -> Result<(), ApiError> {
let args: Vec<String> = std::env::args().skip(1).collect();
let body =
read_project_history_stored_request_cli_stdin(io::stdin().is_terminal(), io::stdin())?;
let invocation = ProjectHistoryStoredRequestCliInvocation::from_args(&args, body)?;
let response = execute_project_history_stored_request_cli(&invocation)?;
let stdout = render_project_history_stored_request_cli_stdout(&invocation, &response)?;
println!("{stdout}");
if (200..300).contains(&response.status_code) {
Ok(())
} else {
Err(ApiError::InvalidWirePayload)
Comment on lines +28 to +33

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Failures print request-shaped output

For any non-2xx response, run prints the error envelope to stdout. Pipelines can consume error JSON as a stored request.

Suggested change
let stdout = render_project_history_stored_request_cli_stdout(&invocation, &response)?;
println!("{stdout}");
if (200..300).contains(&response.status_code) {
Ok(())
} else {
Err(ApiError::InvalidWirePayload)
let stdout = render_project_history_stored_request_cli_stdout(&invocation, &response)?;
if (200..300).contains(&response.status_code) {
println!("{stdout}");
Ok(())
} else {
eprintln!("{stdout}");
Err(ApiError::InvalidWirePayload)
Devin Review

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

}
}
26 changes: 26 additions & 0 deletions crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ mod orchestration;
mod project_history;
mod project_history_collection_http;
mod project_history_retrieval_http;
mod project_history_stored_request_cli;
mod project_history_stored_request_http;
mod project_journey;
mod provider_payload;
mod temporal_context;
Expand Down Expand Up @@ -266,6 +268,30 @@ pub use project_history_retrieval_http::lineageweave_project_history_retrieval_e
pub use project_history_retrieval_http::project_history_retrieval_path_id;
/// Refuse scientific-metric and causal-score keys on retrieval JSON.
pub use project_history_retrieval_http::refuse_metrics_on_project_history_retrieval_payload;
/// Validated loopback CLI invocation for project-history stored-request GET.
pub use project_history_stored_request_cli::ProjectHistoryStoredRequestCliInvocation;
/// Loopback CLI verb for project-history stored-request GET.
pub use project_history_stored_request_cli::ProjectHistoryStoredRequestCliVerb;
/// Compose HTTP/1.1 from a stored-request CLI invocation.
pub use project_history_stored_request_cli::compose_project_history_stored_request_cli_http;
/// Dispatch a stored-request CLI invocation against an in-process listener.
pub use project_history_stored_request_cli::dispatch_project_history_stored_request_cli;
/// Execute a stored-request CLI invocation over loopback TCP.
pub use project_history_stored_request_cli::execute_project_history_stored_request_cli;
/// Render `tepp-loopback` HTTP/1.1 from a stored-request exchange.
pub use project_history_stored_request_cli::loopback_http1_from_project_history_stored_request_exchange;
/// Read leftover stdin for stored-request GET; empty is admitted.
pub use project_history_stored_request_cli::read_project_history_stored_request_cli_stdin;
/// Filter stored-request CLI stdout so scientific-acceptance never prints.
pub use project_history_stored_request_cli::render_project_history_stored_request_cli_stdout;
/// Whether a path is the project-history stored-request extra-segment.
pub use project_history_stored_request_http::is_project_history_stored_request_path;
/// `LineageWeave` GET exchange for one stored project-history create request.
pub use project_history_stored_request_http::lineageweave_project_history_stored_request_exchange;
/// Extract the opaque idempotency key from a stored-request GET path.
pub use project_history_stored_request_http::project_history_stored_request_path_id;
/// Refuse scientific-metric and causal-score keys on stored-request JSON.
pub use project_history_stored_request_http::refuse_metrics_on_project_history_stored_request_payload;
/// Maximum posterior Project Journey artifact size.
pub use project_journey::DEFAULT_PROJECT_JOURNEY_BYTE_LIMIT;
/// Exact posterior Project Journey schema identity.
Expand Down
Loading
Loading