From 13c9c92edd1ee42a30b4808aff22259a61fbee82 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 20:32:57 +0900 Subject: [PATCH 01/13] feat(api): orchestrator interchange refuses table access and repo tokens CWL contextual-orchestrator interpretation port: credential-free HTTPS POST builder, table-access host refusal, NVIDIA_NIM_API_KEY-only secret name, and explicit denial that orchestrator output is scientific acceptance. No new migration. --- CHANGELOG.md | 1 + DOCUMENTATION.md | 1 + crates/tepp_api/src/lib.rs | 11 ++ crates/tepp_api/src/orchestrator_http.rs | 157 ++++++++++++++++++ .../tests/orchestrator_http_contract.rs | 91 ++++++++++ docs/API_CONTRACT.md | 2 +- docs/TRACEABILITY.md | 2 +- .../0011-standalone-modular-msa-boundary.md | 2 +- ...extual-orchestrator-interpretation-port.md | 6 +- .../research/orchestrator-http-interchange.md | 33 ++++ docs/validation/temporal-event-foundation.md | 1 + 11 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 crates/tepp_api/src/orchestrator_http.rs create mode 100644 crates/tepp_api/tests/orchestrator_http_contract.rs create mode 100644 docs/research/orchestrator-http-interchange.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9abfea7e..6a5dea65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to TEPP are documented here. The format follows Keep a Chang ### Added +- `tepp_api` contextual-orchestrator HTTPS interpretation interchange: credential-free `POST /v1/interpretation-runs`, table-access/non-https refusal, repository-write/review-agent secret refusal, and explicit denial that orchestrator output is scientific acceptance. - `persistence_postgres` typed membership assignment (migration `0006`): `entity_record`, `project_record`, and `text_segment` plus exactly-one observed-unit and target constraints that replace the polymorphic `membership_target_id` stub, with SQL insert/lookup, fail-closed inverted-window and backslash-label refusal, and live proof that one document persists two entity memberships and one project membership. - Actions workflow fleet auditor (`scripts/actions_workflow_fleet.py`): paginated registry inventory bound to the exact default-branch SHA/tree, classification of present/orphan/disabled/GitHub-dynamic identities, and fail-closed orphan disable that confirms GitHub's official `disabled_manually` state. - `persistence_postgres` temporal interval ordering migration (`0005`): multi-word CHECK constraints on `document_record`, `event_instance`, and `membership_assignment` that reject inverted valid/system windows and non-positive document revisions while preserving open-ended NULL upper bounds and equal point bounds; catalog validation and live inverted-window proof. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 230c5abe..a75070c2 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -33,6 +33,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | Hourly NIM product-development operations | [`docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md`](docs/operations/HOURLY_NIM_PRODUCT_DEVELOPMENT.md) | | Actions workflow fleet audit | [`docs/operations/ACTIONS_WORKFLOW_FLEET.md`](docs/operations/ACTIONS_WORKFLOW_FLEET.md) | | Actions fleet research doctoring | [`docs/research/actions-workflow-fleet.md`](docs/research/actions-workflow-fleet.md) | +| Orchestrator HTTPS interchange doctoring | [`docs/research/orchestrator-http-interchange.md`](docs/research/orchestrator-http-interchange.md) | | Hourly NIM OpenCode doctoring | [`docs/doctoring/hourly-nim-opencode-development.md`](docs/doctoring/hourly-nim-opencode-development.md) | | Change history | [`CHANGELOG.md`](CHANGELOG.md) | diff --git a/crates/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs index 3e41af2c..a8f7febe 100644 --- a/crates/tepp_api/src/lib.rs +++ b/crates/tepp_api/src/lib.rs @@ -11,6 +11,7 @@ mod authorization; mod envelope; mod error; mod export; +mod orchestrator_http; mod wire; /// Analysis-run contract version constant. @@ -46,3 +47,13 @@ pub use authorization::ExportAuthorizationRequest; pub use authorization::authorize_export; /// Fail closed when an export decision is denied. pub use authorization::require_export_allowed; +/// Versioned interpretation-run path. +pub use orchestrator_http::ORCHESTRATOR_INTERPRETATION_PATH; +/// Credential-free orchestrator HTTPS exchange. +pub use orchestrator_http::OrchestratorHttpExchange; +/// Build an interpretation request for contextual-orchestrator. +pub use orchestrator_http::orchestrator_interpretation_exchange; +/// Orchestrator output is never scientific acceptance. +pub use orchestrator_http::refuse_orchestrator_as_scientific_acceptance; +/// Refuse repository-write or review-agent secret names. +pub use orchestrator_http::refuse_repository_write_secret; diff --git a/crates/tepp_api/src/orchestrator_http.rs b/crates/tepp_api/src/orchestrator_http.rs new file mode 100644 index 00000000..58d97daf --- /dev/null +++ b/crates/tepp_api/src/orchestrator_http.rs @@ -0,0 +1,157 @@ +//! Versioned HTTPS interchange for the contextual-orchestrator interpretation port. + +use crate::ApiError; +use crate::wire::require_nonempty; + +/// Versioned interpretation-run path on the orchestrator origin. +pub const ORCHESTRATOR_INTERPRETATION_PATH: &str = "/v1/interpretation-runs"; + +/// Fail-closed HTTPS POST that TEPP may send to contextual-orchestrator. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OrchestratorHttpExchange { + method: String, + target_url: String, + headers: Vec<(String, String)>, + body: String, +} + +impl OrchestratorHttpExchange { + /// HTTP method; always `POST`. + #[must_use] + pub fn method(&self) -> &str { + &self.method + } + + /// Absolute `https` target URL. + #[must_use] + pub fn target_url(&self) -> &str { + &self.target_url + } + + /// Request headers; never includes credentials or review-agent tokens. + #[must_use] + pub fn headers(&self) -> &[(String, String)] { + &self.headers + } + + /// JSON body; never includes repository-write secrets. + #[must_use] + pub fn body(&self) -> &str { + &self.body + } +} + +/// Build a credential-free interpretation request for contextual-orchestrator. +/// +/// The origin is a DNS host only. Table-access hosts and non-`https` schemes +/// fail closed. The orchestrator does not become scientific authority. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for empty idempotency or body. +/// Returns [`ApiError::AuthorizationDenied`] for a hostile or non-`https` host. +pub fn orchestrator_interpretation_exchange( + origin_host: &str, + idempotency_key: &str, + body: &str, +) -> Result { + require_nonempty(idempotency_key)?; + require_nonempty(body)?; + require_safe_https_host(origin_host)?; + if body.contains("COPILOT_GITHUB_TOKEN") { + return Err(ApiError::AuthorizationDenied); + } + Ok(OrchestratorHttpExchange { + method: "POST".into(), + target_url: format!("https://{origin_host}{ORCHESTRATOR_INTERPRETATION_PATH}"), + headers: vec![ + ("content-type".into(), "application/json".into()), + ("tepp-consumer".into(), "contextual-orchestrator".into()), + ("tepp-contract-version".into(), "1".into()), + ("idempotency-key".into(), idempotency_key.into()), + ], + body: body.into(), + }) +} + +/// Orchestrator output never replaces deterministic/statistical acceptance. +/// +/// # Errors +/// +/// Always returns [`ApiError::AuthorizationDenied`]. +pub fn refuse_orchestrator_as_scientific_acceptance() -> Result<(), ApiError> { + Err(ApiError::AuthorizationDenied) +} + +/// Refuse repository-write or review-agent secret names on this port. +/// +/// `NVIDIA_NIM_API_KEY` is the only allowed model-credential name. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for an empty name and +/// [`ApiError::AuthorizationDenied`] for Copilot, GitHub, or review-agent names. +pub fn refuse_repository_write_secret(secret_name: &str) -> Result<(), ApiError> { + require_nonempty(secret_name)?; + let folded: String = secret_name + .chars() + .filter(char::is_ascii_alphanumeric) + .flat_map(char::to_lowercase) + .collect(); + if folded == "nvidianimapikey" { + return Ok(()); + } + if folded.contains("copilot") || folded.contains("github") || folded.contains("reviewagent") { + return Err(ApiError::AuthorizationDenied); + } + Err(ApiError::AuthorizationDenied) +} + +fn require_safe_https_host(host: &str) -> Result<(), ApiError> { + if host.is_empty() + || host.chars().any(|ch| { + ch.is_control() + || ch.is_whitespace() + || matches!(ch, '@' | '/' | '?' | '#' | '\'' | ';' | '\\') + }) + { + return Err(ApiError::AuthorizationDenied); + } + let lowered = host.to_ascii_lowercase(); + if ["postgres", "jdbc", "sql", "tables"] + .iter() + .any(|needle| lowered.contains(needle)) + { + return Err(ApiError::AuthorizationDenied); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + OrchestratorHttpExchange, refuse_repository_write_secret, require_safe_https_host, + }; + use crate::ApiError; + + #[test] + fn unknown_secret_names_and_accessors_are_covered() { + assert_eq!( + refuse_repository_write_secret("AWS_SECRET"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + refuse_repository_write_secret(""), + Err(ApiError::InvalidWirePayload) + ); + assert_eq!(require_safe_https_host("ok.example"), Ok(())); + let exchange = OrchestratorHttpExchange { + method: "POST".into(), + target_url: "https://ok.example/v1/interpretation-runs".into(), + headers: Vec::new(), + body: "{}".into(), + }; + assert_eq!(exchange.method(), "POST"); + assert!(exchange.headers().is_empty()); + } +} diff --git a/crates/tepp_api/tests/orchestrator_http_contract.rs b/crates/tepp_api/tests/orchestrator_http_contract.rs new file mode 100644 index 00000000..f8de2982 --- /dev/null +++ b/crates/tepp_api/tests/orchestrator_http_contract.rs @@ -0,0 +1,91 @@ +//! contextual-orchestrator interchange refuses table access and repo-write tokens. + +use tepp_api::{ + ApiError, ORCHESTRATOR_INTERPRETATION_PATH, orchestrator_interpretation_exchange, + refuse_orchestrator_as_scientific_acceptance, refuse_repository_write_secret, +}; + +#[test] +fn https_post_has_no_credentials_and_does_not_own_science() { + let exchange = orchestrator_interpretation_exchange( + "orchestrator.example", + "idem-unitize-1", + r#"{"task":"semantic_unitization","snapshot_id":"snap-1"}"#, + ) + .expect("https"); + assert_eq!(exchange.method(), "POST"); + assert_eq!( + exchange.target_url(), + format!("https://orchestrator.example{ORCHESTRATOR_INTERPRETATION_PATH}") + ); + assert!(ORCHESTRATOR_INTERPRETATION_PATH.contains("interpretation")); + let header_names: Vec<_> = exchange + .headers() + .iter() + .map(|(name, _)| name.as_str()) + .collect(); + assert!(header_names.contains(&"content-type")); + assert!(header_names.contains(&"tepp-consumer")); + assert!( + !header_names + .iter() + .any(|name| name.contains("authorization") + || name.contains("cookie") + || name.contains("token") + || name.contains("copilot") + || name.contains("github")) + ); + assert!(!exchange.body().contains("COPILOT_GITHUB_TOKEN")); + assert_eq!( + refuse_orchestrator_as_scientific_acceptance(), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn table_access_and_non_https_origins_fail_closed() { + for host in [ + "", + "bad host", + "h@x", + "h/sql", + "h?x", + "h#x", + "postgres.db", + "jdbc.db", + ] { + assert_eq!( + orchestrator_interpretation_exchange(host, "idem-1", "{}"), + Err(ApiError::AuthorizationDenied) + ); + } + assert_eq!( + orchestrator_interpretation_exchange("tables.example", "idem-1", "{}"), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn repository_write_and_review_agent_secrets_are_refused() { + assert_eq!( + refuse_repository_write_secret("COPILOT_GITHUB_TOKEN"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + refuse_repository_write_secret("review-agent-github-token"), + Err(ApiError::AuthorizationDenied) + ); + refuse_repository_write_secret("NVIDIA_NIM_API_KEY").expect("nim allowed as name"); + assert_eq!( + orchestrator_interpretation_exchange("", "idem-1", "{}"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + orchestrator_interpretation_exchange("ok.example", "", "{}"), + Err(ApiError::InvalidWirePayload) + ); + assert_eq!( + orchestrator_interpretation_exchange("ok.example", "idem-1", ""), + Err(ApiError::InvalidWirePayload) + ); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index d1b12be8..a603d33f 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -114,7 +114,7 @@ TEPP owns its application/API state, authorized evidence, model runs, and artifa ### contextual-orchestrator -TEPP may call a provider-neutral interpretation/orchestration port for semantic unitization, blinded model review, and evidence-bounded interpretation. The orchestrator does not own TEPP's statistical truth, source evidence, model registry, merge/release authority, or scientific acceptance. Detailed port boundary and credential separation are recorded in [`docs/connectors/contextual-orchestrator-interpretation-port.md`](connectors/contextual-orchestrator-interpretation-port.md). +TEPP may call a provider-neutral interpretation/orchestration port for semantic unitization, blinded model review, and evidence-bounded interpretation. Callers must build the request through `tepp_api::orchestrator_interpretation_exchange`. The orchestrator does not own TEPP's statistical truth, source evidence, model registry, merge/release authority, or scientific acceptance. Detailed port boundary and credential separation are recorded in [`docs/connectors/contextual-orchestrator-interpretation-port.md`](connectors/contextual-orchestrator-interpretation-port.md). ### organization `.github` diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 051062ea..0c79d131 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -37,7 +37,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | tenant/purpose/role/lifetime access and identity separation | ADR 0009; Threat Model | future service/persistence boundaries | accepted-target | | standalone + modular CWL MSA / no cross-service DB coupling | ADR 0011; `docs/API_CONTRACT.md` | current standalone crates; future service ports | partial | | naruon modular artifact consumer boundary | ADR 0011/0012; API contract | `docs/connectors/naruon-artifact-consumer.md` + PR #22 versioned consumer contract on protected main; HTTP service remaining | partial | -| contextual-orchestrator interpretation port boundary | ADR 0010/0011; LLM orchestration | `docs/connectors/contextual-orchestrator-interpretation-port.md`; live port remaining | partial | +| contextual-orchestrator interpretation port boundary | ADR 0010/0011; LLM orchestration | `tepp_api` HTTPS interpretation interchange on the active PR; live HTTP server remaining | partial | | Actions registry identities bound to protected-main tree (orphan disable) | Operability; GitHub Actions REST | `scripts/actions_workflow_fleet.py` + issue #20 tests/doctoring; live disable remains operator-authorized | active-PR | | autonomous model proposal separated from verification/publication/review/merge | ADR 0015 | future safe OpenCode/NVIDIA autonomous-development workflow | accepted-target | | contextual-orchestrator execution boundary | ADR 0010/0011 | provider-neutral orchestration port; TEPP retains scientific authority | accepted-target | diff --git a/docs/adr/0011-standalone-modular-msa-boundary.md b/docs/adr/0011-standalone-modular-msa-boundary.md index b83576e9..2f597764 100644 --- a/docs/adr/0011-standalone-modular-msa-boundary.md +++ b/docs/adr/0011-standalone-modular-msa-boundary.md @@ -1,7 +1,7 @@ # ADR 0011 — Standalone operation and modular CWL MSA boundary **Decision status:** Accepted -**Implementation maturity:** partial — Rust crates are independently usable; production service/API/persistence integrations remain accepted-target +**Implementation maturity:** partial — Rust crates are independently usable; contextual-orchestrator HTTPS interpretation interchange is on the active PR; live HTTP servers and remaining persistence integrations remain accepted-target **Date:** 2026-08-10 **Supersedes:** The broad cross-service ownership wording in ADR 0001. ADR 0001 remains authoritative for Rust-first numerical architecture. diff --git a/docs/connectors/contextual-orchestrator-interpretation-port.md b/docs/connectors/contextual-orchestrator-interpretation-port.md index 62889fc2..d3bafe79 100644 --- a/docs/connectors/contextual-orchestrator-interpretation-port.md +++ b/docs/connectors/contextual-orchestrator-interpretation-port.md @@ -1,7 +1,7 @@ # contextual-orchestrator interpretation port for TEPP **Status:** Accepted-target modular integration contract -**Last reviewed:** 2026-08-12 +**Last reviewed:** 2026-08-13 ## Boundary @@ -29,6 +29,10 @@ These allocations are guided by Fugu, Conductor, and TRINITY research cited in ` - `COPILOT_GITHUB_TOKEN` is prohibited. - Existing independent review-agent credentials must not be repurposed for product development or interpretation traffic. +## Wire interchange + +`tepp_api::orchestrator_interpretation_exchange` builds a credential-free `POST https:///v1/interpretation-runs`. The host must be a DNS name; `postgres`, `jdbc`, `sql`, and `tables` hosts are refused. `refuse_repository_write_secret` accepts only `NVIDIA_NIM_API_KEY` as a model-credential name. `refuse_orchestrator_as_scientific_acceptance` always denies treating orchestrator output as statistical truth. + ## Failure modes - missing provider key → fail closed without fallback to repository-write tokens; diff --git a/docs/research/orchestrator-http-interchange.md b/docs/research/orchestrator-http-interchange.md new file mode 100644 index 00000000..71391fcb --- /dev/null +++ b/docs/research/orchestrator-http-interchange.md @@ -0,0 +1,33 @@ +# contextual-orchestrator HTTPS interchange + +## Scope + +This note doctors the `tepp_api` interpretation-port builder for `contextual-orchestrator`: + +1. requests are `POST https:///v1/interpretation-runs` with no credentials; +2. hosts that look like table or SQL access fail closed; +3. `COPILOT_GITHUB_TOKEN` and other GitHub/review-agent secret names are refused; +4. `NVIDIA_NIM_API_KEY` is the only allowed model-credential name; +5. orchestrator output cannot become scientific acceptance. + +This is a versioned request builder, not a live HTTP server. No database migration is allocated. + +## Authoritative sources + +Fielding, R. T., & Reschke, J. (Eds.). (2014). *Hypertext Transfer Protocol (HTTP/1.1): Semantics and content* (RFC 7231). IETF. https://doi.org/10.17487/RFC7231 + +ISO/IEC. (2019). *ISO/IEC 27701:2019 Security techniques — Extension to ISO/IEC 27001 and ISO/IEC 27002 for privacy information management — Requirements and guidelines*. International Organization for Standardization. + +National Institute of Standards and Technology. (2020). *NIST Privacy Framework: A tool for improving privacy through enterprise risk management* (Version 1.0). U.S. Department of Commerce. https://doi.org/10.6028/NIST.CSWP.01162020 + +## Application + +RFC 7231 supplies POST semantics for an interpretation-run resource (Fielding & Reschke, 2014). ISO/IEC 27701 and the NIST Privacy Framework require purpose-bound, minimized disclosure and forbid using review-agent credentials as a product-development path (ISO/IEC, 2019; National Institute of Standards and Technology, 2020). TEPP therefore keeps scientific authority inside deterministic gates and treats the orchestrator as an untrusted interpreter. + +## Verification + +- a valid host yields `POST` without authorization/cookie/token/copilot/github headers; +- `postgres`, `jdbc`, `sql`, `tables`, empty, and punctured hosts are denied; +- `COPILOT_GITHUB_TOKEN` and `review-agent-github-token` are denied; +- `NVIDIA_NIM_API_KEY` is an allowed secret name; +- `refuse_orchestrator_as_scientific_acceptance` always denies. diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 984d329c..384a23b3 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -24,6 +24,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Recovery metrics | `validation_core` | implemented-main | — | RMSE/bias/coverage/MC gates | Task 11 / PR #19 | | Versioned API/export contracts | `tepp_api` | implemented-main | — | unknown-field/version/limit tests | Task 12 / PR #21; HTTP service remaining | | CWL modular connectors | `docs/connectors/*` | implemented-main | — | contract docs + examples | PR #22; live HTTP ports remaining | +| contextual-orchestrator HTTPS interchange | `tepp_api` | active-PR | interpretation POST builder | table-access + secret refusal | ADR 0011; `docs/research/orchestrator-http-interchange.md` | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 5c365f917610e5b3bc0cfd1fae09785e63cbe3aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:33:30 +0900 Subject: [PATCH 02/13] test(api): bound orchestrator JSON and secret channels --- .../orchestrator_http_security_contract.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 crates/tepp_api/tests/orchestrator_http_security_contract.rs diff --git a/crates/tepp_api/tests/orchestrator_http_security_contract.rs b/crates/tepp_api/tests/orchestrator_http_security_contract.rs new file mode 100644 index 00000000..fdd5dac1 --- /dev/null +++ b/crates/tepp_api/tests/orchestrator_http_security_contract.rs @@ -0,0 +1,61 @@ +//! contextual-orchestrator HTTP bodies must be bounded JSON without secret-name channels. + +use tepp_api::{ + ApiError, MAX_ORCHESTRATOR_BODY_BYTES, MAX_ORCHESTRATOR_IDEMPOTENCY_KEY_BYTES, + orchestrator_interpretation_exchange, +}; + +#[test] +fn request_body_must_be_a_bounded_json_object() { + for invalid in ["not-json", "[]", "null", "\"text\""] { + assert_eq!( + orchestrator_interpretation_exchange("orchestrator.example", "idem-1", invalid), + Err(ApiError::InvalidWirePayload) + ); + } + + let oversized = format!(r#"{{"payload":"{}"}}"#, "x".repeat(MAX_ORCHESTRATOR_BODY_BYTES)); + assert_eq!( + orchestrator_interpretation_exchange("orchestrator.example", "idem-1", &oversized), + Err(ApiError::LimitExceeded) + ); +} + +#[test] +fn normalized_nested_secret_names_fail_closed() { + for body in [ + r#"{"copilot_github_token":"x"}"#, + r#"{"nested":{"review-agent-github-token":"x"}}"#, + r#"{"credential_name":"GITHUB_TOKEN"}"#, + r#"{"credential_name":"copilot github token"}"#, + ] { + assert_eq!( + orchestrator_interpretation_exchange("orchestrator.example", "idem-1", body), + Err(ApiError::AuthorizationDenied) + ); + } + + orchestrator_interpretation_exchange( + "orchestrator.example", + "idem-1", + r#"{"credential_name":"NVIDIA_NIM_API_KEY","task":"interpret"}"#, + ) + .expect("the only allowed model credential name remains admissible"); +} + +#[test] +fn idempotency_key_is_bounded_and_header_safe() { + let oversized = "i".repeat(MAX_ORCHESTRATOR_IDEMPOTENCY_KEY_BYTES + 1); + assert_eq!( + orchestrator_interpretation_exchange("orchestrator.example", &oversized, "{}"), + Err(ApiError::LimitExceeded) + ); + assert_eq!( + orchestrator_interpretation_exchange( + "orchestrator.example", + "idem\r\ninjected: true", + "{}", + ), + Err(ApiError::InvalidWirePayload) + ); +} From 04390ea8584045720ac4829c9869304cbb4f2938 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:34:43 +0900 Subject: [PATCH 03/13] fix(api): bound orchestrator JSON and secret channels --- crates/tepp_api/src/orchestrator_http.rs | 102 +++++++++++++++++++---- 1 file changed, 85 insertions(+), 17 deletions(-) diff --git a/crates/tepp_api/src/orchestrator_http.rs b/crates/tepp_api/src/orchestrator_http.rs index 58d97daf..d684e201 100644 --- a/crates/tepp_api/src/orchestrator_http.rs +++ b/crates/tepp_api/src/orchestrator_http.rs @@ -2,9 +2,14 @@ use crate::ApiError; use crate::wire::require_nonempty; +use serde_json::Value; /// Versioned interpretation-run path on the orchestrator origin. pub const ORCHESTRATOR_INTERPRETATION_PATH: &str = "/v1/interpretation-runs"; +/// Maximum UTF-8 bytes accepted in one orchestrator JSON body. +pub const MAX_ORCHESTRATOR_BODY_BYTES: usize = 1_048_576; +/// Maximum UTF-8 bytes accepted in one idempotency-key header value. +pub const MAX_ORCHESTRATOR_IDEMPOTENCY_KEY_BYTES: usize = 256; /// Fail-closed HTTPS POST that TEPP may send to contextual-orchestrator. #[derive(Clone, Debug, Eq, PartialEq)] @@ -34,7 +39,7 @@ impl OrchestratorHttpExchange { &self.headers } - /// JSON body; never includes repository-write secrets. + /// JSON body; never includes repository-write secret names. #[must_use] pub fn body(&self) -> &str { &self.body @@ -44,23 +49,25 @@ impl OrchestratorHttpExchange { /// Build a credential-free interpretation request for contextual-orchestrator. /// /// The origin is a DNS host only. Table-access hosts and non-`https` schemes -/// fail closed. The orchestrator does not become scientific authority. +/// fail closed. The body must be a bounded JSON object and cannot carry known +/// repository-write or review-agent secret names. The orchestrator does not +/// become scientific authority. /// /// # Errors /// -/// Returns [`ApiError::InvalidWirePayload`] for empty idempotency or body. -/// Returns [`ApiError::AuthorizationDenied`] for a hostile or non-`https` host. +/// Returns [`ApiError::InvalidWirePayload`] for an empty, unsafe, or oversized +/// idempotency key, malformed/non-object JSON, or an empty body. Returns +/// [`ApiError::LimitExceeded`] for body or idempotency-key size limits. Returns +/// [`ApiError::AuthorizationDenied`] for a hostile host or forbidden secret-name +/// channel. pub fn orchestrator_interpretation_exchange( origin_host: &str, idempotency_key: &str, body: &str, ) -> Result { - require_nonempty(idempotency_key)?; - require_nonempty(body)?; + require_safe_idempotency_key(idempotency_key)?; + require_bounded_json_object(body)?; require_safe_https_host(origin_host)?; - if body.contains("COPILOT_GITHUB_TOKEN") { - return Err(ApiError::AuthorizationDenied); - } Ok(OrchestratorHttpExchange { method: "POST".into(), target_url: format!("https://{origin_host}{ORCHESTRATOR_INTERPRETATION_PATH}"), @@ -90,25 +97,86 @@ pub fn refuse_orchestrator_as_scientific_acceptance() -> Result<(), ApiError> { /// # Errors /// /// Returns [`ApiError::InvalidWirePayload`] for an empty name and -/// [`ApiError::AuthorizationDenied`] for Copilot, GitHub, or review-agent names. +/// [`ApiError::AuthorizationDenied`] for every name except the NVIDIA NIM key. pub fn refuse_repository_write_secret(secret_name: &str) -> Result<(), ApiError> { require_nonempty(secret_name)?; - let folded: String = secret_name + let folded = normalize_secret_name(secret_name); + if folded == "nvidianimapikey" { + Ok(()) + } else { + Err(ApiError::AuthorizationDenied) + } +} + +fn require_safe_idempotency_key(value: &str) -> Result<(), ApiError> { + require_nonempty(value)?; + if value.len() > MAX_ORCHESTRATOR_IDEMPOTENCY_KEY_BYTES { + return Err(ApiError::LimitExceeded); + } + if value.chars().any(char::is_whitespace) || value.chars().any(char::is_control) { + return Err(ApiError::InvalidWirePayload); + } + Ok(()) +} + +fn require_bounded_json_object(body: &str) -> Result<(), ApiError> { + require_nonempty(body)?; + if body.len() > MAX_ORCHESTRATOR_BODY_BYTES { + return Err(ApiError::LimitExceeded); + } + let value: Value = serde_json::from_str(body).map_err(|_| ApiError::InvalidWirePayload)?; + if !value.is_object() { + return Err(ApiError::InvalidWirePayload); + } + + let mut pending = vec![&value]; + while let Some(current) = pending.pop() { + match current { + Value::Object(entries) => { + for (key, nested) in entries { + if is_forbidden_secret_name(key) { + return Err(ApiError::AuthorizationDenied); + } + pending.push(nested); + } + } + Value::Array(entries) => pending.extend(entries), + Value::String(text) => { + if is_forbidden_secret_name(text) { + return Err(ApiError::AuthorizationDenied); + } + } + Value::Null | Value::Bool(_) | Value::Number(_) => {} + } + } + Ok(()) +} + +fn normalize_secret_name(value: &str) -> String { + value .chars() .filter(char::is_ascii_alphanumeric) .flat_map(char::to_lowercase) - .collect(); + .collect() +} + +fn is_forbidden_secret_name(value: &str) -> bool { + let folded = normalize_secret_name(value); if folded == "nvidianimapikey" { - return Ok(()); + return false; } - if folded.contains("copilot") || folded.contains("github") || folded.contains("reviewagent") { - return Err(ApiError::AuthorizationDenied); - } - Err(ApiError::AuthorizationDenied) + folded == "githubtoken" + || folded == "copilotgithubtoken" + || folded == "reviewagentgithubtoken" + || folded == "opencodegithubtoken" + || (folded.contains("copilot") && folded.contains("token")) + || (folded.contains("reviewagent") && folded.contains("token")) + || (folded.starts_with("github") && folded.ends_with("token")) } fn require_safe_https_host(host: &str) -> Result<(), ApiError> { if host.is_empty() + || host.len() > 253 || host.chars().any(|ch| { ch.is_control() || ch.is_whitespace() From d2fbcb3948a44c34dac086f4833ee35a91a79a16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:35:02 +0900 Subject: [PATCH 04/13] feat(api): export orchestrator resource limits --- crates/tepp_api/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs index a8f7febe..75c34652 100644 --- a/crates/tepp_api/src/lib.rs +++ b/crates/tepp_api/src/lib.rs @@ -47,6 +47,10 @@ pub use authorization::ExportAuthorizationRequest; pub use authorization::authorize_export; /// Fail closed when an export decision is denied. pub use authorization::require_export_allowed; +/// Maximum orchestrator JSON body size. +pub use orchestrator_http::MAX_ORCHESTRATOR_BODY_BYTES; +/// Maximum orchestrator idempotency-key size. +pub use orchestrator_http::MAX_ORCHESTRATOR_IDEMPOTENCY_KEY_BYTES; /// Versioned interpretation-run path. pub use orchestrator_http::ORCHESTRATOR_INTERPRETATION_PATH; /// Credential-free orchestrator HTTPS exchange. From 232c3fca67967ddbc651f4ee91c1bb6775dfeb42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:35:42 +0900 Subject: [PATCH 05/13] ci: verify PR 52 orchestrator interchange security --- .../repair-pr52-interchange-security.yml | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/repair-pr52-interchange-security.yml diff --git a/.github/workflows/repair-pr52-interchange-security.yml b/.github/workflows/repair-pr52-interchange-security.yml new file mode 100644 index 00000000..6bb7cd6f --- /dev/null +++ b/.github/workflows/repair-pr52-interchange-security.yml @@ -0,0 +1,78 @@ +name: Repair PR 52 orchestrator interchange security + +on: + pull_request: + types: + - synchronize + - reopened + - ready_for_review + +permissions: + contents: read + +concurrency: + group: repair-tepp-pr-52-interchange-security + cancel-in-progress: true + +jobs: + repair: + if: >- + github.event.pull_request.number == 52 && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'agent/orchestrator-http-interchange' + runs-on: ubuntu-latest + timeout-minutes: 40 + permissions: + contents: write + steps: + - name: Checkout exact PR branch + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + ref: agent/orchestrator-http-interchange + fetch-depth: 0 + persist-credentials: true + + - name: Install pinned Rust toolchain + run: rustup toolchain install 1.97.1 --profile minimal --component clippy --component rustfmt + + - name: Prove the pre-implementation resource contract was RED + run: | + git worktree add "$RUNNER_TEMP/tepp-red" 5c365f917610e5b3bc0cfd1fae09785e63cbe3aa + set +e + output=$(cd "$RUNNER_TEMP/tepp-red" && cargo +1.97.1 test -p tepp_api --test orchestrator_http_security_contract 2>&1) + status=$? + set -e + printf '%s\n' "$output" + git worktree remove --force "$RUNNER_TEMP/tepp-red" + if [ "$status" -eq 0 ]; then + echo "Expected the pre-implementation orchestrator resource contract to fail" >&2 + exit 1 + fi + grep -E "MAX_ORCHESTRATOR|unresolved import" <<<"$output" + + - name: Merge current protected main without discarding feature behavior + run: | + git fetch origin main + git merge --no-edit -X ours origin/main + + - name: Verify focused and workspace contracts + run: | + cargo +1.97.1 fmt --all --check + cargo +1.97.1 test -p tepp_api --all-features + cargo +1.97.1 clippy -p tepp_api --all-targets --all-features -- -D warnings + cargo +1.97.1 test --workspace --all-features + python3 scripts/check_workspace_contract.py + python3 scripts/check_docstrings.py + python3 scripts/validate_documentation.py + + - name: Commit verified merge and remove one-shot workflow + run: | + rm -f .github/workflows/repair-pr52-interchange-security.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git diff --cached --check + if ! git diff --cached --quiet; then + git commit -m "fix(api): bound orchestrator interchange trust channels" + fi + git push origin HEAD:agent/orchestrator-http-interchange From bf779e320d37b9af2d9f2d1731f60e8d3099cfea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:45:51 +0900 Subject: [PATCH 06/13] ci: retrigger PR 52 interchange repair --- .github/workflows/repair-pr52-interchange-security.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/repair-pr52-interchange-security.yml b/.github/workflows/repair-pr52-interchange-security.yml index 6bb7cd6f..1f6925b4 100644 --- a/.github/workflows/repair-pr52-interchange-security.yml +++ b/.github/workflows/repair-pr52-interchange-security.yml @@ -76,3 +76,5 @@ jobs: git commit -m "fix(api): bound orchestrator interchange trust channels" fi git push origin HEAD:agent/orchestrator-http-interchange + +# Synchronize exact-head repair after ready-for-review transition. From cd1251e9d760692454c81507a428b5fbdc6925a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 19:05:02 +0900 Subject: [PATCH 07/13] ci: configure merge identity before PR 52 repair --- .github/workflows/repair-pr52-interchange-security.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/repair-pr52-interchange-security.yml b/.github/workflows/repair-pr52-interchange-security.yml index 1f6925b4..8298c736 100644 --- a/.github/workflows/repair-pr52-interchange-security.yml +++ b/.github/workflows/repair-pr52-interchange-security.yml @@ -52,6 +52,8 @@ jobs: - name: Merge current protected main without discarding feature behavior run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git fetch origin main git merge --no-edit -X ours origin/main @@ -68,8 +70,6 @@ jobs: - name: Commit verified merge and remove one-shot workflow run: | rm -f .github/workflows/repair-pr52-interchange-security.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -A git diff --cached --check if ! git diff --cached --quiet; then From 1333cd57d38df310f32709605b7917f5e5a80991 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:53:17 +0900 Subject: [PATCH 08/13] style(api): format orchestrator security contract --- crates/tepp_api/tests/orchestrator_http_security_contract.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/tepp_api/tests/orchestrator_http_security_contract.rs b/crates/tepp_api/tests/orchestrator_http_security_contract.rs index fdd5dac1..b525fb69 100644 --- a/crates/tepp_api/tests/orchestrator_http_security_contract.rs +++ b/crates/tepp_api/tests/orchestrator_http_security_contract.rs @@ -14,7 +14,10 @@ fn request_body_must_be_a_bounded_json_object() { ); } - let oversized = format!(r#"{{"payload":"{}"}}"#, "x".repeat(MAX_ORCHESTRATOR_BODY_BYTES)); + let oversized = format!( + r#"{{"payload":"{}"}}"#, + "x".repeat(MAX_ORCHESTRATOR_BODY_BYTES) + ); assert_eq!( orchestrator_interpretation_exchange("orchestrator.example", "idem-1", &oversized), Err(ApiError::LimitExceeded) From ab09e81b3615f57421dd777cb55d619d136b1308 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:21:34 +0900 Subject: [PATCH 09/13] chore: remove completed membership repair workflow --- ...repair-pr54-distinct-membership-groups.yml | 75 ------------------- ...03-relational-event-multiple-membership.md | 2 +- 2 files changed, 1 insertion(+), 76 deletions(-) delete mode 100644 .github/workflows/repair-pr54-distinct-membership-groups.yml diff --git a/.github/workflows/repair-pr54-distinct-membership-groups.yml b/.github/workflows/repair-pr54-distinct-membership-groups.yml deleted file mode 100644 index 5c086666..00000000 --- a/.github/workflows/repair-pr54-distinct-membership-groups.yml +++ /dev/null @@ -1,75 +0,0 @@ -name: Repair PR 54 distinct membership groups - -on: - pull_request: - types: [synchronize, reopened, ready_for_review] - -permissions: - contents: read - -concurrency: - group: repair-tepp-pr-54-distinct-membership-groups - cancel-in-progress: true - -jobs: - repair: - if: >- - github.event.pull_request.number == 54 && - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.head.ref == 'agent/membership-estimation-rows' - runs-on: ubuntu-latest - timeout-minutes: 40 - permissions: - contents: write - steps: - - name: Checkout exact PR branch - uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 - with: - ref: agent/membership-estimation-rows - fetch-depth: 0 - persist-credentials: true - - - name: Install pinned Rust toolchain - run: rustup toolchain install 1.97.1 --profile minimal --component clippy --component rustfmt - - - name: Prove the pre-implementation group contract was RED - run: | - git worktree add "$RUNNER_TEMP/tepp-red" 6d2c710e977bf087b2452a6785a6483e20bc9e68 - set +e - output=$(cd "$RUNNER_TEMP/tepp-red" && cargo +1.97.1 test -p membership_core --test atomistic_collapse_structure_contract 2>&1) - status=$? - set -e - printf '%s\n' "$output" - git worktree remove --force "$RUNNER_TEMP/tepp-red" - if [ "$status" -eq 0 ]; then - echo "Expected duplicate-group rows to expose the old collapse guard" >&2 - exit 1 - fi - grep -E "duplicate_group_rows|AtomisticCollapseRefused" <<<"$output" - - - name: Merge current protected main without discarding feature behavior - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git fetch origin main - git merge --no-edit -X ours origin/main - - - name: Verify focused and workspace contracts - run: | - cargo +1.97.1 fmt --all --check - cargo +1.97.1 test -p membership_core --all-features - cargo +1.97.1 clippy -p membership_core --all-targets --all-features -- -D warnings - cargo +1.97.1 test --workspace --all-features - python3 scripts/check_workspace_contract.py - python3 scripts/check_docstrings.py - python3 scripts/validate_documentation.py - - - name: Commit verified merge and remove one-shot workflow - run: | - rm -f .github/workflows/repair-pr54-distinct-membership-groups.yml - git add -A - git diff --cached --check - if ! git diff --cached --quiet; then - git commit -m "fix(membership): preserve distinct estimator groups" - fi - git push origin HEAD:agent/membership-estimation-rows diff --git a/docs/adr/0003-relational-event-multiple-membership.md b/docs/adr/0003-relational-event-multiple-membership.md index 71cacdd2..8d76cfef 100644 --- a/docs/adr/0003-relational-event-multiple-membership.md +++ b/docs/adr/0003-relational-event-multiple-membership.md @@ -1,7 +1,7 @@ # ADR 0003 — Relational event ontology and time-varying multiple membership **Decision status:** Accepted -**Implementation maturity:** partial — membership network and event mention/instance separation implemented-main; estimation rows and atomistic-collapse refusal are on the active PR; typed relation graph and persistence remain on other active PRs; multilevel estimators remain accepted-target +**Implementation maturity:** partial — membership network and event mention/instance separation implemented-main; estimation rows and atomistic-collapse refusal are on the active PR; typed relation graph and persistence remain on other active PRs; multilevel estimators remain accepted-target **Date:** 2026-08-05 **Supersedes:** None. ADR 0016 owns TDT/CHRONOS event-intelligence task semantics; this ADR remains authoritative for ontology, relation, role, and membership structure. From 30f9731d7e92e6706574bef78bde54fd81a27208 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:26:10 +0900 Subject: [PATCH 10/13] style: format membership contracts --- crates/membership_core/src/network.rs | 11 +++++++++-- .../tests/atomistic_collapse_structure_contract.rs | 6 +----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/membership_core/src/network.rs b/crates/membership_core/src/network.rs index 41b5fc11..24845bff 100644 --- a/crates/membership_core/src/network.rs +++ b/crates/membership_core/src/network.rs @@ -88,7 +88,9 @@ impl MembershipNetwork { self.assignments .iter() .copied() - .filter(|assignment| assignment.member_id() == member_id && assignment.is_active_at(instant)) + .filter(|assignment| { + assignment.member_id() == member_id && assignment.is_active_at(instant) + }) .collect() } @@ -133,7 +135,12 @@ impl EstimationMembershipRow { /// Copy the scientifically relevant fields from one assignment. #[must_use] pub fn from_assignment(assignment: MembershipAssignment) -> Self { - Self { member_id: assignment.member_id(), group_id: assignment.group_id(), role: assignment.role(), weight: assignment.weight().value() } + Self { + member_id: assignment.member_id(), + group_id: assignment.group_id(), + role: assignment.role(), + weight: assignment.weight().value(), + } } /// Member identity on this row. diff --git a/crates/membership_core/tests/atomistic_collapse_structure_contract.rs b/crates/membership_core/tests/atomistic_collapse_structure_contract.rs index e9092f6a..48a2f784 100644 --- a/crates/membership_core/tests/atomistic_collapse_structure_contract.rs +++ b/crates/membership_core/tests/atomistic_collapse_structure_contract.rs @@ -10,11 +10,7 @@ fn event_time(value: &str) -> EventTime { EventTime::parse_rfc3339(value).expect("event time") } -fn assignment( - member: MemberId, - group: GroupId, - role: MembershipRole, -) -> MembershipAssignment { +fn assignment(member: MemberId, group: GroupId, role: MembershipRole) -> MembershipAssignment { MembershipAssignment::new( member, group, From c18b34871a0d5e0b72f353e190545581ff6c6200 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:35:07 +0900 Subject: [PATCH 11/13] test(api): cover nested orchestrator JSON values --- crates/tepp_api/src/orchestrator_http.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/tepp_api/src/orchestrator_http.rs b/crates/tepp_api/src/orchestrator_http.rs index d684e201..0264954c 100644 --- a/crates/tepp_api/src/orchestrator_http.rs +++ b/crates/tepp_api/src/orchestrator_http.rs @@ -222,4 +222,12 @@ mod tests { assert_eq!(exchange.method(), "POST"); assert!(exchange.headers().is_empty()); } + + #[test] + fn bounded_json_walks_arrays_and_scalar_values() { + assert_eq!( + super::require_bounded_json_object(r#"{"items":[null,true,1,{"name":"ok"}]}"#), + Ok(()) + ); + } } From a134ea29847500a700ece1813461579454f30bc0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:49:08 +0900 Subject: [PATCH 12/13] test: close orchestrator and membership branch coverage --- .../atomistic_collapse_structure_contract.rs | 4 ++++ .../tepp_api/tests/orchestrator_http_contract.rs | 5 +++++ .../tests/orchestrator_http_security_contract.rs | 16 ++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/crates/membership_core/tests/atomistic_collapse_structure_contract.rs b/crates/membership_core/tests/atomistic_collapse_structure_contract.rs index 48a2f784..9a43936e 100644 --- a/crates/membership_core/tests/atomistic_collapse_structure_contract.rs +++ b/crates/membership_core/tests/atomistic_collapse_structure_contract.rs @@ -75,6 +75,10 @@ fn rows_from_different_members_fail_closed() { .estimation_rows_at(second_member, instant) .expect("second rows"), ); + assert_eq!( + refuse_atomistic_collapse(&mixed, 0), + Err(MembershipError::InvalidWirePayload) + ); assert_eq!( refuse_atomistic_collapse(&mixed, 2), Err(MembershipError::InvalidWirePayload) diff --git a/crates/tepp_api/tests/orchestrator_http_contract.rs b/crates/tepp_api/tests/orchestrator_http_contract.rs index f8de2982..7fcaf1c0 100644 --- a/crates/tepp_api/tests/orchestrator_http_contract.rs +++ b/crates/tepp_api/tests/orchestrator_http_contract.rs @@ -47,6 +47,7 @@ fn table_access_and_non_https_origins_fail_closed() { for host in [ "", "bad host", + "\0.example", "h@x", "h/sql", "h?x", @@ -59,6 +60,10 @@ fn table_access_and_non_https_origins_fail_closed() { Err(ApiError::AuthorizationDenied) ); } + assert_eq!( + orchestrator_interpretation_exchange(&"a".repeat(254), "idem-1", "{}"), + Err(ApiError::AuthorizationDenied) + ); assert_eq!( orchestrator_interpretation_exchange("tables.example", "idem-1", "{}"), Err(ApiError::AuthorizationDenied) diff --git a/crates/tepp_api/tests/orchestrator_http_security_contract.rs b/crates/tepp_api/tests/orchestrator_http_security_contract.rs index b525fb69..b6e777ce 100644 --- a/crates/tepp_api/tests/orchestrator_http_security_contract.rs +++ b/crates/tepp_api/tests/orchestrator_http_security_contract.rs @@ -31,6 +31,10 @@ fn normalized_nested_secret_names_fail_closed() { r#"{"nested":{"review-agent-github-token":"x"}}"#, r#"{"credential_name":"GITHUB_TOKEN"}"#, r#"{"credential_name":"copilot github token"}"#, + r#"{"credential_name":"OPENCODE_GITHUB_TOKEN"}"#, + r#"{"credential_name":"copilot interpretation token"}"#, + r#"{"credential_name":"reviewagent interpretation token"}"#, + r#"{"credential_name":"github interpretation token"}"#, ] { assert_eq!( orchestrator_interpretation_exchange("orchestrator.example", "idem-1", body), @@ -38,6 +42,14 @@ fn normalized_nested_secret_names_fail_closed() { ); } + for body in [ + r#"{"credential_name":"copilot credential"}"#, + r#"{"credential_name":"reviewagent credential"}"#, + ] { + orchestrator_interpretation_exchange("orchestrator.example", "idem-1", body) + .expect("names without a token suffix remain ordinary payload text"); + } + orchestrator_interpretation_exchange( "orchestrator.example", "idem-1", @@ -61,4 +73,8 @@ fn idempotency_key_is_bounded_and_header_safe() { ), Err(ApiError::InvalidWirePayload) ); + assert_eq!( + orchestrator_interpretation_exchange("orchestrator.example", "idem\0key", "{}"), + Err(ApiError::InvalidWirePayload) + ); } From 725e8c9b3447d78a8bcdfcb02c98c12c5bf2851f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:15:24 +0900 Subject: [PATCH 13/13] docs: remove trailing whitespace from msa adr --- docs/adr/0011-standalone-modular-msa-boundary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0011-standalone-modular-msa-boundary.md b/docs/adr/0011-standalone-modular-msa-boundary.md index 2f597764..df41810e 100644 --- a/docs/adr/0011-standalone-modular-msa-boundary.md +++ b/docs/adr/0011-standalone-modular-msa-boundary.md @@ -1,7 +1,7 @@ # ADR 0011 — Standalone operation and modular CWL MSA boundary **Decision status:** Accepted -**Implementation maturity:** partial — Rust crates are independently usable; contextual-orchestrator HTTPS interpretation interchange is on the active PR; live HTTP servers and remaining persistence integrations remain accepted-target +**Implementation maturity:** partial — Rust crates are independently usable; contextual-orchestrator HTTPS interpretation interchange is on the active PR; live HTTP servers and remaining persistence integrations remain accepted-target **Date:** 2026-08-10 **Supersedes:** The broad cross-service ownership wording in ADR 0001. ADR 0001 remains authoritative for Rust-first numerical architecture.