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/export-stored-request-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp-export-request get` mints naruon stored-request GET onto spawned `tepp-loopback` TCP (ADR 0090). Metric-free. LineageWeave 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/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.
4 changes: 4 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ 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) |
| Export stored-request CLI doctoring | [`docs/research/export-stored-request-cli.md`](docs/research/export-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 Expand Up @@ -109,6 +111,8 @@ 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) |
| Export stored-request CLI doctoring | [`docs/research/export-stored-request-cli.md`](docs/research/export-stored-request-cli.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
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-export-request"
path = "src/bin/tepp_export_request.rs"
test = false
bench = false

[lints]
workspace = true
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
33 changes: 33 additions & 0 deletions crates/tepp_api/src/bin/tepp_export_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Operator CLI for loopback naruon export stored-request GET.

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

use tepp_api::{
ApiError, ExportStoredRequestCliInvocation, execute_export_stored_request_cli,
read_export_stored_request_cli_stdin, render_export_stored_request_cli_stdout,
};

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

fn run() -> Result<(), ApiError> {
let args: Vec<String> = std::env::args().skip(1).collect();
let body = read_export_stored_request_cli_stdin(io::stdin().is_terminal(), io::stdin())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Invalid commands can hang on stdin

run drains non-terminal stdin before validating arguments. Invalid commands can block indefinitely while an open input pipe sends no data.

Prompt for agents
Refactor crates/tepp_api/src/bin/tepp_export_request.rs so argument syntax and required flags are validated before reading non-terminal stdin. ExportStoredRequestCliInvocation::from_args currently combines argument parsing with body validation, so split parsing/assembly or add an argument-only parsing stage. Preserve the requirement that a valid GET invocation reads stdin and rejects any nonempty body.
Devin Review

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

let invocation = ExportStoredRequestCliInvocation::from_args(&args, body)?;
let response = execute_export_stored_request_cli(&invocation)?;
let stdout = render_export_stored_request_cli_stdout(&invocation, &response)?;
println!("{stdout}");
if (200..300).contains(&response.status_code) {
Ok(())
} else {
Err(ApiError::InvalidWirePayload)
}
}
Loading
Loading