From 5b46d78aef81902ab12b8ad691fd83be68d616cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 18:16:58 +0000 Subject: [PATCH] feat(api): mint interpretation-run lookup stored-request GET from a dedicated CLI Publish tepp-interpretation-run-lookup-request get so operators who hold a 202 receipt can recover the stored create onto spawned tepp-orchestrator-loopback TCP. Empty stdin admitted. Metric-free. Naruon and LineageWeave refused. --- ...pretation-run-lookup-stored-request-cli.md | 1 + DOCUMENTATION.md | 1 + crates/orchestrator_live/Cargo.toml | 6 + .../tepp_interpretation_run_lookup_request.rs | 39 + ...pretation_run_lookup_stored_request_cli.rs | 689 ++++++++++++++++++ crates/orchestrator_live/src/lib.rs | 19 + ..._run_lookup_stored_request_cli_contract.rs | 152 ++++ docs/API_CONTRACT.md | 1 + docs/TRACEABILITY.md | 1 + ...pretation-run-lookup-stored-request-cli.md | 87 +++ docs/adr/README.md | 1 + ...pretation-run-lookup-stored-request-cli.md | 17 + 12 files changed, 1014 insertions(+) create mode 100644 CHANGELOG.d/interpretation-run-lookup-stored-request-cli.md create mode 100644 crates/orchestrator_live/src/bin/tepp_interpretation_run_lookup_request.rs create mode 100644 crates/orchestrator_live/src/interpretation_run_lookup_stored_request_cli.rs create mode 100644 crates/orchestrator_live/tests/interpretation_run_lookup_stored_request_cli_contract.rs create mode 100644 docs/adr/0098-interpretation-run-lookup-stored-request-cli.md create mode 100644 docs/research/interpretation-run-lookup-stored-request-cli.md diff --git a/CHANGELOG.d/interpretation-run-lookup-stored-request-cli.md b/CHANGELOG.d/interpretation-run-lookup-stored-request-cli.md new file mode 100644 index 00000000..f1a8fe09 --- /dev/null +++ b/CHANGELOG.d/interpretation-run-lookup-stored-request-cli.md @@ -0,0 +1 @@ +- Published `tepp-interpretation-run-lookup-request get` mints `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request` onto spawned `tepp-orchestrator-loopback` TCP (ADR 0098). Dual identity of stored-request CLI (`idempotency_key`). Empty stdin admitted. Public bind/`localhost`/`http` origin/unpublished consumer/credential flags fail closed. `scientific_authority` remains false. Does not infer causality. Naruon and LineageWeave refused. `NaruonLiveService` stays POST-only. Does not re-open cancel lineages. Not GAP-010 Figma/export, not persistence. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ac4905fa..6bc8b162 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -21,6 +21,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Interpretation-run lookup GET doctoring | [`docs/research/interpretation-run-lookup-http.md`](docs/research/interpretation-run-lookup-http.md) | | Interpretation-run lookup CLI doctoring | [`docs/research/interpretation-run-lookup-cli.md`](docs/research/interpretation-run-lookup-cli.md) | | Interpretation-run lookup stored-request GET doctoring | [`docs/research/interpretation-run-lookup-stored-request-http.md`](docs/research/interpretation-run-lookup-stored-request-http.md) | +| Interpretation-run lookup stored-request CLI doctoring | [`docs/research/interpretation-run-lookup-stored-request-cli.md`](docs/research/interpretation-run-lookup-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) | diff --git a/crates/orchestrator_live/Cargo.toml b/crates/orchestrator_live/Cargo.toml index f9583553..f89c6b0f 100644 --- a/crates/orchestrator_live/Cargo.toml +++ b/crates/orchestrator_live/Cargo.toml @@ -41,5 +41,11 @@ path = "src/bin/tepp_interpretation_run_lookup.rs" test = false bench = false +[[bin]] +name = "tepp-interpretation-run-lookup-request" +path = "src/bin/tepp_interpretation_run_lookup_request.rs" +test = false +bench = false + [lints] workspace = true diff --git a/crates/orchestrator_live/src/bin/tepp_interpretation_run_lookup_request.rs b/crates/orchestrator_live/src/bin/tepp_interpretation_run_lookup_request.rs new file mode 100644 index 00000000..6db7801c --- /dev/null +++ b/crates/orchestrator_live/src/bin/tepp_interpretation_run_lookup_request.rs @@ -0,0 +1,39 @@ +//! Operator CLI for loopback contextual-orchestrator lookup stored-request GET. + +use std::io::{self, IsTerminal}; +use std::process::ExitCode; + +use orchestrator_live::{ + InterpretationRunLookupStoredRequestCliInvocation, OrchestratorLiveError, + execute_interpretation_run_lookup_stored_request_cli, + read_interpretation_run_lookup_stored_request_cli_stdin, + render_interpretation_run_lookup_stored_request_cli_stdout, +}; + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(_) => ExitCode::FAILURE, + } +} + +fn run() -> Result<(), OrchestratorLiveError> { + let args: Vec = std::env::args().skip(1).collect(); + match args.first().map(String::as_str) { + Some("get") => run_get(&args), + _ => Err(OrchestratorLiveError::InvalidWirePayload), + } +} + +fn run_get(args: &[String]) -> Result<(), OrchestratorLiveError> { + let body = read_interpretation_run_lookup_stored_request_cli_stdin( + io::stdin().is_terminal(), + io::stdin(), + )?; + let invocation = InterpretationRunLookupStoredRequestCliInvocation::from_args(args, body)?; + let response = execute_interpretation_run_lookup_stored_request_cli(&invocation)?; + let stdout = + render_interpretation_run_lookup_stored_request_cli_stdout(&invocation, &response)?; + println!("{stdout}"); + Ok(()) +} diff --git a/crates/orchestrator_live/src/interpretation_run_lookup_stored_request_cli.rs b/crates/orchestrator_live/src/interpretation_run_lookup_stored_request_cli.rs new file mode 100644 index 00000000..9b7060ed --- /dev/null +++ b/crates/orchestrator_live/src/interpretation_run_lookup_stored_request_cli.rs @@ -0,0 +1,689 @@ +//! Operator loopback CLI for contextual-orchestrator lookup stored-request GET. +//! +//! GAP-003A unique slice: operators run +//! `tepp-interpretation-run-lookup-request get` to mint +//! `contextual_orchestrator_interpretation_run_lookup_stored_request_exchange` +//! onto spawned `tepp-orchestrator-loopback` TCP. Stdout is the stored create +//! request with `scientific_authority=false`. `tepp.scientific_acceptance.v1` +//! never appears. The CLI does not infer causality or call a model provider. +//! Naruon and `LineageWeave` are refused. `NaruonLiveService` stays POST-only. +//! This module does not duplicate lookup stored-request GET (#469), lookup +//! GET/CLI (#467/#468), stored-request GET/CLI (#453/#454), GET-by-id (#438), +//! retrieval CLI (#439), collection GET/CLI (#433/#436), create CLI (#425), +//! export lookup (#466), analysis-run lookup (#380/#401), or cancel lineages +//! (closed). Persistence remains GAP-003B. + +use std::fmt::Write as _; +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpStream}; +use std::time::Duration; + +use crate::http::{header_is_credential, map_io_error}; +use crate::interpretation_run_cli::CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE; +use crate::interpretation_run_lookup_http::INTERPRETATION_RUN_LOOKUP_PREFIX; +use crate::interpretation_run_lookup_stored_request_http::{ + InterpretationRunLookupStoredRequestHttpExchange, + contextual_orchestrator_interpretation_run_lookup_stored_request_exchange, + interpretation_run_lookup_stored_request_path_id, +}; +use crate::interpretation_run_stored_request_http::refuse_metrics_on_interpretation_run_stored_request_payload; +use crate::request::{ + DEFAULT_INTERPRETATION_BYTE_LIMIT, InterpretationRunRequest, require_nonempty, +}; +use crate::{ + LIVE_HEADER_BYTE_LIMIT, LIVE_HEADER_COUNT_LIMIT, OrchestratorLiveError, + OrchestratorLiveResponse, OrchestratorLiveService, +}; + +const SCIENTIFIC_ACCEPTANCE_SCHEMA: &str = "tepp.scientific_acceptance.v1"; +const CLI_IO_TIMEOUT: Duration = Duration::from_secs(2); +const MAXIMUM_HTTP_RESPONSE_BYTES: usize = + LIVE_HEADER_BYTE_LIMIT + 4 + DEFAULT_INTERPRETATION_BYTE_LIMIT; + +/// Supported operator verbs for lookup stored-request GET. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum InterpretationRunLookupStoredRequestCliVerb { + /// `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request`. + Get, +} + +impl InterpretationRunLookupStoredRequestCliVerb { + /// Parse one exact lowercase verb token. + /// + /// # Errors + /// + /// Returns [`OrchestratorLiveError::InvalidWirePayload`] for an unknown token. + pub fn parse(token: &str) -> Result { + match token { + "get" => Ok(Self::Get), + _ => Err(OrchestratorLiveError::InvalidWirePayload), + } + } + + /// Return the canonical lowercase verb token. + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Get => "get", + } + } +} + +/// One operator CLI invocation against a loopback lookup stored-request listener. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct InterpretationRunLookupStoredRequestCliInvocation { + /// CLI verb to execute. + pub verb: InterpretationRunLookupStoredRequestCliVerb, + /// Loopback `host:port` of `tepp-orchestrator-loopback`. + pub host: String, + /// Published HTTPS origin used to mint the typed exchange. + pub origin: String, + /// Published modular consumer. Lookup stored-request GET admits + /// `contextual-orchestrator` only. + pub consumer: String, + /// Server-assigned opaque interpretation-run identity. + pub interpretation_run_id: String, + /// JSON body. GET requires empty. + pub body: String, +} + +impl InterpretationRunLookupStoredRequestCliInvocation { + /// Parse argv plus stdin body into a validated loopback invocation. + /// + /// Empty stdin is admitted. Nonempty leftover stdin fails closed. + /// + /// # Errors + /// + /// Returns a fail-closed error for unknown verbs, missing required flags, a + /// non-loopback host, a non-`https` origin, an unpublished consumer, + /// credential-shaped flags, a hostile identity, or a nonempty body. + pub fn from_args(args: I, body: impl Into) -> Result + where + I: IntoIterator, + S: AsRef, + { + let tokens: Vec = args + .into_iter() + .map(|token| token.as_ref().to_owned()) + .collect(); + let (verb_token, rest) = tokens + .split_first() + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let verb = InterpretationRunLookupStoredRequestCliVerb::parse(verb_token)?; + let flags = parse_flags(rest)?; + assemble_invocation(verb, flags, body.into()) + } + + /// Reject a non-loopback host, unpublished consumer, or hostile GET body. + /// + /// # Errors + /// + /// Returns [`OrchestratorLiveError::AuthorizationDenied`] for a non-loopback + /// host and [`OrchestratorLiveError::InvalidWirePayload`] or + /// [`OrchestratorLiveError::LimitExceeded`] for empty, unpublished, + /// nonempty-body, reserved-prefix, or oversized fields. + pub fn validate(&self) -> Result<(), OrchestratorLiveError> { + require_loopback_host(&self.host)?; + require_nonempty(&self.origin)?; + if !self.origin.starts_with("https://") { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + require_nonempty(&self.consumer)?; + if self.consumer != CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + require_nonempty(&self.interpretation_run_id)?; + if self.interpretation_run_id == INTERPRETATION_RUN_LOOKUP_PREFIX { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + if self.interpretation_run_id.contains('/') || self.interpretation_run_id.contains('\0') { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + if !self.body.is_empty() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + refuse_scientific_acceptance(&self.body)?; + refuse_metrics_on_interpretation_run_stored_request_payload(&self.body)?; + Ok(()) + } +} + +struct ParsedFlags { + host: Option, + origin: Option, + consumer: Option, + interpretation_run_id: Option, +} + +fn parse_flags(rest: &[String]) -> Result { + let mut flags = ParsedFlags { + host: None, + origin: None, + consumer: None, + interpretation_run_id: None, + }; + let mut index = 0; + while index < rest.len() { + let flag = rest[index].as_str(); + if !flag.starts_with("--") { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let name = &flag[2..]; + if header_is_credential(name) { + return Err(OrchestratorLiveError::AuthorizationDenied); + } + let slot = match name { + "host" => &mut flags.host, + "origin" => &mut flags.origin, + "consumer" => &mut flags.consumer, + "interpretation-run-id" => &mut flags.interpretation_run_id, + _ => return Err(OrchestratorLiveError::InvalidWirePayload), + }; + if slot.is_some() || index + 1 >= rest.len() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let value = rest[index + 1].as_str(); + require_nonempty(value)?; + *slot = Some(value.to_owned()); + index += 2; + } + Ok(flags) +} + +fn assemble_invocation( + verb: InterpretationRunLookupStoredRequestCliVerb, + flags: ParsedFlags, + body: String, +) -> Result { + let invocation = InterpretationRunLookupStoredRequestCliInvocation { + verb, + host: flags + .host + .ok_or(OrchestratorLiveError::InvalidWirePayload)?, + origin: flags + .origin + .ok_or(OrchestratorLiveError::InvalidWirePayload)?, + consumer: flags + .consumer + .unwrap_or_else(|| CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE.to_owned()), + interpretation_run_id: flags + .interpretation_run_id + .ok_or(OrchestratorLiveError::InvalidWirePayload)?, + body, + }; + invocation.validate()?; + Ok(invocation) +} + +fn require_loopback_host(host: &str) -> Result { + let addr: SocketAddr = host + .parse() + .map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + if addr.ip().is_loopback() { + Ok(addr) + } else { + Err(OrchestratorLiveError::AuthorizationDenied) + } +} + +/// Render a typed lookup stored-request GET as HTTP/1.1 for a loopback listener. +/// +/// # Errors +/// +/// Returns [`OrchestratorLiveError::AuthorizationDenied`] for a non-loopback +/// host or a credential-bearing header, and +/// [`OrchestratorLiveError::InvalidWirePayload`] when the exchange is not a +/// GET `/v1/interpretation-runs/by-run-id/{id}/request` with an empty body. +pub fn loopback_http1_from_interpretation_run_lookup_stored_request_exchange( + exchange: &InterpretationRunLookupStoredRequestHttpExchange, + loopback_host: &str, +) -> Result { + let _addr = require_loopback_host(loopback_host)?; + let host = loopback_host.trim(); + if exchange.method != "GET" { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + if !exchange.body.is_empty() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let rest = exchange + .target_url + .strip_prefix("https://") + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let path = rest + .find('/') + .map(|index| &rest[index..]) + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let _interpretation_run_id = interpretation_run_lookup_stored_request_path_id(path)?; + for (name, _) in &exchange.headers { + if header_is_credential(name) { + return Err(OrchestratorLiveError::AuthorizationDenied); + } + if name.eq_ignore_ascii_case("idempotency-key") + || name.eq_ignore_ascii_case("tepp-page-limit") + || name.eq_ignore_ascii_case("tepp-page-cursor") + { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + } + let mut request = String::new(); + write!( + request, + "{} {path} HTTP/1.1\r\nHost: {host}\r\n", + exchange.method + ) + .map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + for (name, value) in &exchange.headers { + if name.eq_ignore_ascii_case("host") || name.eq_ignore_ascii_case("content-length") { + continue; + } + write!(request, "{name}: {value}\r\n") + .map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + } + write!(request, "content-length: 0\r\n\r\n") + .map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + Ok(request) +} + +/// Compose one HTTP/1.1 lookup stored-request GET from the typed exchange. +/// +/// # Errors +/// +/// Returns the same fail-closed errors as +/// [`InterpretationRunLookupStoredRequestCliInvocation::validate`]. +pub fn compose_interpretation_run_lookup_stored_request_cli_http( + invocation: &InterpretationRunLookupStoredRequestCliInvocation, +) -> Result { + invocation.validate()?; + let exchange = contextual_orchestrator_interpretation_run_lookup_stored_request_exchange( + &invocation.origin, + &invocation.interpretation_run_id, + )?; + loopback_http1_from_interpretation_run_lookup_stored_request_exchange( + &exchange, + &invocation.host, + ) +} + +/// Dispatch one lookup stored-request CLI invocation against an in-process listener. +/// +/// # Errors +/// +/// Returns fail-closed validation errors before the HTTP handler runs. +pub fn dispatch_interpretation_run_lookup_stored_request_cli( + service: &mut OrchestratorLiveService, + invocation: &InterpretationRunLookupStoredRequestCliInvocation, +) -> Result { + let request = compose_interpretation_run_lookup_stored_request_cli_http(invocation)?; + Ok(service.handle_http_request(&request)) +} + +/// Execute one lookup stored-request CLI invocation over loopback TCP. +/// +/// # Errors +/// +/// Returns fail-closed validation, transport, or response-framing errors. +pub fn execute_interpretation_run_lookup_stored_request_cli( + invocation: &InterpretationRunLookupStoredRequestCliInvocation, +) -> Result { + let addr = require_loopback_host(&invocation.host)?; + let request = compose_interpretation_run_lookup_stored_request_cli_http(invocation)?; + let mut stream = TcpStream::connect(addr).map_err(|error| map_io_error(&error))?; + stream + .set_read_timeout(Some(CLI_IO_TIMEOUT)) + .map_err(|error| map_io_error(&error))?; + stream + .set_write_timeout(Some(CLI_IO_TIMEOUT)) + .map_err(|error| map_io_error(&error))?; + stream + .write_all(request.as_bytes()) + .map_err(|error| map_io_error(&error))?; + stream.flush().map_err(|error| map_io_error(&error))?; + let bytes = read_bounded(&mut stream, MAXIMUM_HTTP_RESPONSE_BYTES)?; + parse_http_response(&bytes) +} + +/// Filter CLI stdout so lookup stored-request GET never prints scientific acceptance. +/// +/// # Errors +/// +/// Returns [`OrchestratorLiveError::InvalidWirePayload`] when a receipt carries +/// metric keys, evidence, causal scores, or +/// `tepp.scientific_acceptance.v1`. +pub fn render_interpretation_run_lookup_stored_request_cli_stdout( + invocation: &InterpretationRunLookupStoredRequestCliInvocation, + response: &OrchestratorLiveResponse, +) -> Result { + invocation.validate()?; + if response.body.is_empty() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + refuse_scientific_acceptance(&response.body)?; + refuse_metrics_on_interpretation_run_stored_request_payload(&response.body)?; + if response.status_code != 200 { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let stored = InterpretationRunRequest::from_json(&response.body)?; + stored.to_json() +} + +fn refuse_scientific_acceptance(body: &str) -> Result<(), OrchestratorLiveError> { + if body.contains(SCIENTIFIC_ACCEPTANCE_SCHEMA) { + Err(OrchestratorLiveError::InvalidWirePayload) + } else { + Ok(()) + } +} + +fn parse_http_response(bytes: &[u8]) -> Result { + let text = std::str::from_utf8(bytes).map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + let (header_block, body) = text + .split_once("\r\n\r\n") + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + if header_block.len() > LIVE_HEADER_BYTE_LIMIT { + return Err(OrchestratorLiveError::LimitExceeded); + } + let mut lines = header_block.split("\r\n"); + let status_line = lines + .next() + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + let mut parts = status_line.split(' '); + if parts.next() != Some("HTTP/1.1") { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + let code = parts + .next() + .ok_or(OrchestratorLiveError::InvalidWirePayload)? + .parse::() + .map_err(|_| OrchestratorLiveError::InvalidWirePayload)?; + let reason_phrase = static_reason(code)?; + let mut content_length = None; + for (index, line) in lines.enumerate() { + if index >= LIVE_HEADER_COUNT_LIMIT { + return Err(OrchestratorLiveError::LimitExceeded); + } + let (name, value) = line + .split_once(':') + .ok_or(OrchestratorLiveError::InvalidWirePayload)?; + if name.eq_ignore_ascii_case("content-length") { + if content_length.is_some() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + content_length = Some( + value + .trim() + .parse::() + .map_err(|_| OrchestratorLiveError::InvalidWirePayload)?, + ); + } + } + let declared = content_length.ok_or(OrchestratorLiveError::InvalidWirePayload)?; + if declared > DEFAULT_INTERPRETATION_BYTE_LIMIT { + return Err(OrchestratorLiveError::LimitExceeded); + } + if declared != body.len() { + return Err(OrchestratorLiveError::InvalidWirePayload); + } + Ok(OrchestratorLiveResponse { + status_code: code, + reason_phrase, + body: body.to_owned(), + }) +} + +fn static_reason(code: u16) -> Result<&'static str, OrchestratorLiveError> { + match code { + 200 => Ok("OK"), + 202 => Ok("Accepted"), + 400 => Ok("Bad Request"), + 403 => Ok("Forbidden"), + 413 => Ok("Payload Too Large"), + 422 => Ok("Unprocessable Entity"), + _ => Err(OrchestratorLiveError::InvalidWirePayload), + } +} + +/// Read stdin leftover bytes on a non-terminal; GET admits empty. +/// +/// # Errors +/// +/// Returns [`OrchestratorLiveError::InvalidWirePayload`] when stdin cannot be +/// read and [`OrchestratorLiveError::LimitExceeded`] when leftover stdin +/// exceeds the interpretation-run wire limit. +pub fn read_interpretation_run_lookup_stored_request_cli_stdin( + stdin_is_terminal: bool, + mut stdin: impl Read, +) -> Result { + if stdin_is_terminal { + Ok(String::new()) + } else { + let bytes = read_bounded(&mut stdin, DEFAULT_INTERPRETATION_BYTE_LIMIT)?; + String::from_utf8(bytes).map_err(|_| OrchestratorLiveError::InvalidWirePayload) + } +} + +fn read_bounded( + reader: &mut impl Read, + maximum_bytes: usize, +) -> Result, OrchestratorLiveError> { + let mut bytes = Vec::new(); + reader + .take((maximum_bytes + 1) as u64) + .read_to_end(&mut bytes) + .map_err(|error| map_io_error(&error))?; + if bytes.len() > maximum_bytes { + return Err(OrchestratorLiveError::LimitExceeded); + } + Ok(bytes) +} + +#[cfg(test)] +mod tests { + use super::{ + InterpretationRunLookupStoredRequestCliInvocation, + InterpretationRunLookupStoredRequestCliVerb, + compose_interpretation_run_lookup_stored_request_cli_http, + loopback_http1_from_interpretation_run_lookup_stored_request_exchange, + read_interpretation_run_lookup_stored_request_cli_stdin, + }; + use crate::interpretation_run_cli::CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE; + use crate::{ + OrchestratorLiveError, + contextual_orchestrator_interpretation_run_lookup_stored_request_exchange, + }; + + const ORIGIN: &str = "https://tepp.example.test"; + + fn get_args() -> [&'static str; 9] { + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--consumer", + CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE, + "--interpretation-run-id", + "orch-run-1", + ] + } + + #[test] + fn from_args_mints_get_and_refuses_fail_closed_inputs() { + assert_eq!( + InterpretationRunLookupStoredRequestCliVerb::parse("get").expect("get"), + InterpretationRunLookupStoredRequestCliVerb::Get + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliVerb::Get.as_str(), + "get" + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliVerb::parse("lookup"), + Err(OrchestratorLiveError::InvalidWirePayload) + ); + let get = InterpretationRunLookupStoredRequestCliInvocation::from_args(get_args(), "") + .expect("get"); + assert_eq!(get.verb, InterpretationRunLookupStoredRequestCliVerb::Get); + let http = compose_interpretation_run_lookup_stored_request_cli_http(&get).expect("http"); + assert!( + http.starts_with("GET /v1/interpretation-runs/by-run-id/orch-run-1/request HTTP/1.1") + ); + assert!(http.contains("tepp-consumer: contextual-orchestrator")); + assert!(http.contains("content-length: 0")); + assert!(!http.contains("idempotency-key:")); + assert!(!http.contains("authorization")); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "8.8.8.8:80", + "--origin", + ORIGIN, + "--interpretation-run-id", + "orch-run-1" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::AuthorizationDenied + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "localhost:18082", + "--origin", + ORIGIN, + "--interpretation-run-id", + "orch-run-1" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + "http://tepp.example.test", + "--interpretation-run-id", + "orch-run-1" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--interpretation-run-id", + "orch-run-1", + "--authorization", + "secret" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::AuthorizationDenied + ); + } + + #[test] + fn from_args_refuses_unpublished_body_and_reserved_prefix() { + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--consumer", + "naruon", + "--interpretation-run-id", + "orch-run-1" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--consumer", + "lineageweave", + "--interpretation-run-id", + "orch-run-1" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args(get_args(), "{}") + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--interpretation-run-id", + "by-run-id" + ], + "" + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + let leftover = read_interpretation_run_lookup_stored_request_cli_stdin( + false, + std::io::Cursor::new(b"leftover"), + ) + .expect("leftover"); + assert_eq!(leftover, "leftover"); + assert!( + read_interpretation_run_lookup_stored_request_cli_stdin(true, std::io::empty()) + .expect("tty") + .is_empty() + ); + let exchange = contextual_orchestrator_interpretation_run_lookup_stored_request_exchange( + ORIGIN, + "orch-run-1", + ) + .expect("exchange"); + let mut posted = exchange.clone(); + posted.method = "POST"; + assert_eq!( + loopback_http1_from_interpretation_run_lookup_stored_request_exchange( + &posted, + "127.0.0.1:18082" + ), + Err(OrchestratorLiveError::InvalidWirePayload) + ); + } +} diff --git a/crates/orchestrator_live/src/lib.rs b/crates/orchestrator_live/src/lib.rs index db5de61e..daa388b6 100644 --- a/crates/orchestrator_live/src/lib.rs +++ b/crates/orchestrator_live/src/lib.rs @@ -16,6 +16,8 @@ //! `tepp-orchestrator-loopback` TCP. //! `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request` //! returns the stored create request of that unique accepted run. +//! Published `tepp-interpretation-run-lookup-request` mints that GET onto +//! spawned `tepp-orchestrator-loopback` TCP. //! Table-access hosts, review/Copilot/GitHub credentials, and //! `COPILOT_GITHUB_TOKEN` fail closed. This crate does not implement TLS //! termination or call a model provider (ADR 0010; ADR 0011). The published @@ -29,6 +31,7 @@ mod interpretation_run_cli; mod interpretation_run_collection_http; mod interpretation_run_lookup_cli; mod interpretation_run_lookup_http; +mod interpretation_run_lookup_stored_request_cli; mod interpretation_run_lookup_stored_request_http; mod interpretation_run_retrieval_http; mod interpretation_run_stored_request_cli; @@ -115,6 +118,22 @@ pub use interpretation_run_lookup_stored_request_http::contextual_orchestrator_i pub use interpretation_run_lookup_stored_request_http::interpretation_run_lookup_stored_request_path_id; /// Whether a path is the lookup stored-request extra-segment resource. pub use interpretation_run_lookup_stored_request_http::is_interpretation_run_lookup_stored_request_path; +/// Loopback lookup stored-request CLI invocation. +pub use interpretation_run_lookup_stored_request_cli::InterpretationRunLookupStoredRequestCliInvocation; +/// Loopback lookup stored-request CLI verb. +pub use interpretation_run_lookup_stored_request_cli::InterpretationRunLookupStoredRequestCliVerb; +/// Compose HTTP/1.1 lookup stored-request GET from a CLI invocation. +pub use interpretation_run_lookup_stored_request_cli::compose_interpretation_run_lookup_stored_request_cli_http; +/// Dispatch a lookup stored-request CLI invocation against an in-process listener. +pub use interpretation_run_lookup_stored_request_cli::dispatch_interpretation_run_lookup_stored_request_cli; +/// Execute a lookup stored-request CLI invocation over loopback TCP. +pub use interpretation_run_lookup_stored_request_cli::execute_interpretation_run_lookup_stored_request_cli; +/// Render a typed lookup stored-request exchange onto a loopback HTTP/1.1 request. +pub use interpretation_run_lookup_stored_request_cli::loopback_http1_from_interpretation_run_lookup_stored_request_exchange; +/// Read leftover stdin for the lookup stored-request CLI. +pub use interpretation_run_lookup_stored_request_cli::read_interpretation_run_lookup_stored_request_cli_stdin; +/// Filter lookup stored-request CLI stdout so the stored create stays hypothetical. +pub use interpretation_run_lookup_stored_request_cli::render_interpretation_run_lookup_stored_request_cli_stdout; /// Loopback lookup CLI invocation. pub use interpretation_run_lookup_cli::InterpretationRunLookupCliInvocation; /// Loopback lookup CLI verb. diff --git a/crates/orchestrator_live/tests/interpretation_run_lookup_stored_request_cli_contract.rs b/crates/orchestrator_live/tests/interpretation_run_lookup_stored_request_cli_contract.rs new file mode 100644 index 00000000..dca419e5 --- /dev/null +++ b/crates/orchestrator_live/tests/interpretation_run_lookup_stored_request_cli_contract.rs @@ -0,0 +1,152 @@ +//! Contract tests for `tepp-interpretation-run-lookup-request get`. + +use orchestrator_live::{ + CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE, INTERPRETATION_RUN_CONTRACT_VERSION, + InterpretationRunAccepted, InterpretationRunCliInvocation, + InterpretationRunLookupStoredRequestCliInvocation, InterpretationRunRequest, OrchestrationMode, + OrchestratorLiveError, OrchestratorLiveResponse, OrchestratorLiveService, + compose_interpretation_run_cli_http, dispatch_interpretation_run_lookup_stored_request_cli, + execute_interpretation_run_lookup_stored_request_cli, + render_interpretation_run_lookup_stored_request_cli_stdout, +}; + +const ORIGIN: &str = "https://tepp.example.test"; + +fn query_body(idem: &str) -> String { + InterpretationRunRequest::new( + INTERPRETATION_RUN_CONTRACT_VERSION, + idem, + "orch-tenant-demo", + "tepp-snapshot-demo-001", + "2026-08-01T00:00:00Z", + OrchestrationMode::Direct, + 2048, + vec!["span-001".into()], + false, + ) + .expect("request") + .to_json() + .expect("json") +} + +fn create_http(idem: &str) -> String { + let invocation = InterpretationRunCliInvocation::from_args( + [ + "create", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--consumer", + CONTEXTUAL_ORCHESTRATOR_CONSUMER_CODE, + ], + query_body(idem), + ) + .expect("create"); + compose_interpretation_run_cli_http(&invocation).expect("post") +} + +fn get_invocation(run_id: &str) -> InterpretationRunLookupStoredRequestCliInvocation { + InterpretationRunLookupStoredRequestCliInvocation::from_args( + [ + "get", + "--host", + "127.0.0.1:18082", + "--origin", + ORIGIN, + "--interpretation-run-id", + run_id, + ], + "", + ) + .expect("get") +} + +#[test] +fn dispatch_retrieves_stored_request_without_scientific_authority() { + let mut service = OrchestratorLiveService::new(); + let created = service.handle_http_request(&create_http("idem-a")); + assert_eq!(created.status_code, 202, "{}", created.body); + let accepted = InterpretationRunAccepted::from_json(&created.body).expect("accepted"); + let run_id = accepted.interpretation_run_id().to_owned(); + let got = dispatch_interpretation_run_lookup_stored_request_cli( + &mut service, + &get_invocation(&run_id), + ) + .expect("get"); + assert_eq!(got.status_code, 200, "{}", got.body); + let stdout = + render_interpretation_run_lookup_stored_request_cli_stdout(&get_invocation(&run_id), &got) + .expect("out"); + assert!(!stdout.contains("tepp.scientific_acceptance.v1")); + assert!(!stdout.contains("rmse")); + assert!(!stdout.contains("causal_score")); + assert!(stdout.contains("\"idempotency_key\":\"idem-a\"")); + assert!(stdout.contains("\"scientific_authority\":false")); + let stored = InterpretationRunRequest::from_json(&stdout).expect("stored"); + assert_eq!(stored.idempotency_key(), "idem-a"); + assert_eq!( + dispatch_interpretation_run_lookup_stored_request_cli( + &mut service, + &get_invocation("missing") + ) + .expect("missing") + .status_code, + 400 + ); +} + +#[test] +fn render_refuses_metrics_schema_and_empty_bodies() { + let get = get_invocation("orch-run-1"); + assert_eq!( + render_interpretation_run_lookup_stored_request_cli_stdout( + &get, + &OrchestratorLiveResponse { + status_code: 200, + reason_phrase: "OK", + body: String::new(), + } + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + render_interpretation_run_lookup_stored_request_cli_stdout( + &get, + &OrchestratorLiveResponse { + status_code: 200, + reason_phrase: "OK", + body: r#"{"contract_version":1,"idempotency_key":"idem-a","tenant_workspace_id":"orch-tenant-demo","snapshot_id":"tepp-snapshot-demo-001","knowledge_cutoff":"2026-08-01T00:00:00Z","orchestration_mode":"direct","compute_budget_tokens":2048,"evidence_span_ids":["span-001"],"scientific_authority":false,"rmse":1.0}"#.into(), + } + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); + assert_eq!( + render_interpretation_run_lookup_stored_request_cli_stdout( + &get, + &OrchestratorLiveResponse { + status_code: 400, + reason_phrase: "Bad Request", + body: r#"{"error_code":"invalid_wire_payload"}"#.into(), + } + ) + .unwrap_err(), + OrchestratorLiveError::InvalidWirePayload + ); +} + +#[test] +fn execute_over_tcp_returns_missing_identity_as_invalid_wire() { + let mut service = OrchestratorLiveService::bind_loopback().expect("bind"); + let addr = service.local_addr().expect("addr"); + let handle = std::thread::spawn(move || { + drop(service.serve_one()); + }); + let mut invocation = get_invocation("orch-run-1"); + invocation.host = addr.to_string(); + let response = execute_interpretation_run_lookup_stored_request_cli(&invocation).expect("tcp"); + assert_eq!(response.status_code, 400, "{}", response.body); + handle.join().expect("join"); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index 006f50f7..77bcbc0c 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -70,6 +70,7 @@ GET /v1/interpretation-runs/by-run-id/{interpretation_run_id} GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request Loopback `tepp-interpretation-run-request get` mints that stored-request GET onto spawned `tepp-orchestrator-loopback` TCP. Loopback `tepp-interpretation-run-lookup lookup` mints that lookup GET onto spawned `tepp-orchestrator-loopback` TCP. +Loopback `tepp-interpretation-run-lookup-request get` mints that lookup stored-request GET onto spawned `tepp-orchestrator-loopback` TCP. POST /v1/analysis-runs POST /v1/temporal-context GET /v1/analysis-runs/{run_id} diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 1fa2a2c7..63cd9506 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -114,6 +114,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | loopback contextual-orchestrator interpretation-run lookup GET | ADR 0095; ADR 0071; ADR 0010/0011; API contract; RFC 9110 | `orchestrator_live` `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}` on `tepp-orchestrator-loopback`; metric-free hypothetical identity from server-assigned run id; empty body; 0 and >1 matches fail closed; `tepp.scientific_acceptance.v1` never appears; does not infer causality; naruon and LineageWeave refused | active-PR | | loopback contextual-orchestrator interpretation-run lookup CLI | ADR 0096; ADR 0095; ADR 0010/0011; API contract; RFC 9110 | published `tepp-interpretation-run-lookup lookup` mints lookup GET onto spawned `tepp-orchestrator-loopback` TCP; metric-free hypothetical identity; empty stdin admitted; naruon and LineageWeave refused | active-PR | | loopback contextual-orchestrator interpretation-run lookup stored-request GET | ADR 0097; ADR 0095; ADR 0085; ADR 0010/0011; API contract; RFC 9110 | `orchestrator_live` `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request` on `tepp-orchestrator-loopback`; stored create request from server-assigned run id; empty body; 0 and >1 matches fail closed; `tepp.scientific_acceptance.v1` never appears; does not infer causality; naruon and LineageWeave refused | active-PR | +| loopback contextual-orchestrator interpretation-run lookup stored-request CLI | ADR 0098; ADR 0097; ADR 0010/0011; API contract; RFC 9110 | published `tepp-interpretation-run-lookup-request get` mints lookup stored-request GET onto spawned `tepp-orchestrator-loopback` TCP; metric-free stored create; empty stdin admitted; naruon and LineageWeave refused | active-PR | | foundation validation / release-readiness ledger | ADR 0014; Test Strategy | PR #24 `docs/validation/temporal-event-foundation.md` on protected main | implemented-main | | scientific claim promotion separated from design/implementation/release | ADR 0014; ADR policy | `validation_core` exact-head promotion gates on this PR; documentation/CI/domain validation remain; full package/image release bundle remaining | partial | | CSAP/SOC 2/ISO/NIST assurance readiness | `docs/COMPLIANCE_READINESS.md`; research register | repository controls + future deployment evidence | accepted-target / deployment-owned | diff --git a/docs/adr/0098-interpretation-run-lookup-stored-request-cli.md b/docs/adr/0098-interpretation-run-lookup-stored-request-cli.md new file mode 100644 index 00000000..4b895b67 --- /dev/null +++ b/docs/adr/0098-interpretation-run-lookup-stored-request-cli.md @@ -0,0 +1,87 @@ +# ADR 0098 — Loopback interpretation-run lookup stored-request CLI + +**Decision status:** Accepted +**Implementation maturity:** active-PR +**Date:** 2026-09-01 +**Supersedes:** None; complements ADR 0097. Does not re-open cancel lineages. +Does not supersede ADR 0014. Unique versus protected main; 0026–0097 occupied +including #469=0097, #468=0096. +**Figma File ID:** N/A — this increment changes a Rust CLI binary and has no +user-interface surface. +**Storybook inventory:** N/A — no reusable web object or interaction changed. + +## Context + +ADR 0097 publishes +`GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request`. +Operators still had no published binary that mints that GET onto spawned +`tepp-orchestrator-loopback` TCP. Reusing `tepp-interpretation-run-lookup` +(#468) or `tepp-interpretation-run-request` (#454) would collide with identity +lookup and client-key stored-request. Temporal-context stored-request is +already #464. Cancel lineages stay closed. + +## Decision + +Publish `tepp-interpretation-run-lookup-request get` which mints +`contextual_orchestrator_interpretation_run_lookup_stored_request_exchange` +onto spawned loopback TCP. Empty stdin is admitted. Nonempty leftover stdin, +public bind, `localhost`, `http` origin, unpublished consumer, credential +flags, reserved `by-run-id` as an identity, slash/NUL, and metric keys fail +closed. Stdout is the stored metric-free create. +`scientific_authority` remains false. `tepp.scientific_acceptance.v1` never +appears. Naruon and LineageWeave are refused. `NaruonLiveService` stays +POST-only. + +## Non-goals + +- Production TLS, public bind, or durable interpretation-run storage. +- Leiden community detection, Driver p.16 std-family restoration, or + Figma/export work (GAP-010). +- Promoting an ADR 0014 scientific claim from CLI success. +- Duplicating lookup stored-request GET (#469), lookup GET/CLI (#467/#468), + stored-request GET/CLI (#453/#454), GET-by-id (#438), retrieval CLI (#439), + collection GET/CLI (#433/#436), create CLI (#425), export lookup (#466), + analysis-run lookup (#380/#401), or cancel lineages (closed). +- Adding GET to `NaruonLiveService`. Opening naruon or LineageWeave on this + orchestrator-owned adapter. + +## Alternatives considered + +1. Re-open cancel CLI — rejected. +2. Reuse `tepp-interpretation-run-lookup` or `tepp-interpretation-run-request` + — rejected; those are ADR 0096 and ADR 0086. +3. Temporal-context stored-request GET — rejected; already #464. +4. Dedicated lookup stored-request binary — accepted. + +## Consequences + +CLI success is not measurement evidence and is not an ADR 0014 claim. +Operators who hold a 202 receipt or log `orch-run-N` can recover the stored +create without a second hop. + +## Failure and recovery + +Non-orchestrator consumers, nonempty leftover stdin, extra segments, slash/NUL, +reserved prefix, missing ids, 0 or >1 match, and metric keys fail closed. + +## Verification + +- `tepp-interpretation-run-lookup-request get` of an accepted `orch-run-N` + prints the matching stored create without RMSE/`tepp.scientific_acceptance.v1`; +- naruon, LineageWeave, public bind, `localhost`, `http` origin, leftover + stdin, reserved prefix, and missing ids fail closed; +- Clippy `-D warnings`, `orchestrator_live` tests, rustdoc, and exact-head + review remain required. + +## Rollback and supersession + +Rollback removes the published binary; lookup stored-request GET remains valid. +A superseding ADR is required to persist the registry, bind a public address, +emit scientific-acceptance, open naruon or LineageWeave, add GET to +`NaruonLiveService`, re-open cancel lineages, or treat CLI success as an +ADR 0014 claim. + +## Related authority + +ADR 0097, ADR 0096, ADR 0085, ADR 0014, RFC 9110 (Fielding, Nottingham, & +Reschke, 2022). diff --git a/docs/adr/README.md b/docs/adr/README.md index 250d25a8..099369bd 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -38,6 +38,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0095](0095-interpretation-run-lookup-get.md) | Loopback interpretation-run lookup GET by server-assigned id | Accepted | active-PR | Complements ADR 0071; `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}` returns the metric-free identity. Unique versus protected main (0026–0094 occupied including #466=0093+0094). Dual identity of GET-by-id. Does not re-open cancel lineages. | | [0096](0096-interpretation-run-lookup-cli.md) | Loopback interpretation-run lookup CLI | Accepted | active-PR | Complements ADR 0095; published `tepp-interpretation-run-lookup lookup`. Unique versus protected main (0026–0095 occupied including #467=0095). Does not re-open cancel lineages. | | [0097](0097-interpretation-run-lookup-stored-request-get.md) | Loopback interpretation-run lookup stored-request GET | Accepted | active-PR | Complements ADR 0095 and ADR 0085; `GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request` returns the stored create. Unique versus protected main (0026–0096 occupied including #468=0096). Does not re-open cancel lineages. | +| [0098](0098-interpretation-run-lookup-stored-request-cli.md) | Loopback interpretation-run lookup stored-request CLI | Accepted | active-PR | Complements ADR 0097; published `tepp-interpretation-run-lookup-request get`. Unique versus protected main (0026–0097 occupied including #469=0097). Does not re-open cancel lineages. | | [0023](0023-lineage-criterion-anchor-contract.md) | TEPP-owned Event Lineage criterion anchor | Accepted | active-PR | PR #237 publishes the strict accepted/rejected artifact and identities; estimator execution remains fail-closed future work. | | [0024](0024-independent-topic-importance-anchor.md) | Posterior topic-context producer contract | Accepted | contract-only active-PR | Strict DTO/schema only; the current estimator does not emit it. fast-mlsirm owns case-deletion influence. | | [0001](0001-rust-first-modular-msa.md) | Rust-first numerical core and CPU `f64` reference | Accepted | partial | ADR 0011 owns cross-service/MSA authority; 0001 retains numerical/backend authority. | diff --git a/docs/research/interpretation-run-lookup-stored-request-cli.md b/docs/research/interpretation-run-lookup-stored-request-cli.md new file mode 100644 index 00000000..a9b01c0f --- /dev/null +++ b/docs/research/interpretation-run-lookup-stored-request-cli.md @@ -0,0 +1,17 @@ +# Interpretation-run lookup stored-request CLI (doctoring) + +`tepp-interpretation-run-lookup-request get` mints +`GET /v1/interpretation-runs/by-run-id/{interpretation_run_id}/request` onto +spawned `tepp-orchestrator-loopback` TCP. HTTP semantics follow RFC 9110 +(Fielding, Nottingham, & Reschke, 2022). Fail-closed unpublished consumers, +leftover stdin, public bind, `localhost`, `http` origin, reserved prefix, +slash/NUL, credential flags, cancel extra-segment, and scientific-authority +promotion are repository contract (ADR 0098; ADR 0014). + +`scientific_authority` remains false. `tepp.scientific_acceptance.v1` never +appears. CLI success is not a scientific claim. + +Does not re-open cancel lineages, GAP-010 Figma/export, persistence, Leiden, +or an ADR 0014 claim-promotion package. Dual identity of stored-request CLI +(`idempotency_key`) versus this lookup (`interpretation_run_id`). Not a +duplicate of lookup stored-request GET (#469) or of `{key}/request` CLI (#454).