Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/required-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,16 @@ gates:
flip happens only after 2 independent green runs on dev, tracked as a follow-up. mode stays ADVISORY
until that flip lands (same PR as the flip, per schema-v3 rule: mode changes and branch-protection
changes land together)."

- name: "no-unguarded-git-subprocess"
mode: ADVISORY
producer_kind: local
workflow: validator-no-unguarded-git-subprocess.yml
job_path: ["no-unguarded-git-subprocess"]
skip_semantics: never
rationale: "OMN-14891: blocks reintroduction of git shell-outs in tests/ that inherit GIT_DIR / GIT_WORK_TREE
/ GIT_INDEX_FILE / GIT_COMMON_DIR from a git hook (those OVERRIDE cwd= and -C, so a tmp_path fixture
under the pre-push hook mutates the REAL invoking worktree -- observed twice, incl. 6707 phantom
staged deletions on the shared canonical clone). Runs unconditionally on every PR but is NOT YET
in required_status_checks; per the schema-v3 ordering rule the branch-protection flip and the mode
change to REQUIRED land together in a follow-up, after 2 independent green runs on dev."
124 changes: 124 additions & 0 deletions .github/workflows/validator-no-unguarded-git-subprocess.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT
---
# No Unguarded Git Subprocess — test git-environment isolation gate [OMN-14891]
#
# Canonical CI gate for `omnibase_core.validators.no_unguarded_git_subprocess`.
# Required status check context (once flipped): "no-unguarded-git-subprocess".
#
# The job id/name is deliberately unique ("no-unguarded-git-subprocess"), NOT the
# shared "validate" used by some of core's other validator workflows. GitHub
# surfaces the check-run context as the bare job name, so a shared name collides
# across unrelated workflows and cannot be made a required status check without
# forcing all of them. This mirrors validator-no-plugin-daemon-classes.yml
# (OMN-13309 / OMN-13284).
#
# THE DEFECT THIS GATES
# ---------------------
# Git exports GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE / GIT_COMMON_DIR into the
# environment of every hook it runs, and those variables OVERRIDE both `cwd=` and
# `git -C`. So when pytest runs as a subprocess of the repo's own pre-push hook, a
# fixture doing `subprocess.run(["git", "init"], cwd=tmp_path)` re-initialises the
# REAL invoking worktree instead of tmp_path.
#
# Observed twice, log-proven:
# * 2026-07-20 (OMN-14891) — the shared canonical clone's .git/config gained
# `core.bare = true` plus a bogus `[user] t / t@t.t` block; 6707 lines of
# phantom staged deletions entered the real index; an amend committed that
# index as a 6668-file / 1.4M-line deletion.
# * 2026-07-21 (OMN-14863) — recurred; fixture commits wrote f.txt / file.txt
# into the real index.
# `$OMNI_HOME/<repo>/.git/config` is shared by every worktree of that repo, so one
# session's test run corrupts every concurrent session in a sibling worktree.
#
# Rationale (Operating Rule #5, "enforcement not detection"): OMN-14744 fixed one
# file in omnibase_infra by hand and the class recurred in a different repo across
# five different files. The systemic remedy is the session-level os.environ scrub
# in tests/conftest.py; this workflow is what stops the shape being reintroduced.
# A pre-commit-only check is advisory — it does not block a PR that bypassed local
# hooks.
#
# Related: OMN-14892 (index corruption under the same full-suite run),
# OMN-14744 (the per-file precedent that did not hold).
#
# This workflow runs:
# 1. unit pytest for the validator (incl. its live self-scan of tests/)
# 2. pytest for the git-environment isolation regression (the RED-proven pair)
# 3. a standalone self-scan of the tests/ tree; must exit 0

name: No Unguarded Git Subprocess

on:
push:
branches: [main, develop, dev, "hotfix/**"]
pull_request:
branches: [main, develop, dev]

env:
PYTHON_VERSION: "3.12"
UV_VERSION: "0.8.3"

concurrency:
group: no-unguarded-git-subprocess-${{ github.ref }}
cancel-in-progress: true

jobs:
occ-preflight:
uses: ./.github/workflows/occ-preflight.yml
permissions:
contents: read
pull-requests: read
with:
contracts-dir: contracts
receipts-dir: drift/dod_receipts

no-unguarded-git-subprocess:
needs: occ-preflight
name: no-unguarded-git-subprocess
# OMNI_RUNNER_SELECTOR_V1 - trusted events default to self-hosted omnibase-ci; pull_request defaults to ubuntu-latest
runs-on: >-
${{
github.event_name != 'pull_request'
&& fromJSON(vars.OMNI_TRUSTED_CI_RUNS_ON_JSON || '["self-hosted","omnibase-ci"]')
|| fromJSON(vars.OMNI_PUBLIC_PR_RUNS_ON_JSON || '["ubuntu-latest"]')
}}
timeout-minutes: 10

steps:
- name: Checkout omnibase_core
uses: actions/checkout@v7

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: ${{ env.UV_VERSION }}

- name: Install dependencies
run: uv sync --all-extras

# The regression test shells out to git, so CI needs an identity for the
# decoy repos it creates in tmp_path.
- name: Configure git identity for fixture repos
run: |
git config --global user.email "ci@omninode.ai"
git config --global user.name "OmniNode CI"

- name: Run no-unguarded-git-subprocess unit tests
run: |
uv run pytest \
tests/unit/validators/test_no_unguarded_git_subprocess.py \
-v

- name: Run git-environment isolation regression
run: |
uv run pytest \
tests/unit/test_git_env_isolation.py \
-v

- name: Self-scan omnibase_core tests/
run: uv run python -m omnibase_core.validators.no_unguarded_git_subprocess tests/
17 changes: 17 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,23 @@ repos:
pass_filenames: false
stages: [pre-commit]

# OMN-14891: no test may shell out to git while able to inherit GIT_DIR /
# GIT_WORK_TREE / GIT_INDEX_FILE / GIT_COMMON_DIR from a git hook. Those
# variables OVERRIDE `cwd=` and `git -C`, so a tmp_path fixture running under
# the pre-push hook mutates the REAL invoking worktree — twice observed
# (core.bare=true + a bogus [user] block + 6707 phantom staged deletions on
# the shared canonical clone). The session scrub in tests/conftest.py is the
# remedy; this blocks reintroduction. Whole-tree arg so a partially-staged
# commit cannot hide a violation elsewhere.
- id: no-unguarded-git-subprocess
name: Block unguarded git subprocess calls in tests (OMN-14891)
entry: uv run python -m omnibase_core.validators.no_unguarded_git_subprocess tests
language: system
types: [python]
files: ^tests/.*\.py$
pass_filenames: false
stages: [pre-commit]

# OMN-14350: non-canonical lifecycle-word class ratchet (omnibase_core canary).
# Flags CamelCase classes carrying Service/Engine/Adapter/Router/Daemon/Manager/
# Runner/Registry/Controller/Server/Client and blocks any NEW one outside the
Expand Down
36 changes: 27 additions & 9 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -483,15 +483,15 @@
description: >
Reject forbidden I/O surfaces inside .py modules that sit in a NON-EFFECT node package: database-driver
imports (sqlite3, aiosqlite, psycopg, psycopg2, asyncpg, sqlalchemy), network/HTTP clients (httpx,
requests, aiohttp, urllib3, socket, urllib.request), subprocess execution, git invocation, Linear API
access, filesystem writes (open write-mode, os/shutil mutators, pathlib.Path write methods), and direct
event-bus / infra-adapter instantiation. Unlike the universal OMN-14659 arch lints, this rule is
archetype-keyed: the pure COMPUTE node (node_no_io_outside_effects_check_compute) keys off each node's
declared archetype (contract.yaml node_type / descriptor.node_archetype) and only scans the modules
co-located with a non-EFFECT contract — EFFECT is the only I/O-permitted archetype. The paired EFFECT
node (node_source_file_gather_effect) owns the filesystem walk for full-tree (no-filenames) invocation;
the same COMPUTE node backs this pre-commit hook and the CI gate. Only EFFECT nodes may perform I/O.
Suppress a proven-legitimate exception with: # io-ok: <reason>
requests, aiohttp, urllib3, socket, urllib.request), subprocess execution, git invocation, Linear
API access, filesystem writes (open write-mode, os/shutil mutators, pathlib.Path write methods), and
direct event-bus / infra-adapter instantiation. Unlike the universal OMN-14659 arch lints, this rule
is archetype-keyed: the pure COMPUTE node (node_no_io_outside_effects_check_compute) keys off each
node's declared archetype (contract.yaml node_type / descriptor.node_archetype) and only scans the
modules co-located with a non-EFFECT contract — EFFECT is the only I/O-permitted archetype. The paired
EFFECT node (node_source_file_gather_effect) owns the filesystem walk for full-tree (no-filenames)
invocation; the same COMPUTE node backs this pre-commit hook and the CI gate. Only EFFECT nodes may
perform I/O. Suppress a proven-legitimate exception with: # io-ok: <reason>
language: python
entry: python -m omnibase_core.nodes.node_no_io_outside_effects_check_compute.runtime_no_io_outside_effects_check
types_or: [python, yaml]
Expand Down Expand Up @@ -709,3 +709,21 @@
files: (^|/)(contract\.yaml|migrations/.*\.sql)$
pass_filenames: false
stages: [pre-commit]

- id: no-unguarded-git-subprocess
name: Block unguarded git subprocess calls in tests (OMN-14891)
description: >
OMN-14891: reject a subprocess call in tests/ whose argv starts with "git" unless it passes an env=
that provably drops GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY,
and GIT_ALTERNATE_OBJECT_DIRECTORIES. Git exports those into every hook environment and they OVERRIDE
both `cwd=` and `git -C`, so a tmp_path fixture running under a pre-push hook mutates the REAL invoking
worktree instead. `git config --global`/`--system` in tests/ is rejected outright. Consumers should
also install the session-level os.environ scrub in their top-level tests/conftest.py (see omnibase_core.validators.no_unguarded_git_subprocess.strip_git_location_env),
which is the primary remedy; this hook prevents reintroduction. Precedent: OMN-14744 (per-file fix
that did not hold).
language: system
entry: python -m omnibase_core.validators.no_unguarded_git_subprocess tests
types: [python]
files: ^tests/.*\.py$
pass_filenames: false
stages: [pre-commit]
17 changes: 16 additions & 1 deletion architecture-handshakes/pull-request-workflow-budget.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,22 @@ allowlisted_workflows:
# + justification + `retires` (the surface/manual step it removes, per the
# net-negative rule). A past-expiry waiver fails the gate. Empty at baseline —
# the sanctioned default is retire-one, not waive.
waivers: []
waivers:
- workflow_file: validator-no-unguarded-git-subprocess.yml
ticket: OMN-14891
expires: "2026-08-19"
justification: >-
Load-bearing git-environment-isolation gate blocking a twice-observed (2026-07-20 OMN-14891, 2026-07-21
OMN-14863) repo-corruption class: git hook-exported GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE override
cwd=/-C in test subprocesses and mutate the REAL shared canonical clone. The gate needs its own
unique check context ("no-unguarded-git-subprocess") ahead of the branch-protection required flip
tracked in .github/required-checks.yaml (ADVISORY until 2 independent green dev runs). Waived rather
than budget-bumped so the count lock stays flat; the slot regularizes when the OMN-14877 decorative-validator
drain frees allowlist slots (this file then takes a freed slot or migrates into ci.yml and the waiver
is removed).
retires: >-
The recurring manual post-incident .git/config + index forensics-and-repair step for hook-context
test corruption, and the per-file hand-fix pattern of OMN-14744 that demonstrably did not hold.

metadata:
generated_by: "C7 / OMN-14907, 2026-07-21"
Expand Down
6 changes: 6 additions & 0 deletions architecture-handshakes/standalone-validator-debt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ decorative_debt:
context: validate
- workflow_file: validator-no-raw-sqlite3.yml
context: validate
# OMN-14891 git-env-isolation gate: unique context, ADVISORY today (NOT in
# required_status_checks — verified 2026-07-22 via the gh api command above).
# Moves to natively_required_contexts when the required flip tracked in
# .github/required-checks.yaml lands (after 2 independent green dev runs).
- workflow_file: validator-no-unguarded-git-subprocess.yml
context: no-unguarded-git-subprocess
- workflow_file: validator-operation-match.yml
context: validate
- workflow_file: validator-pin-hygiene.yml
Expand Down
9 changes: 9 additions & 0 deletions scripts/validation/validate-file-locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ class FileLocationValidator:
# commit. Exempting runtime/ unblocks the fleet; whether the seam should
# instead relocate to mixins/ is an OMN-12549 call tracked as a follow-up.
"mixin_node_dispatch.py": ["runtime"],
# Single canonical ProtocolSemVer, co-located with the SemVer type it
# describes (OMN-14624, #1471). Same situation as mixin_node_dispatch.py
# above: it landed in types/ without an exemption, so the whole-tree
# file-location scan is RED on dev and blocks EVERY core src/ commit
# (this exemption is not this PR's subject — see OMN-14891 PR body).
# Exempting types/ unblocks the fleet; whether ProtocolSemVer should
# instead relocate to protocol/ is an OMN-14624 call tracked as a
# follow-up.
"type_semver.py": ["types"],
# Navigation subsystem data types (OMN-2540, OMN-2546)
"model_action_set.py": ["navigation"],
"model_backward_chaining.py": ["navigation"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2025 OmniNode.ai Inc.
# SPDX-License-Identifier: MIT

"""ModelGitSubprocessFinding — finding from the unguarded-git-subprocess guard (OMN-14891)."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True, slots=True)
class ModelGitSubprocessFinding:
"""An unguarded ``git`` subprocess invocation found in a test file."""

path: Path
line: int
column: int
call: str
reason: str

def format(self) -> str:
return (
f"{self.path}:{self.line}:{self.column + 1}: {self.call}(...) shells out to "
f"git without a scrubbed env ({self.reason})"
)
Loading
Loading