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..bd50b677 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). - `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..ab496220 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/membership_core/src/network.rs b/crates/membership_core/src/network.rs index 41b5fc11..456fe4fc 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. @@ -235,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 @@ -245,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/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, 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..bd85e09a --- /dev/null +++ b/crates/tepp_api/src/org_github.rs @@ -0,0 +1,283 @@ +//! 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"; + +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 +/// 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/…@sha`). + #[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 immutable org reusable workflow as CI/review/security control only. +/// +/// 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, 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.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.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(|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(()) +} + +/// 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_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" { + Ok(()) + } else { + Err(ApiError::AuthorizationDenied) + } +} + +fn require_org_workflow_identity(workflow_ref: &str) -> Result<(), ApiError> { + if workflow_ref.trim() != workflow_ref { + return Err(ApiError::AuthorizationDenied); + } + 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, '-' | '_' | '.') + }) + || !std::path::Path::new(workflow_file) + .extension() + .is_some_and(|extension| { + extension.eq_ignore_ascii_case("yml") || extension.eq_ignore_ascii_case("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(()) +} + +#[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; + + const SHA: &str = "f070c504c1cb06891b800d7ab0cf6ac7d3cf8eae"; + + #[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/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}" + )), + Err(ApiError::AuthorizationDenied) + ); + assert_eq!( + require_org_workflow_identity(&format!( + "ContextualWisdomLab/.github/.github/workflows/recovery-claim.yml@{SHA}" + )), + Err(ApiError::AuthorizationDenied) + ); + require_org_workflow_identity(&format!( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yml@{SHA}" + )) + .expect("org workflow"); + require_org_workflow_identity(&format!( + "ContextualWisdomLab/.github/.github/workflows/security-scan.yaml@{SHA}" + )) + .expect("yaml org workflow"); + assert_eq!( + refuse_org_workflow_secret("AWS_SECRET"), + Err(ApiError::AuthorizationDenied) + ); + 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(&format!( + "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@{SHA}" + )) + .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(&format!( + "ContextualWisdomLab/.github/.github/workflows/scientific-acceptance.yml@{SHA}" + )), + 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..5bd0f47b --- /dev/null +++ b/crates/tepp_api/tests/org_github_contract.rs @@ -0,0 +1,97 @@ +//! 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, +}; + +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 binding = bind_org_github_workflow(&identity).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() + .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"), + 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 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!( + 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/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}" + ); + } +} 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 7f14515e..d8516b24 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/0003-relational-event-multiple-membership.md b/docs/adr/0003-relational-event-multiple-membership.md index 71cacdd2..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 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 020d0083..29e16427 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 9b9ef94b..30560a33 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 | +| 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 |