Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions CHANGELOG.d/project-history-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- `tepp_api` loopback `tepp-project-history query` mints a typed LineageWeave `POST /v1/project-histories` onto spawned `tepp-loopback` TCP (ADR 0061). Metric-free `temporal_association_only` JSON only. `tepp.scientific_acceptance.v1` never appears. Does not infer causality. Naruon is refused. Not temporal-context CLI, not export CLI, not persistence.
- Fail closed on HTTP field injection, duplicate or transfer-encoded framing, non-2xx stdout, and stdin/response payloads above the existing project-history wire limits.
2 changes: 2 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ 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) |
| Project-history CLI doctoring | [`docs/research/project-history-cli.md`](docs/research/project-history-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) |

| Logical/physical ERD | [`docs/ERD.md`](docs/ERD.md) |
| Security policy | [`SECURITY.md`](SECURITY.md) |
| Threat model | [`docs/THREAT_MODEL.md`](docs/THREAT_MODEL.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"
path = "src/bin/tepp_project_history.rs"
test = false
bench = false

[lints]
workspace = true
33 changes: 33 additions & 0 deletions crates/tepp_api/src/bin/tepp_project_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Operator CLI for loopback `LineageWeave` project-history POST.

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

use tepp_api::{
ApiError, ProjectHistoryCliInvocation, execute_project_history_cli,
read_project_history_cli_stdin, render_project_history_cli_stdout,
};

fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(_) => ExitCode::FAILURE,
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}

fn run() -> Result<(), ApiError> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("query") => run_query(&args),
_ => Err(ApiError::InvalidWirePayload),
}
}

fn run_query(args: &[String]) -> Result<(), ApiError> {
let body = read_project_history_cli_stdin(io::stdin().is_terminal(), io::stdin())?;
let invocation = ProjectHistoryCliInvocation::from_args(args, body)?;
let response = execute_project_history_cli(&invocation)?;
let stdout = render_project_history_cli_stdout(&invocation, &response)?;
println!("{stdout}");
Ok(())
}
23 changes: 22 additions & 1 deletion crates/tepp_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
//! may also request a cutoff-safe project-history projection from explicit
//! source evidence. Naruon owns the current purpose-bound export adapter.
//! Loopback listeners prove the HTTP boundary without claiming production TLS,
//! causality, or completed psychometric model results.
//! causality, or completed psychometric model results. The published
//! `tepp-project-history` CLI mints typed `LineageWeave` project-history POST
//! exchanges onto spawned `tepp-loopback` TCP.

mod analysis_result;
mod analysis_run;
Expand All @@ -28,6 +30,7 @@ mod naruon_http;
mod naruon_live;
mod orchestration;
mod project_history;
mod project_history_cli;
mod project_journey;
mod provider_payload;
mod temporal_context;
Expand Down Expand Up @@ -230,6 +233,24 @@ pub use project_history::ProjectHistoryProjection;
pub use project_history::ProjectHistoryRequest;
/// Build a cutoff-safe project-history projection.
pub use project_history::project_history_projection;
/// Loopback project-history CLI invocation.
pub use project_history_cli::ProjectHistoryCliInvocation;
/// Loopback project-history CLI verb.
pub use project_history_cli::ProjectHistoryCliVerb;
/// Compose HTTP/1.1 project-history POST from a CLI invocation.
pub use project_history_cli::compose_project_history_cli_http;
/// Dispatch a project-history CLI invocation against an in-process listener.
pub use project_history_cli::dispatch_project_history_cli;
/// Execute a project-history CLI invocation over loopback TCP.
pub use project_history_cli::execute_project_history_cli;
/// Render a typed project-history exchange onto a loopback HTTP/1.1 request.
pub use project_history_cli::loopback_http1_from_project_history_exchange;
/// Read leftover stdin for the project-history CLI.
pub use project_history_cli::read_project_history_cli_stdin;
/// Refuse scientific-metric keys on project-history CLI JSON.
pub use project_history_cli::refuse_metrics_on_project_history_cli_payload;
/// Filter project-history CLI stdout so the projection stays metric-free.
pub use project_history_cli::render_project_history_cli_stdout;
/// Maximum posterior Project Journey artifact size.
pub use project_journey::DEFAULT_PROJECT_JOURNEY_BYTE_LIMIT;
/// Exact posterior Project Journey schema identity.
Expand Down
19 changes: 17 additions & 2 deletions crates/tepp_api/src/project_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ impl ProjectHistoryRequest {

fn validate(&self) -> Result<(), ApiError> {
require_contract_version(self.contract_version, PROJECT_HISTORY_CONTRACT_VERSION)?;
validate_bounded_text(&self.idempotency_key, 256)?;
validate_http_field_value(&self.idempotency_key, 256)?;
validate_bounded_text(&self.tenant_workspace_id, 256)?;
validate_bounded_text(&self.project_key, 256)?;
validate_bounded_text(&self.project_name, 512)?;
Expand Down Expand Up @@ -391,6 +391,13 @@ fn validate_bounded_text(value: &str, maximum_bytes: usize) -> Result<(), ApiErr
Ok(())
}

fn validate_http_field_value(value: &str, maximum_bytes: usize) -> Result<(), ApiError> {
validate_bounded_text(value, maximum_bytes)?;
(!value.chars().any(char::is_control))
.then_some(())
.ok_or(ApiError::InvalidWirePayload)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment on lines +396 to +398

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: Whitespace rejection stays scoped

validate_http_field_value only validates idempotency keys. Project names, evidence, and identifiers retain their existing whitespace behavior.

Devin Review

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

}

fn validate_code(value: &str) -> Result<(), ApiError> {
validate_bounded_text(value, 64)?;
if !value
Expand Down Expand Up @@ -597,7 +604,7 @@ mod tests {
use super::{
PROJECT_HISTORY_CONTRACT_VERSION, ProjectHistoryEvent, ProjectHistoryProjection,
ProjectHistoryRequest, build_project_history_exchange, compose_https_target,
project_history_projection, validate_code,
project_history_projection, validate_code, validate_http_field_value,
};
use crate::ApiError;

Expand Down Expand Up @@ -707,6 +714,14 @@ mod tests {
project_history_projection(&excess),
Err(ApiError::LimitExceeded)
);

let mut injected = request_with_single_event();
injected.idempotency_key = "safe\r\nx-api-key: secret".into();
assert_eq!(injected.to_json(), Err(ApiError::InvalidWirePayload));
assert_eq!(
validate_http_field_value("safe\0value", 256),
Err(ApiError::InvalidWirePayload)
);
}

#[test]
Expand Down
Loading
Loading