Skip to content
Closed
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/export-stored-request-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `GET /v1/exports/{export_id}/request` returns the accepted naruon export-authorization request on `tepp-loopback` (ADR 0089). Metric-free. LineageWeave 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 @@ -11,6 +11,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) |
| Modular/API integration contract | [`docs/API_CONTRACT.md`](docs/API_CONTRACT.md) |
| naruon modular consumer contract | [`docs/connectors/naruon-artifact-consumer.md`](docs/connectors/naruon-artifact-consumer.md) |
| Export stored-request GET doctoring | [`docs/research/export-stored-request-get.md`](docs/research/export-stored-request-get.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 Expand Up @@ -109,6 +110,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin
| Architecture | [`ARCHITECTURE.md`](ARCHITECTURE.md) |
| Modular/API integration contract | [`docs/API_CONTRACT.md`](docs/API_CONTRACT.md) |
| naruon modular consumer contract | [`docs/connectors/naruon-artifact-consumer.md`](docs/connectors/naruon-artifact-consumer.md) |
| Export stored-request GET doctoring | [`docs/research/export-stored-request-get.md`](docs/research/export-stored-request-get.md) |
| contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) |
| UML/runtime/scientific flows | [`docs/UML.md`](docs/UML.md) |
| Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) |
Expand Down
137 changes: 121 additions & 16 deletions crates/tepp_api/src/analysis_run_live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//! This module keeps the Naruon compatibility listener intact while providing
//! the shared `/v1/analysis-runs` and cutoff-safe `/v1/temporal-context`
//! boundaries needed by Naruon and `LineageWeave`. Naruon may also POST and
//! GET `/v1/exports/{export_id}` for metric-free purpose-bound retrieval.
//! GET `/v1/exports/{export_id}` for metric-free purpose-bound retrieval, and
//! GET `/v1/exports/{export_id}/request` for the stored authorization request.
//! It accepts transport acknowledgements, temporal evidence context, and
//! export identities only; completed psychometric results remain outside this
//! crate.
Expand All @@ -13,21 +14,24 @@ use std::io::Write;
use std::net::{SocketAddr, TcpListener};

use crate::export_http::{export_retrieval_path_id, refuse_metrics_on_export_retrieval_payload};
use crate::export_stored_request_http::{
export_stored_request_path_id, refuse_metrics_on_export_stored_request_payload,
};
use crate::lineageweave_http::{
LINEAGEWEAVE_CONSUMER_CODE, NARUON_CONSUMER_CODE, consumer_is_supported,
consumer_is_supported, LINEAGEWEAVE_CONSUMER_CODE, NARUON_CONSUMER_CODE,
};
use crate::live_http::{
header_value, map_io_error, parse_headers, parse_request_line, read_http_request_with_limit,
split_request_with_limit, validate_common_headers,
};
use crate::naruon_http::{NARUON_ANALYSIS_RUN_PATH, NARUON_EXPORT_PATH};
use crate::{
AnalysisRunAccepted, AnalysisRunRequest, AnalyticalPurpose, ApiError,
DEFAULT_PROJECT_HISTORY_BYTE_LIMIT, ErrorEnvelope, ExportAuthorizationRequest, ExportRetrieval,
NARUON_LIVE_IO_TIMEOUT, NaruonLiveResponse, PROJECT_HISTORY_PATH, ProjectHistoryProjection,
ProjectHistoryRequest, TEMPORAL_CONTEXT_PATH, TemporalContextRequest, authorize_export,
build_temporal_context, project_history_projection, requests_are_idempotent_matches,
require_export_allowed,
authorize_export, build_temporal_context, project_history_projection,
requests_are_idempotent_matches, require_export_allowed, AnalysisRunAccepted,
AnalysisRunRequest, AnalyticalPurpose, ApiError, ErrorEnvelope, ExportAuthorizationRequest,
ExportRetrieval, NaruonLiveResponse, ProjectHistoryProjection, ProjectHistoryRequest,
TemporalContextRequest, DEFAULT_PROJECT_HISTORY_BYTE_LIMIT, NARUON_LIVE_IO_TIMEOUT,
PROJECT_HISTORY_PATH, TEMPORAL_CONTEXT_PATH,
};

const MAX_LIVE_REQUEST_BODY_BYTES: usize = DEFAULT_PROJECT_HISTORY_BYTE_LIMIT;
Expand Down Expand Up @@ -162,6 +166,12 @@ impl AnalysisRunLiveService {
let (method, path) = parse_request_line(lines.next().unwrap_or(""))?;
let headers = parse_headers(&mut lines)?;
if method == "GET" {
if matches!(
export_stored_request_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
) {
return self.get_export_stored_request(path, &headers, body);
}
if matches!(
export_retrieval_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
Expand Down Expand Up @@ -342,6 +352,35 @@ impl AnalysisRunLiveService {
Ok(json_response(200, "OK", response_body))
}

fn get_export_stored_request(
&self,
path: &str,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
if !body.trim().is_empty() {
return Err(ApiError::InvalidWirePayload);
}
refuse_metrics_on_export_stored_request_payload(body)?;
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != NARUON_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
let export_id = export_stored_request_path_id(path)?;
let replay_key = self
.exports_by_id
.get(&export_id)
.cloned()
.ok_or(ApiError::InvalidWirePayload)?;
let stored = self
.authorized_exports
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
let response_body = crate::wire::to_json(&stored.request)?;
refuse_metrics_on_export_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 @@ -417,17 +456,16 @@ mod tests {
use std::time::Duration;

use super::{
AnalysisRunLiveService, consumer_tenant_idempotency_key, declared_content_length,
error_envelope_json, host_implies_table_access, map_io_error, parse_headers,
require_headers, split_header_line, status_for,
consumer_tenant_idempotency_key, declared_content_length, error_envelope_json,
host_implies_table_access, map_io_error, parse_headers, require_headers, split_header_line,
status_for, AnalysisRunLiveService,
};
use crate::live_http::{host_is_loopback, read_http_request, split_request};
use crate::{
ANALYSIS_RUN_CONTRACT_VERSION, AnalysisRunRequest, ApiError,
DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, ErrorEnvelope, LINEAGEWEAVE_CONSUMER_CODE,
NARUON_ANALYSIS_RUN_PATH, NARUON_CONSUMER_CODE, NARUON_EXPORT_PATH,
NARUON_LIVE_HEADER_BYTE_LIMIT, NARUON_LIVE_HEADER_COUNT_LIMIT, NARUON_LIVE_IO_TIMEOUT,
TEMPORAL_CONTEXT_PATH,
AnalysisRunRequest, ApiError, ErrorEnvelope, ANALYSIS_RUN_CONTRACT_VERSION,
DEFAULT_ANALYSIS_RUN_BYTE_LIMIT, LINEAGEWEAVE_CONSUMER_CODE, NARUON_ANALYSIS_RUN_PATH,
NARUON_CONSUMER_CODE, NARUON_EXPORT_PATH, NARUON_LIVE_HEADER_BYTE_LIMIT,
NARUON_LIVE_HEADER_COUNT_LIMIT, NARUON_LIVE_IO_TIMEOUT, TEMPORAL_CONTEXT_PATH,
};

fn sample_run() -> AnalysisRunRequest {
Expand Down Expand Up @@ -1220,6 +1258,73 @@ mod tests {
);
}

#[test]
fn handler_returns_stored_export_authorization_request_and_fails_closed() {
use crate::ExportAuthorizationRequest;

let request = ExportAuthorizationRequest {
tenant_workspace_id: "export-live-tenant".into(),
principal_id: "principal-analyst-1".into(),
purpose: crate::AnalyticalPurpose::ModularServiceConsumer,
artifact_id: "artifact-live-1".into(),
includes_source_text: false,
};
let body = crate::wire::to_json(&request).expect("export json");
let mut service = AnalysisRunLiveService::new();
let posted = service.handle_http_request(&export_post_http(
&body,
NARUON_CONSUMER_CODE,
"export-idem-1",
));
assert_eq!(posted.status_code, 200);
let retrieval = crate::ExportRetrieval::from_json(&posted.body).expect("posted retrieval");
let got = service.handle_http_request(&format!(
"GET {NARUON_EXPORT_PATH}/{}/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\ncontent-length: 0\r\n\r\n",
retrieval.export_id
));
assert_eq!(got.status_code, 200, "{}", got.body);
let stored: ExportAuthorizationRequest = crate::wire::from_json(&got.body).expect("stored");
assert_eq!(stored, request);
assert!(!got.body.contains("rmse"));
assert!(!got.body.contains("tepp.scientific_acceptance.v1"));
assert!(!got.body.contains("scientific_acceptance"));
assert_eq!(
service
.handle_http_request(&format!(
"GET {NARUON_EXPORT_PATH}/{}/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",
retrieval.export_id
))
.status_code,
400
);
assert_eq!(
service
.handle_http_request(&format!(
"GET {NARUON_EXPORT_PATH}/{}/cancel 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",
retrieval.export_id
))
.status_code,
400
);
assert_eq!(
service
.handle_http_request(&format!(
"GET {NARUON_EXPORT_PATH}/missing/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\ncontent-length: 0\r\n\r\n"
))
.status_code,
400
);
assert_eq!(
service
.handle_http_request(&format!(
"GET {NARUON_EXPORT_PATH}/{}/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\ncontent-length: 2\r\n\r\n{{}}",
retrieval.export_id
))
.status_code,
400
);
}

fn export_post_http(body: &str, consumer: &str, idempotency_key: &str) -> String {
format!(
"POST {NARUON_EXPORT_PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: {consumer}\r\ntepp-contract-version: 1\r\nidempotency-key: {idempotency_key}\r\ncontent-length: {}\r\n\r\n{body}",
Expand Down
Loading
Loading