Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
26 changes: 4 additions & 22 deletions .github/workflows/python-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,25 +232,7 @@ jobs:
- name: Run pip-audit (hard gate on any known vulnerability)
run: |
set -euo pipefail
status=0

# Audit every discovered requirements file.
while IFS= read -r req; do
echo "::group::pip-audit -r ${req}"
pip-audit --strict --desc=on -r "${req}" || status=1
echo "::endgroup::"
done < <(find . -type f -name 'requirements*.txt' -not -path './.git/*')

# Audit the project itself when a PEP 621 / lock manifest exists.
if find . -maxdepth 2 -type f \
\( -name 'pyproject.toml' -o -name 'pylock.*.toml' \) \
-not -path './.git/*' | head -1 | grep -q .; then
echo "::group::pip-audit . (project manifest)"
pip-audit --strict --desc=on . || status=1
echo "::endgroup::"
fi

if [ "${status}" != "0" ]; then
echo "::error::pip-audit reported known-vulnerable Python dependencies."
exit 1
fi
# Hashed complete locks are audited with --disable-pip so pip cannot
# re-apply a stale Requires-Dist bound and label ResolutionImpossible
# as a known vulnerability (ContextualWisdomLab/.github#961).
python3 scripts/ci/pip_audit_requirements.py
8 changes: 7 additions & 1 deletion .github/workflows/strix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,13 @@ jobs:
# private install umask before creating the credential-bearing Strix
# entry point; the runtime gate still rejects any later relaxation.
umask 022
python3 -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-strix-ci-hashes.txt
# The compiled lock is the complete closed set. --no-deps is required
# so pip does not re-apply a stale Requires-Dist bound (strix-agent
# still declares cryptography<49) after a CVE-fixed cryptography pin
# was resolved at compile time. pull_request_target runs this file
# from the base branch, so this flag must land on main before a
# lock that needs it can install.
python3 -m pip install --disable-pip-version-check --no-cache-dir --require-hashes --no-deps -r requirements-strix-ci-hashes.txt
strix_executable="$(command -v strix || true)"
if [ -z "$strix_executable" ] || [[ "$strix_executable" != /* ]] \
|| [ ! -f "$strix_executable" ] || [ -L "$strix_executable" ] \
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__/
.coverage
.pytest_cache/
.codegraph/
.venv/
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

<!-- CWL-ENTRY -->
> **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** <https://github.com/orgs/ContextualWisdomLab/projects/1> (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth.
>
> Required Strix (`pull_request_target`) installs the hashed lock with `--require-hashes --no-deps`. Hashed-lock pip-audit uses `--disable-pip` via `scripts/ci/pip_audit_requirements.py` only when every package line carries a `--hash=` pin. A lone `--require-hashes` directive or a mixed hashed-plus-unhashed file is not a complete lock. Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`). Do not drop `cryptography==50.0.0` to satisfy strix-agent's stale `<49` bound. See [`docs/doctoring/strix-hashed-lock-no-deps.md`](docs/doctoring/strix-hashed-lock-no-deps.md) and [`ARCHITECTURE.md`](ARCHITECTURE.md).
53 changes: 53 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Architecture — org-central `.github` control plane

This repository is the ContextualWisdomLab organization profile, the central
PR-governance / required-workflow hub, and the Cloudflare DNS/Pages
declaration. The live work tracker is GitHub Project #1.

## Trust boundary: required Strix vs head-tree locks

```mermaid
flowchart LR
PR["PR head tree<br/>requirements-strix-ci-hashes.txt"]
BASE["Protected main<br/>.github/workflows/strix.yml"]
PIP["pip install --require-hashes --no-deps"]
AUDIT["python-security.yml<br/>pip_audit_requirements.py --disable-pip"]
GATE["strix_quick_gate.sh<br/>artifact fail-closed"]

PR -->|"pull_request_target checks out head"| PIP
BASE -->|"workflow YAML comes from base"| PIP
PIP --> GATE
PR -->|"pull_request uses the PR workflow"| AUDIT
```

A `pull_request_target` job cannot honor an installer flag that exists only on
the pull request. `--no-deps` must therefore merge to `main` before a lock
that needs it (strix-agent 1.5.3 + cryptography 50.0.0) can install. pip-audit
is `pull_request`, so hashed-lock `--disable-pip` takes effect on the same
head. Decision record:
[`docs/doctoring/strix-hashed-lock-no-deps.md`](docs/doctoring/strix-hashed-lock-no-deps.md).

## Trusted-uv installer platform gate

```mermaid
flowchart TD
Host["Developer or CI host"]
Gate{"sys.platform is linux and machine is x86_64?"}
Install["Verify pinned archive, version, and cache"]
Reject["Fail closed: unsupported runner"]

Host --> Gate
Gate -->|"no"| Reject
Gate -->|"yes"| Install
```

Installer verification tests simulate Linux x86_64 and clear the process cache
(CWE-670) so they measure verification rather than the host-architecture gate.

## Other control-plane surfaces

- OpenCode / Noema are reviewers (`edit: deny`); GitHub Actions performs
mechanical updates and merges.
- Review JSON embedded in Markdown is escaped by
`scripts/ci/opencode_review_normalize_output.py`.
- Cloudflare reconcile is dry-run by default; PRs never see the API token.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context.
- Required every package line to carry a `--hash=` pin before pip-audit treats a requirements file as a complete hashed lock. A lone `--require-hashes` directive or a mixed hashed-plus-unhashed file no longer receives `--disable-pip`, which would otherwise audit an incomplete set as clean.
- Landed `--require-hashes --no-deps` on the required Strix installer and taught pip-audit to audit hashed complete locks with `--disable-pip`, so a later strix-agent 1.5.3 + cryptography 50.0.0 lock can install under `pull_request_target` and pip-audit no longer labels pip `ResolutionImpossible` as a known vulnerability (ContextualWisdomLab/.github#961, #952). A `*-hashes.txt` name without hash evidence no longer receives `--disable-pip`, and discovery skips virtualenv trees.
- Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics.
- 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 each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped.
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ interrogate
## Hash-pinned requirements discipline

CI installs Python tools only with `pip install --require-hashes` from the `*-hashes.txt` files.
The required Strix installer adds `--no-deps` so pip cannot re-apply a stale
`Requires-Dist` bound after a compile-time cryptography override. `python-security.yml`
audits those hashed locks through `scripts/ci/pip_audit_requirements.py` (`pip-audit
--disable-pip`). See `docs/doctoring/strix-hashed-lock-no-deps.md`.
Never hand-edit a `-hashes.txt` file: edit the top-level `requirements-<tool>-ci.txt` input, then
regenerate with the exact `uv pip compile` command recorded in the hashes file's header comment,
e.g.:
Expand Down
86 changes: 86 additions & 0 deletions docs/doctoring/strix-hashed-lock-no-deps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Strix hashed-lock install and pip-audit without pip re-resolution

검토 기준일: **2026-08-13**

## Incident

The required Strix workflow is `pull_request_target`: GitHub runs the **base
branch** copy of `.github/workflows/strix.yml` against the pull-request head
tree. ContextualWisdomLab/.github#961 therefore compiled a complete lock of
`strix-agent==1.5.3` plus `cryptography==50.0.0` (override for CVE-2026-39892
and CVE-2026-69247) and added `--no-deps` to the PR copy of the installer, but
the live required job still executed main's installer:

```text
pip install --require-hashes -r requirements-strix-ci-hashes.txt
```

pip re-applied `strix-agent 1.5.3 depends on cryptography<49 and >=48.0.1`
and exited `ResolutionImpossible`. The same resolver path is what
`python-security.yml` used for every `requirements*.txt` file. pip-audit then
printed `::error::pip-audit reported known-vulnerable Python dependencies`
even though no advisory was returned. A buyer watching the required security
dashboard saw two red X marks on an honest lock.

## Decision

1. Land `--require-hashes --no-deps` on **main's** Strix installer first, with
no lock change. The current main lock (`strix-agent==1.0.4` +
`cryptography==50.0.0`) is already a complete hashed set, so `--no-deps`
does not widen the install. After this lands, a later 1.5.3 lock can
install under the required base-branch workflow.
2. Audit hashed locks with `pip-audit --disable-pip` via
`scripts/ci/pip_audit_requirements.py`. Compile-time `*-overrides.txt`
files and unhashed inputs that already have a `*-hashes.txt` sibling are
not separate install sets.
3. Do not drop `cryptography==50.0.0` to satisfy the stale `<49` metadata
bound. Do not treat `ResolutionImpossible` as a vulnerability.

`python-security.yml` is `pull_request` (not `_target`), so the helper takes
effect on the same head that introduces it. Strix still needs this installer
line on the protected base before #961 can go green.

## Trust boundary

- `--no-deps` is not an unhashed install: every wheel remains hash-pinned.
- `--disable-pip` still queries the advisory database for every pinned name
and version; it only skips pip's metadata resolver.
- NVIDIA NIM / OpenCode review-agent credentials are untouched.
- No operational PII is masked.

## Verification contract

`tests/test_pip_audit_requirements.py` reconstructs the #961 lock shape and
requires `--disable-pip` for that file, a skip for the override/input pair,
and the `--no-deps` installer line on `strix.yml`. A `*-hashes.txt` name
or a lone `--require-hashes` directive without `--hash=` is not treated
as a complete lock. A mixed file with one hashed line beside unhashed
packages also stays on the resolver path. Discovery skips `.venv` trees.
Materialize accepts a requirements include only as a two-token
``-r``/``--requirement`` form whose target is a normalized relative POSIX
lock path with no ``.`` or ``..`` components, so a dotted include cannot
enter the trusted build context (CWE-22; MITRE, 2026).

## References (APA 7th)

MITRE. (2026). *CWE-22: Improper limitation of a pathname to a restricted
directory ('Path Traversal')*. https://cwe.mitre.org/data/definitions/22.html

GitHub. (2026). *Cryptography vulnerable to buffer overflow if
non-contiguous buffers were passed to APIs (CVE-2026-39892,
GHSA-p423-j2cm-9vmq)*. GitHub Advisory Database.
https://github.com/advisories/GHSA-p423-j2cm-9vmq

GitHub. (2026). *PKCS#7 decryption timing oracle in pyca/cryptography
(CVE-2026-69247, GHSA-g6cj-pr64-35w5)*. GitHub Advisory Database.
https://github.com/advisories/GHSA-g6cj-pr64-35w5

National Institute of Standards and Technology. (2026).
*CVE-2026-69247*. National Vulnerability Database.
https://nvd.nist.gov/vuln/detail/CVE-2026-69247

pypa. (2025). *pip-audit: ``--disable-pip`` (hashed requirements / ``--no-deps``
only)*. https://github.com/pypa/pip-audit

Python Packaging Authority. (n.d.). *Hash-checking mode*. pip documentation.
https://pip.pypa.io/en/stable/topics/secure-installs/#hash-checking-mode
82 changes: 68 additions & 14 deletions scripts/ci/materialize_base_python_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,57 @@ def _is_candidate_lock_name(name: str) -> bool:
)


def _is_candidate_lock_path(path: pathlib.PurePosixPath) -> bool:
"""Return whether one safe tracked path can name a pip requirements lock.

In addition to conventional ``requirements*.txt`` names, repositories often
keep concrete environment closures as direct children such as
``requirements/ci.txt`` or ``service/requirements/package.txt``. Only direct
``.txt`` children of a directory named ``requirements`` gain this path-based
eligibility; content must still pass the independent complete hash-pin
validation before it reaches the trusted image build context.
"""
return _is_candidate_lock_name(path.name) or (
path.suffix == ".txt" and path.parent.name == "requirements"
)


def _is_bounded_requirement_include(line: str) -> bool:
"""Return whether one requirements include names a bounded relative file.

Includes are accepted only as a two-token ``-r``/``--requirement`` form
whose target is itself a candidate lock path written as a normalized
relative POSIX path. Absolute paths, ``.`` or ``..`` components, double
slashes, URLs, option-like targets, shell/Windows path separators,
fragments, queries, extra inline options or hashes, and includes of
non-lock files are rejected before a base-owned file can enter the
trusted build context.
The downstream installer still proves that the candidate is an independently
complete hash closure; this predicate grants syntax eligibility only.
"""
fields = line.split()
if len(fields) != 2 or fields[0] not in {"-r", "--requirement"}:
return False
target = fields[1]
if (
target.startswith(("-", "~"))
or "\\" in target
or ":" in target
or "?" in target
or "#" in target
):
return False
include_path = pathlib.PurePosixPath(target)
return (
bool(include_path.parts)
and target == include_path.as_posix()
and not include_path.is_absolute()
and "." not in include_path.parts
and ".." not in include_path.parts
and _is_candidate_lock_path(include_path)
)


def _requirement_lines(content: bytes) -> list[str]:
"""Return logical requirement lines, joining backslash line-continuations.

Expand All @@ -107,23 +158,26 @@ def _requirement_lines(content: bytes) -> list[str]:


def _is_hash_pinned(content: bytes) -> bool:
"""Return whether content carries hash pins and is safe to preflight.

Discovery is content-based rather than name-based so hash-pinned locks in any
location (a service subdirectory, ``requirements-dev.txt``,
``requirements-test.txt``) can be considered for offline coverage, while an
unpinned or PR-mutable requirements file is still excluded from the networked
build context. Hash syntax cannot prove that a file includes every transitive
dependency, so the trusted image installer separately preflights every
candidate as an independent ``--require-hashes`` closure. An empty file
carries no installable dependency and is not materialized.
"""Return whether content carries only trusted pins or bounded includes.

Discovery is content-based rather than name-based so exact hash-pinned locks
in service subdirectories and role-specific requirements files can be
considered for offline coverage. Candidate syntax is deliberately stricter
than a substring search: each package line must be an exact ``==`` pin with
one or more complete SHA-256 hashes, or a bounded relative requirements
include. A global ``--require-hashes`` directive is not trust evidence by
itself. The downstream installer separately preflights every candidate as an
independent ``pip --require-hashes`` closure, so syntax eligibility never
substitutes for dependency-closure proof.
"""
lines = _requirement_lines(content)
if not lines:
requirement_lines = [line for line in lines if line != "--require-hashes"]
if not requirement_lines:
return False
return any(line == "--require-hashes" for line in lines) or all(
"--hash=" in line or line.startswith(("-r ", "--requirement "))
for line in lines
return all(
_is_fully_hash_pinned_requirement(line)
or _is_bounded_requirement_include(line)
for line in requirement_lines
)


Expand Down
Loading
Loading