From f8d942a1c9e8cfb2a2ed95f1c21bb1c317ed1410 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 22:24:42 +0900 Subject: [PATCH 01/12] feat(api): bind org .github workflows as CI/review/security only Reusable-workflow identities from ContextualWisdomLab/.github cannot become scientific authority, access application tables, or carry Copilot/review-agent secrets. Check conclusions never promote implemented-main claims (ADR 0011). --- CHANGELOG.md | 1 + DOCUMENTATION.md | 1 + crates/tepp_api/src/lib.rs | 17 ++ crates/tepp_api/src/org_github.rs | 212 ++++++++++++++++++ crates/tepp_api/tests/org_github_contract.rs | 86 +++++++ docs/API_CONTRACT.md | 1 + docs/TRACEABILITY.md | 1 + .../0011-standalone-modular-msa-boundary.md | 4 +- docs/adr/README.md | 2 +- docs/connectors/org-github-control-plane.md | 45 ++++ docs/research/org-github-control-plane.md | 35 +++ docs/research/standards-and-literature.md | 2 + docs/validation/temporal-event-foundation.md | 1 + 13 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 crates/tepp_api/src/org_github.rs create mode 100644 crates/tepp_api/tests/org_github_contract.rs create mode 100644 docs/connectors/org-github-control-plane.md create mode 100644 docs/research/org-github-control-plane.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9abfea7e..e9fdfbd3 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` org-central `.github` reusable-workflow bindings: CI/review/security authority only, fail-closed table-access and Copilot/review-agent secret refusal, and Check conclusions that cannot promote scientific or implemented-main claims (ADR 0011; no new migration). - `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..5bfd83e7 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -11,6 +11,7 @@ TEPP's approved PRD v0.4 and implementation plan are the primary product baselin | 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) | | contextual-orchestrator interpretation port | [`docs/connectors/contextual-orchestrator-interpretation-port.md`](docs/connectors/contextual-orchestrator-interpretation-port.md) | +| org-central `.github` control plane | [`docs/connectors/org-github-control-plane.md`](docs/connectors/org-github-control-plane.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/tepp_api/src/lib.rs b/crates/tepp_api/src/lib.rs index 3e41af2c..a417fd37 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 org_github; mod wire; /// Analysis-run contract version constant. @@ -46,3 +47,19 @@ pub use authorization::ExportAuthorizationRequest; pub use authorization::authorize_export; /// Fail closed when an export decision is denied. pub use authorization::require_export_allowed; +/// Org reusable-workflow contract version. +pub use org_github::ORG_GITHUB_WORKFLOW_CONTRACT_VERSION; +/// Organization control-plane repository identity. +pub use org_github::ORG_GITHUB_WORKFLOW_OWNER; +/// Fail-closed org reusable-workflow binding. +pub use org_github::OrgGithubWorkflowBinding; +/// Authority an org reusable workflow may hold. +pub use org_github::OrgWorkflowAuthority; +/// Bind an org reusable workflow as CI/review/security only. +pub use org_github::bind_org_github_workflow; +/// Check conclusions never promote scientific claims. +pub use org_github::refuse_check_conclusion_as_scientific_claim; +/// Refuse Copilot, GitHub, or review-agent secret names. +pub use org_github::refuse_org_workflow_secret; +/// Org workflows never receive application-table access. +pub use org_github::refuse_org_workflow_table_access; diff --git a/crates/tepp_api/src/org_github.rs b/crates/tepp_api/src/org_github.rs new file mode 100644 index 00000000..afe5be1b --- /dev/null +++ b/crates/tepp_api/src/org_github.rs @@ -0,0 +1,212 @@ +//! Versioned org-central `.github` reusable-workflow interchange. + +use crate::ApiError; +use crate::wire::require_nonempty; + +/// Contract version for org-central reusable-workflow bindings. +pub const ORG_GITHUB_WORKFLOW_CONTRACT_VERSION: u16 = 1; + +/// Organization control-plane repository that may own reusable workflows. +pub const ORG_GITHUB_WORKFLOW_OWNER: &str = "ContextualWisdomLab/.github"; + +/// Authority an org reusable workflow may hold. +/// +/// Scientific acceptance is intentionally absent: CI conclusions cannot +/// promote recovery, invariance, or implemented-main claims (ADR 0014). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum OrgWorkflowAuthority { + /// CI, review, security, and release-control only. + CiReviewSecurity, +} + +/// Fail-closed binding of an org reusable workflow identity. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OrgGithubWorkflowBinding { + contract_version: u16, + workflow_identity: String, + authority: OrgWorkflowAuthority, +} + +impl OrgGithubWorkflowBinding { + /// Contract version accepted for this binding. + #[must_use] + pub const fn contract_version(&self) -> u16 { + self.contract_version + } + + /// Canonical reusable-workflow identity (`owner/.github/workflows/…@ref`). + #[must_use] + pub fn workflow_identity(&self) -> &str { + &self.workflow_identity + } + + /// Bound authority; always [`OrgWorkflowAuthority::CiReviewSecurity`]. + #[must_use] + pub const fn authority(&self) -> OrgWorkflowAuthority { + self.authority + } +} + +/// Bind an org reusable workflow as CI/review/security control only. +/// +/// The reference must name [`ORG_GITHUB_WORKFLOW_OWNER`] and a `workflows/` +/// path. Table-access hosts and scientific-claim names fail closed. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for an empty reference and +/// [`ApiError::AuthorizationDenied`] for hostile or non-org identities. +pub fn bind_org_github_workflow(workflow_ref: &str) -> Result { + require_nonempty(workflow_ref)?; + refuse_org_workflow_table_access(workflow_ref)?; + require_org_workflow_identity(workflow_ref)?; + Ok(OrgGithubWorkflowBinding { + contract_version: ORG_GITHUB_WORKFLOW_CONTRACT_VERSION, + workflow_identity: workflow_ref.trim().to_owned(), + authority: OrgWorkflowAuthority::CiReviewSecurity, + }) +} + +/// GitHub Check conclusions never promote scientific or implemented-main claims. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for an empty conclusion and +/// [`ApiError::AuthorizationDenied`] for every nonempty conclusion. +pub fn refuse_check_conclusion_as_scientific_claim(conclusion: &str) -> Result<(), ApiError> { + require_nonempty(conclusion)?; + Err(ApiError::AuthorizationDenied) +} + +/// Org workflows never receive TEPP application-table access. +/// +/// # Errors +/// +/// Returns [`ApiError::InvalidWirePayload`] for an empty target and +/// [`ApiError::AuthorizationDenied`] for SQL, JDBC, or table hosts. +pub fn refuse_org_workflow_table_access(target: &str) -> Result<(), ApiError> { + require_nonempty(target)?; + let lowered = target.to_ascii_lowercase(); + if ["postgres", "jdbc", "sql", "tables"] + .iter() + .any(|needle| lowered.contains(needle)) + { + return Err(ApiError::AuthorizationDenied); + } + Ok(()) +} + +/// 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_org_workflow_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_org_workflow_identity(workflow_ref: &str) -> Result<(), ApiError> { + let folded = workflow_ref.to_ascii_lowercase(); + if !folded.contains("contextualwisdomlab/.github") || !folded.contains("workflows/") { + return Err(ApiError::AuthorizationDenied); + } + if folded.contains("scientific") || folded.contains("recovery") { + return Err(ApiError::AuthorizationDenied); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + OrgGithubWorkflowBinding, OrgWorkflowAuthority, bind_org_github_workflow, + refuse_check_conclusion_as_scientific_claim, refuse_org_workflow_secret, + refuse_org_workflow_table_access, require_org_workflow_identity, + }; + use crate::ApiError; + + #[test] + fn identity_and_secret_branches_are_covered() { + assert_eq!( + require_org_workflow_identity("other/.github/workflows/ci.yml"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + require_org_workflow_identity("ContextualWisdomLab/.github/readme.md"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + require_org_workflow_identity( + "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml" + ), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + require_org_workflow_identity( + "ContextualWisdomLab/.github/.github/workflows/recovery-claim.yml" + ), + Err(ApiError::AuthorizationDenied) + ); + require_org_workflow_identity( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", + ) + .expect("org workflow"); + assert_eq!( + refuse_org_workflow_secret("AWS_SECRET"), + Err(ApiError::AuthorizationDenied) + ); + refuse_org_workflow_table_access( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", + ) + .expect("not a table host"); + assert_eq!( + refuse_check_conclusion_as_scientific_claim("FAILURE"), + Err(ApiError::AuthorizationDenied) + ); + let binding = bind_org_github_workflow( + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@main", + ) + .expect("review workflow"); + assert_eq!(binding.authority(), OrgWorkflowAuthority::CiReviewSecurity); + let constructed = OrgGithubWorkflowBinding { + contract_version: 1, + workflow_identity: "constructed".into(), + authority: OrgWorkflowAuthority::CiReviewSecurity, + }; + assert_eq!(constructed.workflow_identity(), "constructed"); + assert_eq!(constructed.contract_version(), 1); + assert_eq!( + bind_org_github_workflow("other/.github/workflows/ci.yml"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + bind_org_github_workflow( + "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml" + ), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + refuse_org_workflow_secret("REVIEW_AGENT"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + refuse_org_workflow_secret("GITHUB_TOKEN"), + Err(ApiError::AuthorizationDenied) + ); + } +} diff --git a/crates/tepp_api/tests/org_github_contract.rs b/crates/tepp_api/tests/org_github_contract.rs new file mode 100644 index 00000000..875f2ff3 --- /dev/null +++ b/crates/tepp_api/tests/org_github_contract.rs @@ -0,0 +1,86 @@ +//! Org-central `.github` reusable workflows own CI/review/security only. + +use tepp_api::{ + ApiError, ORG_GITHUB_WORKFLOW_CONTRACT_VERSION, ORG_GITHUB_WORKFLOW_OWNER, + OrgWorkflowAuthority, bind_org_github_workflow, refuse_check_conclusion_as_scientific_claim, + refuse_org_workflow_secret, refuse_org_workflow_table_access, +}; + +#[test] +fn reusable_workflow_binds_as_ci_review_security_only() { + let binding = bind_org_github_workflow( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", + ) + .expect("org reusable workflow"); + assert_eq!( + binding.contract_version(), + ORG_GITHUB_WORKFLOW_CONTRACT_VERSION + ); + assert_eq!(ORG_GITHUB_WORKFLOW_CONTRACT_VERSION, 1); + assert_eq!(ORG_GITHUB_WORKFLOW_OWNER, "ContextualWisdomLab/.github"); + assert!( + binding + .workflow_identity() + .contains(ORG_GITHUB_WORKFLOW_OWNER) + ); + assert!( + binding + .workflow_identity() + .contains("workflows/security-scan.yml") + ); + assert_eq!(binding.authority(), OrgWorkflowAuthority::CiReviewSecurity); + assert_eq!( + refuse_check_conclusion_as_scientific_claim("SUCCESS"), + Err(ApiError::AuthorizationDenied) + ); +} + +#[test] +fn table_access_and_hostile_workflow_refs_fail_closed() { + for workflow_ref in [ + "", + " ", + "postgres://tepp/application_table", + "jdbc:postgresql://db/tepp", + "sql.internal/tables", + "tables.example/workflows/ci.yml", + ] { + assert_eq!( + bind_org_github_workflow(workflow_ref), + Err(if workflow_ref.trim().is_empty() { + ApiError::InvalidWirePayload + } else { + ApiError::AuthorizationDenied + }) + ); + } + assert_eq!( + refuse_org_workflow_table_access("postgres://tepp/application_table"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + refuse_org_workflow_table_access(""), + Err(ApiError::InvalidWirePayload) + ); +} + +#[test] +fn review_agent_and_copilot_secrets_are_refused() { + assert_eq!( + refuse_org_workflow_secret("COPILOT_GITHUB_TOKEN"), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + refuse_org_workflow_secret("review-agent-github-token"), + Err(ApiError::AuthorizationDenied) + ); + refuse_org_workflow_secret("NVIDIA_NIM_API_KEY").expect("nim allowed as name"); + assert_eq!( + refuse_org_workflow_secret(""), + Err(ApiError::InvalidWirePayload) + ); + assert_eq!( + refuse_check_conclusion_as_scientific_claim(""), + Err(ApiError::InvalidWirePayload) + ); +} diff --git a/docs/API_CONTRACT.md b/docs/API_CONTRACT.md index d1b12be8..40cc5685 100644 --- a/docs/API_CONTRACT.md +++ b/docs/API_CONTRACT.md @@ -21,6 +21,7 @@ Current protected main exposes Rust library/domain contracts, not a production H | LLM interpretation provider port | TEPP interpretation gateway | contextual-orchestrator | accepted-target | | model/artifact/export API | `tepp_api` export envelopes + future HTTP service | standalone UI/CWL consumers | partial | | analysis-run request/accepted contracts | `tepp_api` v1 wire DTOs | naruon, orchestrator, UI | active-PR | +| org-central `.github` reusable-workflow binding | `tepp_api` `bind_org_github_workflow` | organization control-plane workflows | active-PR | ## 3. Versioning diff --git a/docs/TRACEABILITY.md b/docs/TRACEABILITY.md index 051062ea..b72227d1 100644 --- a/docs/TRACEABILITY.md +++ b/docs/TRACEABILITY.md @@ -38,6 +38,7 @@ The full APA 7th standards/literature register remains `docs/research/standards- | 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 | +| org-central `.github` CI/review/security control plane | ADR 0011/0014/0015; API contract | `docs/connectors/org-github-control-plane.md` + `tepp_api` reusable-workflow bindings; live reusable-workflow dispatch remaining | active-PR | | 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..b8270d8a 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; org-central `.github` reusable-workflow bindings are on the active PR; live reusable-workflow dispatch 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. @@ -50,7 +50,7 @@ Every public API/artifact contract is versioned. A breaking consumer/provider ch ## Verification -Required tests cover contract version negotiation, unauthorized cross-service access, idempotency, stale artifact/model identities, missing dependencies, standalone CPU operation, contextual-orchestrator optional integration, naruon consumer contracts, and absence of direct cross-service database coupling. +Required tests cover contract version negotiation, unauthorized cross-service access, idempotency, stale artifact/model identities, missing dependencies, standalone CPU operation, contextual-orchestrator optional integration, naruon consumer contracts, org-central `.github` reusable-workflow identity and scientific-claim refusal, and absence of direct cross-service database coupling. ## Rollback and supersession diff --git a/docs/adr/README.md b/docs/adr/README.md index 1a9a7b31..ecbe00bd 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -16,7 +16,7 @@ Read [`ADR_POLICY.md`](ADR_POLICY.md) first. **Decision status and implementatio | [0008](0008-immutable-evidence-identities-digests-and-spans.md) | Immutable evidence identities, `SHA-256` digests, exact spans, and strict wire reconstruction | Accepted | implemented-main | ADR 0013 governs future persistence/reproducibility/split authority. | | [0009](0009-purpose-bound-pii-governance.md) | Purpose-bound PII governance without blanket masking | Accepted | accepted-target | Controls are normative architecture; deployment/control evidence is not yet a certification claim. | | [0010](0010-adaptive-llm-orchestration.md) | Adaptive LLM orchestration and test-time compute | Accepted | accepted-target | Owns direct/verify/committee/conductor selection, budget, role/topology, and ablation policy. | -| [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; no direct cross-service application-table coupling. | +| [0011](0011-standalone-modular-msa-boundary.md) | Standalone operation and modular CWL MSA boundary | Accepted | partial | Owns cross-service persistence/credential/API authority; org `.github` reusable-workflow bindings are on the active PR; no direct cross-service application-table coupling. | | [0012](0012-temporal-relational-shared-latent-topic-measurement.md) | Temporal Relational Shared-Latent Topic Measurement (TRSL-TM) | Accepted | accepted-target | Owns topic backend compatibility, global topic identity, method effects, K/model-selection prerequisites, and compositional topic coordinates. | | [0013](0013-bitemporal-persistence-reproducibility-and-split-authority.md) | Bitemporal persistence, reproducibility manifests, and relation-aware split authority | Accepted | partial | Owns PostgreSQL adapter semantics, immutable run/split manifests, leakage-safe partitions, and recovery identity; optional `live-sqlx` `PgPool`, live PG CI, and tenant RLS implemented; full physical ERD remaining. | | [0014](0014-scientific-claim-promotion-and-release-evidence.md) | Scientific claim promotion and release evidence authority | Accepted | partial | Separates design, implementation, scientific/product claim, and release authority; repository SBOM/provenance generator implemented, full release bundle remaining. | diff --git a/docs/connectors/org-github-control-plane.md b/docs/connectors/org-github-control-plane.md new file mode 100644 index 00000000..8e8cf1e5 --- /dev/null +++ b/docs/connectors/org-github-control-plane.md @@ -0,0 +1,45 @@ +# Organization `.github` control-plane contract for TEPP + +**Status:** Accepted-target modular integration contract; reusable-workflow bindings are on the active PR +**Last reviewed:** 2026-08-13 + +## Boundary + +Organization `.github` reusable workflows own CI, review, security, and release-control functions only (ADR 0011; ADR 0015). They must not: + +- become runtime scientific authority or promote implemented-main / recovery claims (ADR 0014); +- read or write TEPP application tables; +- receive `COPILOT_GITHUB_TOKEN` or existing independent review-agent credentials as a product-development path; +- replace deterministic TEPP validation with a GitHub Check conclusion. + +TEPP remains the scientific authority for estimation, recovery metrics, temporal eligibility, and purpose-bound export decisions. + +## Allowed control-plane surfaces + +| Surface | Contract | Direction | +|---|---|---| +| reusable workflow identity | `tepp_api` `bind_org_github_workflow` | org `.github` → TEPP binding | +| authority class | `OrgWorkflowAuthority::CiReviewSecurity` | TEPP gate | +| Check conclusion | `refuse_check_conclusion_as_scientific_claim` | TEPP gate | +| secret names | `refuse_org_workflow_secret` | TEPP gate | + +`NVIDIA_NIM_API_KEY` is the only allowed model-credential name for product-development workflows. Live reusable-workflow dispatch remains accepted-target. + +## Purpose-bound disclosure + +Control-plane workflows receive repository metadata, check identities, and SBOM/provenance artifacts they are authorized to process. They do not receive application-table credentials or identity-mapping stores (ADR 0009). + +## Failure modes + +- empty workflow identity → reject; +- table, JDBC, SQL, or `postgres` targets → reject; +- Copilot / GitHub / review-agent secret names → reject; +- any Check conclusion used as scientific acceptance → reject. + +## Authority sources + +GitHub. (n.d.). *Reusing workflows*. GitHub Docs. Retrieved August 13, 2026, from https://docs.github.com/en/actions/using-workflows/reusing-workflows + +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. diff --git a/docs/research/org-github-control-plane.md b/docs/research/org-github-control-plane.md new file mode 100644 index 00000000..0f0475f3 --- /dev/null +++ b/docs/research/org-github-control-plane.md @@ -0,0 +1,35 @@ +# Organization `.github` reusable-workflow interchange + +## Scope + +This note doctors the `tepp_api` org-central `.github` control-plane binding: + +1. reusable workflow identities bind only as CI/review/security authority; +2. table, JDBC, SQL, and `postgres` targets 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. GitHub Check conclusions cannot become scientific or implemented-main claims. + +This is a versioned identity/authority gate, not a live reusable-workflow dispatcher. No database migration is allocated. + +## Authoritative sources + +GitHub. (n.d.). *Reusing workflows*. GitHub Docs. Retrieved August 13, 2026, from https://docs.github.com/en/actions/using-workflows/reusing-workflows + +GitHub. (n.d.). *REST API endpoints for workflows*. GitHub Docs. Retrieved August 13, 2026, from https://docs.github.com/en/rest/actions/workflows + +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 + +Reusable workflows are a versioned control-plane composition mechanism, not a scientific estimator (GitHub, n.d.). 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 binds org `.github` identities to `CiReviewSecurity` and refuses Check conclusions as claim-promotion evidence. + +## Verification + +- a valid `ContextualWisdomLab/.github/.../workflows/*.yml@ref` binds as `CiReviewSecurity`; +- `postgres`, `jdbc`, `sql`, `tables`, and empty identities are denied; +- `COPILOT_GITHUB_TOKEN` and `review-agent-github-token` are denied; +- `NVIDIA_NIM_API_KEY` is an allowed secret name; +- `refuse_check_conclusion_as_scientific_claim` always denies nonempty conclusions. diff --git a/docs/research/standards-and-literature.md b/docs/research/standards-and-literature.md index b4b14468..c2063718 100644 --- a/docs/research/standards-and-literature.md +++ b/docs/research/standards-and-literature.md @@ -124,6 +124,8 @@ OpenSSF. (2023). *Supply-chain Levels for Software Artifacts (SLSA) specificatio GitHub. (n.d.). *REST API endpoints for workflows*. GitHub Docs. Retrieved August 13, 2026, from https://docs.github.com/en/rest/actions/workflows +GitHub. (n.d.). *Reusing workflows*. GitHub Docs. Retrieved August 13, 2026, from https://docs.github.com/en/actions/using-workflows/reusing-workflows + OWASP Foundation. (2023). *OWASP Top 10 CI/CD Security Risks*. https://owasp.org/www-project-top-10-ci-cd-security-risks/ TEPP treats documents and model output as untrusted, requires exact evidence and fail-closed validation, supplies accessible exact-value alternatives to graphics, and emits SBOM and provenance evidence for releases. Actions registry identities are inventoried against the protected-main tree rather than trusted because a YAML path once existed (GitHub, n.d.; OpenSSF, 2023; OWASP Foundation, 2023). diff --git a/docs/validation/temporal-event-foundation.md b/docs/validation/temporal-event-foundation.md index 984d329c..c7b136cf 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 | +| Org `.github` control-plane binding | `tepp_api` | active-PR | this PR | reusable-workflow identity + scientific-claim refusal | ADR 0011; live reusable-workflow dispatch remaining | | Release SBOM/provenance generator | `scripts/release_evidence.py` | partial | — | generate+validate in CI | Task 13 partial / PR #28 | From 5a8401756fb21627ea9648977e87f4dad182275a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:41:43 +0900 Subject: [PATCH 02/12] test(api): require immutable canonical org workflow identities --- .../org_github_identity_security_contract.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 crates/tepp_api/tests/org_github_identity_security_contract.rs diff --git a/crates/tepp_api/tests/org_github_identity_security_contract.rs b/crates/tepp_api/tests/org_github_identity_security_contract.rs new file mode 100644 index 00000000..136c0b4e --- /dev/null +++ b/crates/tepp_api/tests/org_github_identity_security_contract.rs @@ -0,0 +1,28 @@ +//! Org workflow identities must be canonical and immutable. + +use tepp_api::{ApiError, bind_org_github_workflow}; + +const PINNED_WORKFLOW: &str = "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae"; + +#[test] +fn canonical_workflow_requires_an_exact_owner_path_and_full_commit_sha() { + let binding = bind_org_github_workflow(PINNED_WORKFLOW).expect("pinned org workflow"); + assert_eq!(binding.workflow_identity(), PINNED_WORKFLOW); + + for invalid in [ + "evil.example/ContextualWisdomLab/.github/.github/workflows/security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "contextualwisdomlab/.github/.github/workflows/security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@f070c504", + "ContextualWisdomLab/.github/.github/workflows/../security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/subdir/security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security-scan.txt@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@F070C504C1CB06891B800D7AB0CF6AC7D3CF8EAE", + ] { + assert_eq!( + bind_org_github_workflow(invalid), + Err(ApiError::AuthorizationDenied), + "hostile or mutable workflow reference must fail closed: {invalid}" + ); + } +} From d4d04a0de029fb27d4700b8986fadd03390dca2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:42:41 +0900 Subject: [PATCH 03/12] fix(api): require immutable canonical org workflow identities --- crates/tepp_api/src/org_github.rs | 96 ++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 33 deletions(-) diff --git a/crates/tepp_api/src/org_github.rs b/crates/tepp_api/src/org_github.rs index afe5be1b..1c462d63 100644 --- a/crates/tepp_api/src/org_github.rs +++ b/crates/tepp_api/src/org_github.rs @@ -9,6 +9,9 @@ pub const ORG_GITHUB_WORKFLOW_CONTRACT_VERSION: u16 = 1; /// Organization control-plane repository that may own reusable workflows. pub const ORG_GITHUB_WORKFLOW_OWNER: &str = "ContextualWisdomLab/.github"; +const ORG_GITHUB_WORKFLOW_PREFIX: &str = + "ContextualWisdomLab/.github/.github/workflows/"; + /// Authority an org reusable workflow may hold. /// /// Scientific acceptance is intentionally absent: CI conclusions cannot @@ -34,7 +37,7 @@ impl OrgGithubWorkflowBinding { self.contract_version } - /// Canonical reusable-workflow identity (`owner/.github/workflows/…@ref`). + /// Canonical reusable-workflow identity (`owner/.github/workflows/…@sha`). #[must_use] pub fn workflow_identity(&self) -> &str { &self.workflow_identity @@ -47,22 +50,26 @@ impl OrgGithubWorkflowBinding { } } -/// Bind an org reusable workflow as CI/review/security control only. +/// Bind an immutable org reusable workflow as CI/review/security control only. /// -/// The reference must name [`ORG_GITHUB_WORKFLOW_OWNER`] and a `workflows/` -/// path. Table-access hosts and scientific-claim names fail closed. +/// The identity must use the exact [`ORG_GITHUB_WORKFLOW_OWNER`] spelling, a +/// top-level `.github/workflows/*.yml` or `*.yaml` file, and a lowercase 40-hex +/// commit SHA. Mutable branches, tags, path traversal, nested workflow paths, +/// and look-alike owner prefixes fail closed. The workflow never becomes +/// scientific authority. /// /// # Errors /// /// Returns [`ApiError::InvalidWirePayload`] for an empty reference and -/// [`ApiError::AuthorizationDenied`] for hostile or non-org identities. +/// [`ApiError::AuthorizationDenied`] for hostile, mutable, or non-org +/// identities. pub fn bind_org_github_workflow(workflow_ref: &str) -> Result { require_nonempty(workflow_ref)?; refuse_org_workflow_table_access(workflow_ref)?; require_org_workflow_identity(workflow_ref)?; Ok(OrgGithubWorkflowBinding { contract_version: ORG_GITHUB_WORKFLOW_CONTRACT_VERSION, - workflow_identity: workflow_ref.trim().to_owned(), + workflow_identity: workflow_ref.to_owned(), authority: OrgWorkflowAuthority::CiReviewSecurity, }) } @@ -103,7 +110,7 @@ pub fn refuse_org_workflow_table_access(target: &str) -> 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_org_workflow_secret(secret_name: &str) -> Result<(), ApiError> { require_nonempty(secret_name)?; let folded: String = secret_name @@ -112,20 +119,41 @@ pub fn refuse_org_workflow_secret(secret_name: &str) -> Result<(), ApiError> { .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); + Ok(()) + } else { + Err(ApiError::AuthorizationDenied) } - Err(ApiError::AuthorizationDenied) } fn require_org_workflow_identity(workflow_ref: &str) -> Result<(), ApiError> { - let folded = workflow_ref.to_ascii_lowercase(); - if !folded.contains("contextualwisdomlab/.github") || !folded.contains("workflows/") { + if workflow_ref.trim() != workflow_ref { return Err(ApiError::AuthorizationDenied); } - if folded.contains("scientific") || folded.contains("recovery") { + let Some(remainder) = workflow_ref.strip_prefix(ORG_GITHUB_WORKFLOW_PREFIX) else { + return Err(ApiError::AuthorizationDenied); + }; + let Some((workflow_file, commit_sha)) = remainder.split_once('@') else { + return Err(ApiError::AuthorizationDenied); + }; + if workflow_file.is_empty() + || workflow_file.contains('/') + || workflow_file.contains("..") + || !workflow_file + .chars() + .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')) + || !(workflow_file.ends_with(".yml") || workflow_file.ends_with(".yaml")) + { + return Err(ApiError::AuthorizationDenied); + } + if commit_sha.len() != 40 + || !commit_sha + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err(ApiError::AuthorizationDenied); + } + let lowered_file = workflow_file.to_ascii_lowercase(); + if lowered_file.contains("scientific") || lowered_file.contains("recovery") { return Err(ApiError::AuthorizationDenied); } Ok(()) @@ -140,6 +168,8 @@ mod tests { }; use crate::ApiError; + const SHA: &str = "f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae"; + #[test] fn identity_and_secret_branches_are_covered() { assert_eq!( @@ -151,36 +181,36 @@ mod tests { Err(ApiError::AuthorizationDenied) ); assert_eq!( - require_org_workflow_identity( - "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml" - ), + require_org_workflow_identity(&format!( + "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml@{SHA}" + )), Err(ApiError::AuthorizationDenied) ); assert_eq!( - require_org_workflow_identity( - "ContextualWisdomLab/.github/.github/workflows/recovery-claim.yml" - ), + require_org_workflow_identity(&format!( + "ContextualWisdomLab/.github/.github/workflows/recovery-claim.yml@{SHA}" + )), Err(ApiError::AuthorizationDenied) ); - require_org_workflow_identity( - "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", - ) + require_org_workflow_identity(&format!( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@{SHA}" + )) .expect("org workflow"); assert_eq!( refuse_org_workflow_secret("AWS_SECRET"), Err(ApiError::AuthorizationDenied) ); - refuse_org_workflow_table_access( - "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", - ) + refuse_org_workflow_table_access(&format!( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@{SHA}" + )) .expect("not a table host"); assert_eq!( refuse_check_conclusion_as_scientific_claim("FAILURE"), Err(ApiError::AuthorizationDenied) ); - let binding = bind_org_github_workflow( - "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@main", - ) + let binding = bind_org_github_workflow(&format!( + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@{SHA}" + )) .expect("review workflow"); assert_eq!(binding.authority(), OrgWorkflowAuthority::CiReviewSecurity); let constructed = OrgGithubWorkflowBinding { @@ -195,9 +225,9 @@ mod tests { Err(ApiError::AuthorizationDenied) ); assert_eq!( - bind_org_github_workflow( - "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml" - ), + bind_org_github_workflow(&format!( + "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml@{SHA}" + )), Err(ApiError::AuthorizationDenied) ); assert_eq!( From 7ed7a48497601ff3078c2cc4c41c8c26c810be63 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:43:09 +0900 Subject: [PATCH 04/12] test(api): pin org workflow contract to full commit SHA --- crates/tepp_api/tests/org_github_contract.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/tepp_api/tests/org_github_contract.rs b/crates/tepp_api/tests/org_github_contract.rs index 875f2ff3..67016413 100644 --- a/crates/tepp_api/tests/org_github_contract.rs +++ b/crates/tepp_api/tests/org_github_contract.rs @@ -6,12 +6,14 @@ use tepp_api::{ refuse_org_workflow_secret, refuse_org_workflow_table_access, }; +const PINNED_SHA: &str = "f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae"; + #[test] fn reusable_workflow_binds_as_ci_review_security_only() { - let binding = bind_org_github_workflow( - "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", - ) - .expect("org reusable workflow"); + let identity = format!( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@{PINNED_SHA}" + ); + let binding = bind_org_github_workflow(&identity).expect("org reusable workflow"); assert_eq!( binding.contract_version(), ORG_GITHUB_WORKFLOW_CONTRACT_VERSION @@ -21,13 +23,14 @@ fn reusable_workflow_binds_as_ci_review_security_only() { assert!( binding .workflow_identity() - .contains(ORG_GITHUB_WORKFLOW_OWNER) + .starts_with(ORG_GITHUB_WORKFLOW_OWNER) ); assert!( binding .workflow_identity() .contains("workflows/security-scan.yml") ); + assert!(binding.workflow_identity().ends_with(PINNED_SHA)); assert_eq!(binding.authority(), OrgWorkflowAuthority::CiReviewSecurity); assert_eq!( refuse_check_conclusion_as_scientific_claim("SUCCESS"), From d2e5d35ef502c108a438961639f2f7cb40b88d0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:43:49 +0900 Subject: [PATCH 05/12] ci: verify PR 55 immutable org workflow binding --- ...repair-pr55-immutable-workflow-binding.yml | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/repair-pr55-immutable-workflow-binding.yml diff --git a/.github/workflows/repair-pr55-immutable-workflow-binding.yml b/.github/workflows/repair-pr55-immutable-workflow-binding.yml new file mode 100644 index 00000000..e49724af --- /dev/null +++ b/.github/workflows/repair-pr55-immutable-workflow-binding.yml @@ -0,0 +1,95 @@ +name: Repair PR 55 immutable workflow binding + +on: + pull_request: + types: + - synchronize + - reopened + - ready_for_review + +permissions: + contents: read + +concurrency: + group: repair-tepp-pr-55-immutable-workflow-binding + cancel-in-progress: true + +jobs: + repair: + if: >- + github.event.pull_request.number == 55 && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'agent/org-github-control-plane' + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: write + steps: + - name: Checkout exact PR branch + uses: actions/checkout@631c942040754b6e095e929c1677c07e10ed4f87 + with: + ref: agent/org-github-control-plane + fetch-depth: 0 + persist-credentials: true + + - name: Install pinned Rust toolchains + run: | + rustup toolchain install 1.97.1 --profile minimal --component clippy --component rustfmt --component llvm-tools-preview + rustup toolchain install nightly-2026-08-01 --profile minimal --component llvm-tools-preview + + - name: Prove the pre-implementation identity contract was RED + run: | + git worktree add "$RUNNER_TEMP/tepp-red" 5a8401756fb21627ea9648977e87f4dad182275a + set +e + output=$(cd "$RUNNER_TEMP/tepp-red" && cargo +1.97.1 test -p tepp_api --test org_github_identity_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 mutable or look-alike workflow identities to fail the new contract" >&2 + exit 1 + fi + grep -E "canonical_workflow_requires|hostile or mutable" <<<"$output" + + - name: Merge current protected main without discarding feature behavior + run: | + git fetch origin main + git merge --no-edit -X ours origin/main + + - name: Refuse mutable workflow examples in product documentation + run: | + if git grep -n -E 'ContextualWisdomLab/\.github/\.github/workflows/[^ @]+@(main|master|develop|HEAD)' -- '*.md' '*.rs'; then + echo "Mutable org-workflow references remain" >&2 + exit 1 + fi + + - 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: Enforce exact authored coverage + run: | + cargo +1.97.1 install cargo-llvm-cov --locked --version 0.8.6 + cargo +1.97.1 llvm-cov -p tepp_api --all-features --fail-under-lines 100 + cargo +nightly-2026-08-01 llvm-cov --branch -p tepp_api --all-features --json --summary-only --output-path coverage-branches.json + python3 scripts/check_coverage.py coverage-branches.json --kind branches + + - name: Commit verified merge and remove one-shot workflow + run: | + rm -f coverage-branches.json + rm -f .github/workflows/repair-pr55-immutable-workflow-binding.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): pin org workflow identities to commits" + fi + git push origin HEAD:agent/org-github-control-plane From 0527898e06d0a5f99223563b533afd6f75f03bee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 18:48:04 +0900 Subject: [PATCH 06/12] ci: retrigger PR 55 immutable workflow repair --- .github/workflows/repair-pr55-immutable-workflow-binding.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/repair-pr55-immutable-workflow-binding.yml b/.github/workflows/repair-pr55-immutable-workflow-binding.yml index e49724af..de6121fd 100644 --- a/.github/workflows/repair-pr55-immutable-workflow-binding.yml +++ b/.github/workflows/repair-pr55-immutable-workflow-binding.yml @@ -93,3 +93,5 @@ jobs: git commit -m "fix(api): pin org workflow identities to commits" fi git push origin HEAD:agent/org-github-control-plane + +# Synchronize exact-head repair after ready-for-review transition. From 9d60a6bfe2b4b8534d6d44e3abaa1b2bf12cf289 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 19:06:55 +0900 Subject: [PATCH 07/12] ci: configure merge identity before PR 55 repair --- .../repair-pr55-immutable-workflow-binding.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/repair-pr55-immutable-workflow-binding.yml b/.github/workflows/repair-pr55-immutable-workflow-binding.yml index de6121fd..6bbcb155 100644 --- a/.github/workflows/repair-pr55-immutable-workflow-binding.yml +++ b/.github/workflows/repair-pr55-immutable-workflow-binding.yml @@ -2,10 +2,7 @@ name: Repair PR 55 immutable workflow binding on: pull_request: - types: - - synchronize - - reopened - - ready_for_review + types: [synchronize, reopened, ready_for_review] permissions: contents: read @@ -54,6 +51,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 @@ -83,15 +82,10 @@ jobs: - name: Commit verified merge and remove one-shot workflow run: | - rm -f coverage-branches.json - rm -f .github/workflows/repair-pr55-immutable-workflow-binding.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + rm -f coverage-branches.json .github/workflows/repair-pr55-immutable-workflow-binding.yml git add -A git diff --cached --check if ! git diff --cached --quiet; then git commit -m "fix(api): pin org workflow identities to commits" fi git push origin HEAD:agent/org-github-control-plane - -# Synchronize exact-head repair after ready-for-review transition. From a3f21502811d07e2c02b46d9783f5fc65273de48 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 18:55:10 +0900 Subject: [PATCH 08/12] style(api): format org workflow contract --- crates/tepp_api/src/org_github.rs | 9 ++++----- crates/tepp_api/tests/org_github_contract.rs | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/tepp_api/src/org_github.rs b/crates/tepp_api/src/org_github.rs index 1c462d63..bed60802 100644 --- a/crates/tepp_api/src/org_github.rs +++ b/crates/tepp_api/src/org_github.rs @@ -9,8 +9,7 @@ pub const ORG_GITHUB_WORKFLOW_CONTRACT_VERSION: u16 = 1; /// Organization control-plane repository that may own reusable workflows. pub const ORG_GITHUB_WORKFLOW_OWNER: &str = "ContextualWisdomLab/.github"; -const ORG_GITHUB_WORKFLOW_PREFIX: &str = - "ContextualWisdomLab/.github/.github/workflows/"; +const ORG_GITHUB_WORKFLOW_PREFIX: &str = "ContextualWisdomLab/.github/.github/workflows/"; /// Authority an org reusable workflow may hold. /// @@ -138,9 +137,9 @@ fn require_org_workflow_identity(workflow_ref: &str) -> Result<(), ApiError> { if workflow_file.is_empty() || workflow_file.contains('/') || workflow_file.contains("..") - || !workflow_file - .chars() - .all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.')) + || !workflow_file.chars().all(|character| { + character.is_ascii_alphanumeric() || matches!(character, '-' | '_' | '.') + }) || !(workflow_file.ends_with(".yml") || workflow_file.ends_with(".yaml")) { return Err(ApiError::AuthorizationDenied); diff --git a/crates/tepp_api/tests/org_github_contract.rs b/crates/tepp_api/tests/org_github_contract.rs index 67016413..dbe653b0 100644 --- a/crates/tepp_api/tests/org_github_contract.rs +++ b/crates/tepp_api/tests/org_github_contract.rs @@ -10,9 +10,8 @@ const PINNED_SHA: &str = "f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae"; #[test] fn reusable_workflow_binds_as_ci_review_security_only() { - let identity = format!( - "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@{PINNED_SHA}" - ); + let identity = + format!("ContextualWisdomLab/.github/.github/workflows/security-scan.yml@{PINNED_SHA}"); let binding = bind_org_github_workflow(&identity).expect("org reusable workflow"); assert_eq!( binding.contract_version(), From d5035300815b5a203e307245cd8d020f6ffe2f7b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 23:36:55 +0900 Subject: [PATCH 09/12] 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 bf821a8ce247e49d747e1b16fac56dbb23bec876 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:01:44 +0900 Subject: [PATCH 10/12] test: close stacked contract branch coverage --- crates/membership_core/src/network.rs | 42 +++++++++++++++++++++++++++ crates/tepp_api/src/org_github.rs | 27 +++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/crates/membership_core/src/network.rs b/crates/membership_core/src/network.rs index 24845bff..456fe4fc 100644 --- a/crates/membership_core/src/network.rs +++ b/crates/membership_core/src/network.rs @@ -242,6 +242,9 @@ mod tests { assert!(network.active_memberships_for(other, during).is_empty()); let active = network.active_memberships_for(member, during); assert_eq!(active.len(), 1); + assert_eq!(network.active_group_multiplicity(member, during), 1); + let active_weights = network.active_weight_by_role(member, during); + assert_eq!(active_weights.get(&MembershipRole::Template), Some(&1.0)); assert_eq!( active[0].validity().certainty(), temporal_core::TemporalCertainty::Bounded @@ -252,7 +255,46 @@ mod tests { assert!(network.active_weight_by_role(other, during).is_empty()); let rows = network.estimation_rows_at(member, during).expect("one row"); assert_eq!(rows.len(), 1); + assert_eq!( + network.estimation_rows_at(other, during), + Err(MembershipError::InvalidWirePayload) + ); + assert_eq!( + network.insert(active[0]), + Err(MembershipError::DuplicateMembershipAssignment) + ); + network + .insert( + MembershipAssignment::new( + other, + GroupId::new(), + MembershipRole::Project, + MembershipWeight::full().expect("full"), + start, + end, + ) + .expect("other assignment"), + ) + .expect("other insert"); super::refuse_atomistic_collapse(&rows, 1).expect("single membership"); + assert_eq!( + super::refuse_atomistic_collapse(&[], 1), + Err(MembershipError::InvalidWirePayload) + ); + assert_eq!( + super::refuse_atomistic_collapse(&rows, 2), + Err(MembershipError::AtomisticCollapseRefused) + ); + let mut mixed_rows = rows.clone(); + mixed_rows.extend( + network + .estimation_rows_at(other, during) + .expect("other row"), + ); + assert_eq!( + super::refuse_atomistic_collapse(&mixed_rows, 2), + Err(MembershipError::InvalidWirePayload) + ); assert_eq!(rows[0].member_id(), member); assert_eq!(rows[0].group_id(), group); assert_eq!(rows[0].role(), MembershipRole::Template); diff --git a/crates/tepp_api/src/org_github.rs b/crates/tepp_api/src/org_github.rs index 468dfc36..589ee7f5 100644 --- a/crates/tepp_api/src/org_github.rs +++ b/crates/tepp_api/src/org_github.rs @@ -183,6 +183,33 @@ mod tests { require_org_workflow_identity("ContextualWisdomLab/.github/readme.md"), Err(ApiError::AuthorizationDenied) ); + assert_eq!( + require_org_workflow_identity( + " ContextualWisdomLab/.github/.github/workflows/security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae" + ), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + require_org_workflow_identity( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml" + ), + Err(ApiError::AuthorizationDenied) + ); + for invalid in [ + "ContextualWisdomLab/.github/.github/workflows/@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/subdir/security-scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security-scan.txt@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security..yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security scan.yml@f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae", + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@main", + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@f070c504", + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@F070C504C1CB06891B800D7AB0CF6AC7D3CF8EAE", + ] { + assert_eq!( + require_org_workflow_identity(invalid), + Err(ApiError::AuthorizationDenied) + ); + } assert_eq!( require_org_workflow_identity(&format!( "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml@{SHA}" From 382e156959c2c6197c00a298510380e8ba093f36 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:54:08 +0900 Subject: [PATCH 11/12] fix(api): avoid workflow filename table false positives --- crates/tepp_api/src/org_github.rs | 15 +++++++++++---- crates/tepp_api/tests/org_github_contract.rs | 9 +++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/tepp_api/src/org_github.rs b/crates/tepp_api/src/org_github.rs index 589ee7f5..bd85e09a 100644 --- a/crates/tepp_api/src/org_github.rs +++ b/crates/tepp_api/src/org_github.rs @@ -92,11 +92,18 @@ pub fn refuse_check_conclusion_as_scientific_claim(conclusion: &str) -> Result<( /// [`ApiError::AuthorizationDenied`] for SQL, JDBC, or table hosts. pub fn refuse_org_workflow_table_access(target: &str) -> Result<(), ApiError> { require_nonempty(target)?; - let lowered = target.to_ascii_lowercase(); - if ["postgres", "jdbc", "sql", "tables"] + let lowered = target.trim().to_ascii_lowercase(); + let authority = lowered + .split_once("://") + .map_or(lowered.as_str(), |(_, remainder)| remainder); + let host = authority.split(['/', '?', '#']).next().unwrap_or_default(); + let blocked_scheme = ["jdbc:", "postgres://", "postgresql://"] .iter() - .any(|needle| lowered.contains(needle)) - { + .any(|scheme| lowered.starts_with(scheme)); + let blocked_host = host + .split('.') + .any(|label| matches!(label, "postgres" | "postgresql" | "sql" | "tables")); + if blocked_scheme || blocked_host { return Err(ApiError::AuthorizationDenied); } Ok(()) diff --git a/crates/tepp_api/tests/org_github_contract.rs b/crates/tepp_api/tests/org_github_contract.rs index dbe653b0..5bd0f47b 100644 --- a/crates/tepp_api/tests/org_github_contract.rs +++ b/crates/tepp_api/tests/org_github_contract.rs @@ -66,6 +66,15 @@ fn table_access_and_hostile_workflow_refs_fail_closed() { ); } +#[test] +fn legitimate_workflow_filenames_do_not_trigger_table_host_guard() { + for filename in ["graphql-lint.yml", "mysql-migrations.yml", "postgresql.yml"] { + let identity = + format!("ContextualWisdomLab/.github/.github/workflows/{filename}@{PINNED_SHA}"); + bind_org_github_workflow(&identity).expect("workflow filename is not a table host"); + } +} + #[test] fn review_agent_and_copilot_secrets_are_refused() { assert_eq!( From 345f26a4eadda8aa89e91468918ea1b0455400f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 02:14:33 +0900 Subject: [PATCH 12/12] docs(adr): preserve metadata line breaks --- docs/adr/0003-relational-event-multiple-membership.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0003-relational-event-multiple-membership.md b/docs/adr/0003-relational-event-multiple-membership.md index d31b8807..7d5d475c 100644 --- a/docs/adr/0003-relational-event-multiple-membership.md +++ b/docs/adr/0003-relational-event-multiple-membership.md @@ -1,8 +1,8 @@ # 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 -**Date:** 2026-08-05 +**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
+**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. ## Context