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-collection-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp-export-list list` mints naruon `GET /v1/exports` onto spawned `tepp-loopback` TCP (ADR 0076). Metric-free receipts only. `tepp.scientific_acceptance.v1` never appears. Does not infer causality. LineageWeave refused. `NaruonLiveService` stays POST-only. Not export-retrieval CLI, not GAP-010 Figma/export, not persistence.
1 change: 1 addition & 0 deletions CHANGELOG.d/export-collection-get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `tepp_api` loopback `GET /v1/exports` enumerates authorized purpose-bound export identities on `AnalysisRunLiveService` / `tepp-loopback` (ADR 0075). Metric-free receipts only. `tepp.scientific_acceptance.v1` never appears. Does not infer causality. LineageWeave refused. `NaruonLiveService` stays POST-only. Not export retrieval GET, 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) |
| Export collection GET doctoring | [`docs/research/export-collection-http.md`](docs/research/export-collection-http.md) |
| Export collection CLI doctoring | [`docs/research/export-collection-cli.md`](docs/research/export-collection-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-export-list"
path = "src/bin/tepp_export_list.rs"
test = false
bench = false

[lints]
workspace = true
72 changes: 55 additions & 17 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` to enumerate those identities.
//! It accepts transport acknowledgements, temporal evidence context, and
//! export identities only; completed psychometric results remain outside this
//! crate.
Expand All @@ -12,22 +13,26 @@ use std::collections::HashMap;
use std::io::Write;
use std::net::{SocketAddr, TcpListener};

use crate::export_collection_http::{
is_export_collection_path, page_export_collection_items, parse_export_collection_page_cursor,
parse_export_collection_page_limit, ExportCollection,
};
use crate::export_http::{export_retrieval_path_id, refuse_metrics_on_export_retrieval_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 +167,9 @@ impl AnalysisRunLiveService {
let (method, path) = parse_request_line(lines.next().unwrap_or(""))?;
let headers = parse_headers(&mut lines)?;
if method == "GET" {
if is_export_collection_path(path) {
return self.list_exports(&headers, body);
}
if matches!(
export_retrieval_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
Expand Down Expand Up @@ -342,6 +350,37 @@ impl AnalysisRunLiveService {
Ok(json_response(200, "OK", response_body))
}

fn list_exports(
&self,
headers: &HashMap<String, String>,
body: &str,
) -> Result<NaruonLiveResponse, ApiError> {
if !body.trim().is_empty() {
return Err(ApiError::InvalidWirePayload);
Comment on lines +358 to +359

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Whitespace GET bodies bypass refusal

list_exports trims the body before testing emptiness. Whitespace-only bodies receive a successful collection despite the empty-body contract.

Suggested change
if !body.trim().is_empty() {
return Err(ApiError::InvalidWirePayload);
if !body.is_empty() {
return Err(ApiError::InvalidWirePayload);
Devin Review

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

}
refuse_metrics_on_export_retrieval_payload(body)?;
let consumer = require_headers(headers, self.bound_addr, false)?;
if consumer != NARUON_CONSUMER_CODE {
return Err(ApiError::InvalidWirePayload);
}
if headers.contains_key("idempotency-key") {
return Err(ApiError::InvalidWirePayload);
}
let limit =
parse_export_collection_page_limit(headers.get("tepp-page-limit").map(String::as_str))?;
let cursor = parse_export_collection_page_cursor(
headers.get("tepp-page-cursor").map(String::as_str),
)?;
let items = self
.authorized_exports
.values()
.map(|stored| stored.retrieval.clone())
.collect();
Comment on lines +374 to +378

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Export listing crosses tenant boundaries

list_exports returns every stored export to any naruon caller without tenant or principal scope. One caller can enumerate other tenants’ export capabilities.

Devin Review

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

let (page, next_cursor) = page_export_collection_items(items, cursor.as_deref(), limit);
let collection = ExportCollection::new(page, next_cursor)?;
Ok(json_response(200, "OK", collection.to_json()?))
}

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 @@ -1135,7 +1173,7 @@ mod tests {
"GET {NARUON_EXPORT_PATH} HTTP/1.1\r\nHost: 127.0.0.1\r\ncontent-type: application/json\r\ntepp-consumer: naruon\r\ntepp-contract-version: 1\r\ncontent-length: 0\r\n\r\n"
))
.status_code,
400
200
);
assert_eq!(
service
Expand Down
30 changes: 30 additions & 0 deletions crates/tepp_api/src/bin/tepp_export_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//! Operator CLI for loopback naruon export collection GET.

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

use tepp_api::{
execute_export_collection_cli, read_export_collection_cli_stdin,
render_export_collection_cli_stdout, ApiError, ExportCollectionCliInvocation,
};

fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(_) => ExitCode::FAILURE,
}
Comment on lines +11 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 CLI failures hide their cause

main discards every error and returns only a failure code. Operators cannot distinguish bad flags, connection failures, timeouts, or invalid responses.

Devin Review

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

}

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Published CLI lacks process-level coverage

Tests call library functions but never launch tepp-export-list. Its argument parsing, stdin mode, exit status, and stdout remain outside end-to-end coverage.

Devin Review

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

Comment on lines +23 to +29

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: Success status check is redundant

render_export_collection_cli_stdout already accepts only status 200. The later 2xx check cannot change any successful execution.

Devin Review

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

}
Loading
Loading