Skip to content

Codifide Harness 3.1.0

Choose a tag to compare

@codifide codifide released this 11 Nov 21:54
· 13 commits to trunk since this release

Security and distribution hardening of the enforcement layer. Driven by a read-only assessment and a Grok 4.7 cold adversarial review; every finding is either fixed with a regression test or stated as a documented limit in templates/enforcement/README.md.

Changed (behaviour you may notice)

  • check-immutable-paths.sh: the verifier approval (CODIFIDE_VERIFIER_CHANGE_APPROVED_BY) no longer waives edits to .codifide/, .github/workflows/, CODEOWNERS, .kiro/hooks/ or .claude/settings.json. Those need CODIFIDE_ENFORCER_CHANGE_APPROVED_BY.
  • check-licenses.sh: reads the policy from the base branch (--base REF, CODIFIDE_BASE, or origin/main). Falls back to the working tree with a warning only when no base ref exists and none was requested. Pass --base in CI. CycloneDX entries with several licenses are read as AND. OR now means either side may be chosen.
  • check-review-level.sh: docs/ files are R1 only with a document extension; binary files count 100 lines each toward the size rule; more migration, schema, Terraform, session, oauth, jwt, password, login, middleware, rbac, acl, iam and policy paths raise the floor; matching is case-insensitive.
  • Guard: empty input and an unreadable list now block.

Fixed

  • Guard allowed a case variant of a protected path (Verifiers/check.py) on case-insensitive filesystems.
  • Guard dropped any string of 1,024 characters or more, so a long a/../ path reached a protected file.
  • Guard dropped a path ending in a newline.
  • Guard did not resolve hard links to protected files.
  • Any unexpected guard exit code other than 2 was a possible "allow"; an EXIT trap now converts it to 2, and the hook files run bash <guard> || exit 2.
  • CI check missed protected paths with non-ASCII or newline characters in the name (git quotes them), a file named like a protected directory, case variants, and symlinks added into protected paths.
  • License check: policy came from the PR's own tree; * in a license expression expanded to file names; a policy line of * allowed everything; MIT License and similar names were rejected; A OR B was treated as AND; SPDX tag-value expressions were truncated to the first word.
  • Review-level check: wrong file names for git-quoted paths; R1 allowlist accepted code under docs/.
  • Guard resolved .. before symlinks (src/link/../check.py), judged the target of a symlink instead of its own name (deleting verifiers/link.py), ignored a hard link whose other name lives outside the repo, allowed a recursive delete of an ancestor of a protected directory, and dropped any string with an interior newline. Symlinks are now resolved component by component and every candidate is checked three ways; interior-newline and NUL-truncated strings and JSON object keys are checked too.
  • CI immutable check: a workflow or other enforcement-path edit was not reported unless the list named it; a symlink to another symlink (or through a directory symlink) escaped the one-hop check. The tree is now followed across any number of hops.
  • Review-level check: a root commit (orphan history merged in) was never checked (git diff-tree --root).
  • License check: nested CycloneDX components were skipped; partial parser output was accepted; an SPDX package with no license line was dropped; MIT WITH with no exception id was accepted.
  • Iteration 3 review (gpt-6.1-sol), all with regression tests: a symlink into .codifide/ was waived by the verifier approval (now classified by its target); a symlink target differing only in case from a directory symlink escaped the CI check; NFD spellings of a protected NFC file name passed the guard; a hard link to a newline-named protected file passed (find output was newline-split); license entries were joined without parentheses (MIT OR GPL AND GPL passed); a License value on a later csv row was dropped as a header; an SPDX Concluded license was ignored after Declared; a merge resolved with --ours had an empty combined diff and so an R1 floor; an approval marker inside a file containing a NUL byte was not scanned; the example workflow had the license step commented out.
  • Iteration 4 review (gpt-6.1-sol), all with regression tests: a relative path was resolved against the repository root, not the hook's working directory (check.py from verifiers/ passed), so relative paths are now judged against the root, the hook cwd and the payload cwd, and the hook metadata cwd is no longer treated as a write target; CODIFIDE_ROOT could point the guard at a decoy list, so a root that is not the real top level now blocks; an NFD alias of a protected NFC name passed the CI check on git that does not precompose, so non-ASCII changed paths are matched in both forms; a plain-list license row with a missing license was dropped, so it is now a malformed report (exit 2) and SPDX tag-value lines are no longer read as list rows.
  • Iteration 5 review (gpt-6.1-sol), all with regression tests: the guard honoured GIT_DIR/GIT_WORK_TREE, so a decoy work tree chose the list that was read (all GIT_* variables are now dropped); a delete of / was allowed (an ancestor of the repository is now blocked); a payload cwd such as src/link/.. was normalised before the symlink was resolved; docs/CODEOWNERS was not an enforcer path; .claude/, .cursor/ and .codex/ agent settings could be changed at R2 (now governance, R3); CycloneDX metadata.component.components was not scanned; a plain-list row with an empty package name was dropped; a plain row mixed into an SPDX tag-value report was ignored (now exit 2).
  • Iteration 6 (a bounded fix pass after the fifth gpt-6.1-sol review; these fixes are not re-reviewed), all with regression tests: the three CI scripts now drop every GIT_* variable, so GIT_DIR/GIT_WORK_TREE cannot point them at a decoy repository; a symlink target or file name ending in a newline lost the newline in $(...) (guard and CI), so such a link bypassed a protected *? pattern (results now travel in variables, readlink -n, sentinel for decoded values); the CI symlink traversal now folds NFC/NFD spellings of a path component (fail closed without python3); the guard's hard-link and directory scans block when find cannot read part of the repository; an SPDX <text> comment could overwrite a PackageLicenseConcluded/Declared value; CycloneDX nested components that is not an array was silently skipped (now exit 2); a symlink named .codifide, .kiro, .claude, .cursor, .codex or .github only got R2 (the directory entries themselves are now R3); the workflow_ok documentation test matched words anywhere (comments included) and now requires run: steps that execute each script.
  • Stale text: the case count, the gate count in CONTRIBUTING and the whitepaper, the old version footer, a counsel marker in CONTRIBUTING and NOTICE, unqualified coverage and timing claims in the whitepaper and an article.

Added

  • templates/hooks/ real hook files (Kiro x4, Claude Code guard).
  • templates/cursor/to-mdc.sh Cursor .mdc converter (Cursor ignores plain .md rules).
  • templates/enforcement/workflow.example.yml complete workflow: trusted base copies, first-run bootstrap, push tripwire, two scoped labels.
  • VERSION, CHANGELOG.md, .gitattributes (LF endings), SPDX headers on scripts.
  • Documented limits and compensating controls; Windows/WSL and jq/python3 prerequisites.
  • Regression tests for each fix (245 cases), and documentation-consistency tests.
  • README "Scope and limits": the guard is a seatbelt, the base-branch CI checks plus branch protection are the gate, one table of documented limits, and a statement that the scripts are defense in depth, not a proof.

Verification. shasum -a 256 -c SHA256SUMS. This tag is annotated but not cryptographically signed; signed tags start with the next release.

Enforcement suite: 245 cases, ALL PASS (bash 5 and bash 3.2).