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/CHANGELOG.md b/CHANGELOG.md index acf1cbe4..b20a4302 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. - `membership_core` estimation rows: one document emits every active membership at an event time, recovered weights are scored with computed RMSE, and collapsing a known multiple-membership set into a single independent row is refused (atomistic fallacy). - `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. diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 74803970..38d0b16f 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) | | Membership estimation-row doctoring | [`docs/research/membership-estimation-rows.md`](docs/research/membership-estimation-rows.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/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..9a43936e 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, @@ -79,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/src/lib.rs b/crates/tepp_api/src/lib.rs index 3e41af2c..75c34652 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,17 @@ 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. +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..0264954c --- /dev/null +++ b/crates/tepp_api/src/orchestrator_http.rs @@ -0,0 +1,233 @@ +//! Versioned HTTPS interchange for the contextual-orchestrator interpretation port. + +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)] +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 secret names. + #[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 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 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_safe_idempotency_key(idempotency_key)?; + require_bounded_json_object(body)?; + require_safe_https_host(origin_host)?; + 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 every name except the NVIDIA NIM key. +pub fn refuse_repository_write_secret(secret_name: &str) -> Result<(), ApiError> { + require_nonempty(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() +} + +fn is_forbidden_secret_name(value: &str) -> bool { + let folded = normalize_secret_name(value); + if folded == "nvidianimapikey" { + return false; + } + 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() + || 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()); + } + + #[test] + fn bounded_json_walks_arrays_and_scalar_values() { + assert_eq!( + super::require_bounded_json_object(r#"{"items":[null,true,1,{"name":"ok"}]}"#), + Ok(()) + ); + } +} 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..7fcaf1c0 --- /dev/null +++ b/crates/tepp_api/tests/orchestrator_http_contract.rs @@ -0,0 +1,96 @@ +//! 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", + "\0.example", + "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(&"a".repeat(254), "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/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..b6e777ce --- /dev/null +++ b/crates/tepp_api/tests/orchestrator_http_security_contract.rs @@ -0,0 +1,80 @@ +//! 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"}"#, + 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), + Err(ApiError::AuthorizationDenied) + ); + } + + 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", + 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) + ); + assert_eq!( + orchestrator_interpretation_exchange("orchestrator.example", "idem\0key", "{}"), + 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 7f14515e..011b9556 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/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. diff --git a/docs/adr/0011-standalone-modular-msa-boundary.md b/docs/adr/0011-standalone-modular-msa-boundary.md index b83576e9..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; 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 9b9ef94b..2befd107 100644 --- a/docs/validation/temporal-event-foundation.md +++ b/docs/validation/temporal-event-foundation.md @@ -25,6 +25,7 @@ This report tracks exact-head scientific and engineering evidence required befor | Versioned API/export contracts | `tepp_api` | implemented-main | — | unknown-field/version/limit tests | Task 12 / PR #21; HTTP service remaining | | Multiple-membership estimation rows | `membership_core` | active-PR | rows + collapse refusal | 3-row RMSE + collapse deny | ADR 0003; `docs/research/membership-estimation-rows.md` | | 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 |