Skip to content
Open
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
58 changes: 58 additions & 0 deletions .github/workflows/strix-changed-path-quality-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Strix Changed Path Quality CI

on:
pull_request:
branches: [main]
paths:
- ".github/workflows/strix-changed-path-quality-ci.yml"
- "CHANGELOG.md"
- "docs/doctoring/strix-legal-git-paths.md"
- "scripts/ci/strix_quick_gate.sh"
- "tests/test_strix_changed_path_policy.py"
Comment thread
seonghobae marked this conversation as resolved.
- "tests/test_strix_workflow_dependency_hashes.py"

permissions:
contents: read

concurrency:
group: strix-changed-path-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
exact-head-path-policy:
if: github.event_name != 'pull_request' || github.event.action != 'closed'
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout exact source revision
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"

- name: Install exact hash-verified test runner dependencies
env:
PIP_DISABLE_PIP_VERSION_CHECK: "1"
PIP_NO_INPUT: "1"
run: >-
python -m pip install --only-binary=:all: --require-hashes
coverage==7.15.2 --hash=sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f
iniconfig==2.1.0 --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760
packaging==26.2 --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
pluggy==1.6.0 --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
pygments==2.20.0 --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
pytest==9.1.1 --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c

- name: Verify exact-head path policy and syntax
shell: bash --noprofile --norc -e -o pipefail {0}
run: |
test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha || github.sha }}"
python -m coverage run -m pytest tests -q
python -m compileall -q tests/test_strix_changed_path_policy.py tests/test_strix_workflow_dependency_hashes.py
bash -n scripts/ci/strix_quick_gate.sh
git diff --exit-code
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed.
- Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision.
- Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities.
116 changes: 116 additions & 0 deletions docs/doctoring/strix-legal-git-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Strix legal Git path compatibility

## Incident and buyer impact

The organization-required Strix quick gate rejected the exact changed-file list
for `ContextualWisdomLab/aFIPC#160` at head
`804ea97cd83144f94c5020a9d42f2573cc8cb442`. The pull request deletes generated
Packrat artifacts, including the tracked fixture path
`Ugly, but legal, path for a project (long)`. The central gate classified that
path as unsafe solely because its comma and parentheses were absent from the
bounded ASCII allowlist. Security analysis therefore stopped before examining
the pull request, leaving a valid supply-chain cleanup without exact-head Strix
evidence.

## Decision

The normalizer now admits comma and ASCII parentheses. No other punctuation is
broadened. Existing fail-closed controls remain authoritative:

- empty, dot, absolute, leading/trailing-whitespace, NUL, CR, LF, and
backslash forms are rejected;
- raw `..` components are rejected before `posixpath.normpath()` can collapse
an embedded traversal such as `safe/../target.txt`;
- shell metacharacters such as semicolon, dollar sign, backtick, pipe, and
ampersand remain rejected;
- only the existing Unicode letter, combining-mark, and number categories are
accepted outside ASCII;
- `Path.resolve(strict=False)` followed by `relative_to()` proves containment
beneath the trusted repository root; and
- downstream Git and filesystem operations receive normalized paths as quoted
arguments, never as executable shell source.

This is a compatibility correction, not a general relaxation to every pathname
byte Git can represent. The privileged scanner intentionally retains a smaller,
audited path policy.

## Test-first evidence

`tests/test_strix_changed_path_policy.py` extracts and executes the exact Python
normalizer embedded in `scripts/ci/strix_quick_gate.sh`. The materializer first
requires the historical Packrat fixture regression to fail on protected main,
then applies the narrow allowlist change and requires the same test to pass.
A test-only exact-head commit first demonstrated that `safe/../target.txt`
passed after normalization; the production repair now rejects its raw `..`
component before normalization. Permanent tests preserve established punctuation
and reject traversal, absolute paths, controls, whitespace ambiguity, backslashes,
and representative shell punctuation. The dedicated workflow runs the complete
repository test suite through coverage.py and pytest whenever code or either
authoritative contract document changes.

## Workflow dependency integrity

The exact-head policy workflow downloads its Python test runner from PyPI, so
version pins alone are insufficient: a compromised index response or replaced
artifact could otherwise change executable CI code without a repository diff.
The workflow therefore uses pip hash-checking mode (`--require-hashes`) together
with `--only-binary=:all:` and the exact SHA-256 digest of every wheel selected
on the fixed `ubuntu-24.04` x86-64 / CPython 3.14 runner:

- coverage 7.15.2: `b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f`;
- iniconfig 2.1.0: `9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760`;
- packaging 26.2: `5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e`;
- pluggy 1.6.0: `e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746`;
- Pygments 2.20.0: `81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176`;
- pytest 9.1.1: `37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c`.

`tests/test_strix_workflow_dependency_hashes.py` was committed before the
workflow implementation and fails against the preceding exact head because
hash-checking mode and its trigger path are absent. It is now part of the
workflow's own path filter and verifies every requirement/digest pair. Any
package or runner-platform change must update the package version, PyPI wheel
digest, regression contract, and this record together. A digest mismatch must
fail closed; do not disable hash checking to restore availability.

## Rollback and incident response

Roll back the allowlist and regression together only if a downstream call is
proven to evaluate normalized paths as shell source. Until that defect is fixed,
fail Strix closed and retain the offending path, workflow run, and commit SHA as
incident evidence. Do not bypass the required security check.

If an exact dependency wheel becomes unavailable, first verify the release and
artifact digest against PyPI's file record and provenance. A rollback may select
the last known-good fully versioned wheel only when its exact hash is recorded in
the workflow, regression contract, and this document. Never replace
`--require-hashes` with an unhashed install.

## References

Batchelder, N., & contributors. (2026). *coverage.py 7.15.2* [Computer
software]. Python Package Index. https://pypi.org/project/coverage/7.15.2/

Git Project. (2026). *Git index format*. https://git-scm.com/docs/index-format

Git Project. (2026). *git-ls-tree documentation*. https://git-scm.com/docs/git-ls-tree

Python Packaging Authority. (2026). *Secure installs*. pip documentation.
https://pip.pypa.io/en/stable/topics/secure-installs/

Python Software Foundation. (2026). *pathlib—Object-oriented filesystem paths
(Python 3.14.6 documentation)*. https://docs.python.org/3.14/library/pathlib.html

pytest development team. (2025). *iniconfig 2.1.0* [Computer software]. Python
Package Index. https://pypi.org/project/iniconfig/2.1.0/

pytest development team. (2025). *pluggy 1.6.0* [Computer software]. Python
Package Index. https://pypi.org/project/pluggy/1.6.0/

pytest development team. (2026). *pytest 9.1.1* [Computer software]. Python
Package Index. https://pypi.org/project/pytest/9.1.1/

Python Packaging Authority. (2026). *packaging 26.2* [Computer software].
Python Package Index. https://pypi.org/project/packaging/26.2/

Pygments contributors. (2026). *Pygments 2.20.0* [Computer software]. Python
Package Index. https://pypi.org/project/Pygments/2.20.0/
17 changes: 11 additions & 6 deletions scripts/ci/strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -406,16 +406,21 @@ if "\x00" in relative_path_str:
raise SystemExit(1)
if "\\" in relative_path_str:
raise SystemExit(1)
if any(component == ".." for component in relative_path_str.split("/")):
raise SystemExit(1)
normalized = posixpath.normpath(relative_path_str)
if normalized in (".", "") or normalized.startswith("../") or normalized == "..":
raise SystemExit(1)
# '@' is required for Apple/Tauri retina asset names (128x128@2x.png) and '+'
# for SvelteKit's mandatory route files (+page.svelte, +layout.ts). Preserve
# the existing ASCII allowlist and additionally accept only Unicode letters,
# combining marks, and numbers. This supports internationalized repository
# paths without admitting controls, separators, bidi formatting, shell
# metacharacters, or Unicode punctuation that could resemble a path boundary.
allowed_ascii = frozenset("_.@+/ []-")
# for SvelteKit's mandatory route files (+page.svelte, +layout.ts). Commas and
# parentheses are ordinary Git filename characters used by historical Packrat
# fixtures. They remain data because downstream filesystem and Git calls pass
# the normalized path as a quoted argument rather than executable shell source.
# Preserve the bounded ASCII allowlist and additionally accept only Unicode
# letters, combining marks, and numbers. This supports internationalized
# repository paths without admitting controls, separators, bidi formatting,
# shell metacharacters, or Unicode punctuation resembling a path boundary.
allowed_ascii = frozenset("_.@+/ [],()-")
if not all(
(character.isascii() and (character.isalnum() or character in allowed_ascii))
or (
Expand Down
94 changes: 94 additions & 0 deletions tests/test_strix_changed_path_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Regression tests for the production Strix changed-path normalizer."""

from __future__ import annotations

import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
GATE_SCRIPT = REPOSITORY_ROOT / "scripts" / "ci" / "strix_quick_gate.sh"
START_MARKER = 'python3 - "$REPO_ROOT" "$changed_file" <<\'PY\'\n'
END_MARKER = "\nPY\n}\n\nnormalize_changed_files_cache()"
LEGAL_PACKRAT_PATH = (
"packrat/lib/x86_64-pc-linux-gnu/3.4.1/packrat/tests/testthat/"
"Ugly, but legal, path for a project (long)/bread/DESCRIPTION"
)


def _normalizer_source() -> str:
"""Return the exact embedded Python program used in production."""

gate_source = GATE_SCRIPT.read_text(encoding="utf-8")
prefix, separator, remainder = gate_source.partition(START_MARKER)
if not separator or not prefix:
raise AssertionError("Strix changed-path normalizer start marker is missing")
source, separator, _suffix = remainder.partition(END_MARKER)
if not separator:
raise AssertionError("Strix changed-path normalizer end marker is missing")
return source


def _normalize(candidate: str) -> subprocess.CompletedProcess[str]:
"""Execute the production normalizer with an isolated repository root."""

with tempfile.TemporaryDirectory() as temporary_directory:
return subprocess.run(
[sys.executable, "-c", _normalizer_source(), temporary_directory, candidate],
check=False,
capture_output=True,
text=True,
)


class StrixChangedPathPolicyTests(unittest.TestCase):
"""Verify legal Git paths and fail-closed path boundaries."""

def test_accepts_historical_packrat_fixture_path(self) -> None:
"""A tracked Packrat fixture with commas and parentheses is valid input."""

result = _normalize(LEGAL_PACKRAT_PATH)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), LEGAL_PACKRAT_PATH)

def test_preserves_existing_supported_punctuation(self) -> None:
"""Existing bracket, at-sign, plus-sign, space, and hyphen support remains."""

candidate = "ui/[slug]/128x128@2x +page-safe/file-name.ts"
result = _normalize(candidate)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), candidate)

def test_rejects_traversal_absolute_controls_and_shell_punctuation(self) -> None:
"""The repair does not admit traversal, controls, or shell syntax."""

rejected = (
"",
".",
"..",
"../secret.txt",
"safe/../target.txt",
"/tmp/secret.txt",
"safe\\escape.txt",
"safe\nname.txt",
"safe\rname.txt",
" leading.txt",
"trailing.txt ",
"safe;command.txt",
"safe$(command).txt",
"safe`command`.txt",
"safe|command.txt",
"safe&command.txt",
)
Comment thread
seonghobae marked this conversation as resolved.
for candidate in rejected:
with self.subTest(candidate=repr(candidate)):
result = _normalize(candidate)
self.assertNotEqual(result.returncode, 0)
self.assertEqual(result.stdout, "")


if __name__ == "__main__":
unittest.main()
32 changes: 32 additions & 0 deletions tests/test_strix_workflow_dependency_hashes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Supply-chain contracts for the Strix changed-path policy workflow."""

from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
WORKFLOW = ROOT / ".github" / "workflows" / "strix-changed-path-quality-ci.yml"
EXPECTED_WHEEL_HASHES = {
"coverage==7.15.2": "b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f",
"iniconfig==2.1.0": "9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760",
"packaging==26.2": "5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e",
"pluggy==1.6.0": "e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746",
"pygments==2.20.0": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176",
"pytest==9.1.1": "37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c",
}


def test_strix_workflow_installs_only_hash_verified_wheels() -> None:
"""Every network-installed test dependency is versioned and hash verified."""
workflow = WORKFLOW.read_text(encoding="utf-8")

assert "--only-binary=:all:" in workflow
assert "--require-hashes" in workflow
for requirement, digest in EXPECTED_WHEEL_HASHES.items():
assert f"{requirement} --hash=sha256:{digest}" in workflow


def test_strix_workflow_reruns_when_hash_contract_changes() -> None:
"""Changing this regression contract must trigger the exact-head workflow."""
workflow = WORKFLOW.read_text(encoding="utf-8")

assert ' - "tests/test_strix_workflow_dependency_hashes.py"' in workflow