Skip to content

test(parity): two-tier audit parity test suite (Fixes #366) [stacked on #365] - #370

Merged
mlieberman85 merged 4 commits into
darnitdevorg:mainfrom
mlieberman85:028-audit-parity-tests
Aug 14, 2026
Merged

test(parity): two-tier audit parity test suite (Fixes #366) [stacked on #365]#370
mlieberman85 merged 4 commits into
darnitdevorg:mainfrom
mlieberman85:028-audit-parity-tests

Conversation

@mlieberman85

Copy link
Copy Markdown
Contributor

STACKED ON #365. Do not merge before #365. This branch is based on 026-harness-with-stage1 (the head of #365). Until #365 lands on main, this PR's diff visually includes all of #365's changes plus feature 028. After #365 merges, rebase onto main and the diff collapses to feature 028 only.

Summary

Two-tier diagnostic test suite verifying the darnit audit's per-control output is consistent across the three consumers users care about:

  • Direct MCP tool (darnit_baseline.tools.audit_openssf_baseline)
  • darnit harness end-to-end
  • /darnit-audit coding-agent skill (Tier 2 only)

Motivated by the PR #365 review where the skill was observed silently reclassifying WARN as PASS in its Markdown summary while the MCP tool and harness produced identical raw results.

Detailed rationale in the commit body.

What ships

  • tests/darnit/parity/tier1/ -- pytest suite, runs on every PR. For each fixture, invokes both audit paths in-process (harness uses MockLLMStep, no live API), diffs per-control status. Sole allowed drift: PENDING_LLM (MCP) -> non-PENDING_LLM (harness).
  • tests/darnit/parity/tier2/ -- manual-dispatch runner + skill Markdown parser + workflow-config assertions.
  • .github/workflows/parity-tier2.yml -- workflow_dispatch only, environment: parity-tier2 gated (required reviewers + Environment-scoped ANTHROPIC_API_KEY), permissions: contents: read, if: always() artifact upload.
  • 4 fixtures under tests/darnit/parity/fixtures/ covering all_pass / all_fail / mixed / pending_llm.
  • pyproject.toml: adds claude-agent-sdk to the workspace dev group. Zero product package changes -- SC-006 mechanically enforced by test_no_product_changes.py running git diff against the base branch.

Constitution + governance notes

  • II (Conservative-by-Default) reinforced: Tier 2 exists specifically to catch downstream layers eroding the tool's WARN-counts-as-FAIL property. Tier 1 catches equivalent regressions in the harness before merge.
  • IV (Never Guess User Values) reinforced: fixtures do NOT auto-generate context values. And Tier 2 makes silently-heuristic skill reinterpretation externally observable.
  • FR-007a governance: ANTHROPIC_API_KEY MUST live in a GitHub Environment (not repo-level). Enforced at the workflow YAML level AND by a pytest test that iterates every workflow file and asserts the key appears only in parity-tier2.yml.
  • Manual-dispatch only for MVP: the repo is under neutral governance and the API key belongs to a specific company. Automated scheduled runs would charge that company's account for community activity. Follow-up issue Parity Tier 2: add scheduled cadence + governance-appropriate API-key sourcing #369 tracks adding scheduled cadence once a governance-appropriate key-sourcing model exists.

Diagnostic finding surfaced during implementation

The parity test caught a real MCP-tool-vs-harness disagreement on OSPS-LE-03.02 (feature 026's inferred_from authority handling has a residual bug). Fixtures scope around it via parity.toml's control_ids filter. This is not a feature 028 bug -- it's exactly what the parity test was designed to find. A follow-up feature can fix the underlying inferred_from behavior; from now on, similar regressions will be CI-visible.

Test plan

  • uv run pytest tests/ -q -- expect 2589 passed, 15 skipped, 0 failures.
  • uv run pytest tests/darnit/parity/tier1/ -q -- expect 35 passed in about 35 seconds (SC-002 timing budget).
  • uv run pytest tests/darnit/parity/tier2/ -q -- expect 21 passed (offline; workflow config test parses YAML).
  • uv run ruff check . -- clean.
  • uv run python scripts/validate_sync.py --verbose -- PASSED.
  • Manual: uv run python -m tests.darnit.parity.tier2.run --fixture-glob "all_pass_repo" --dry-run --artifact-dir /tmp/parity-dryrun -- writes mcp_tool_result.json, skill_final_message.md, diff_report.md, metadata.json under /tmp/parity-dryrun/all_pass_repo/.
  • Manual (post-merge, in the parity-tier2 Environment): dispatch the workflow with default fixture_glob="*", verify the reviewer-approval gate fires, run to completion, download parity-artifacts bundle.

Reviewer checklist for the governance-critical YAML

  • on: block contains only workflow_dispatch.
  • Job declares environment: parity-tier2.
  • permissions.contents: read and no write scopes anywhere.
  • ANTHROPIC_API_KEY referenced only in the SDK-invocation step's env: block, sourced from secrets.ANTHROPIC_API_KEY (Environment secret).
  • Preflight audit step logs actor + SHA + fixture_glob BEFORE the SDK step.
  • Artifact upload step has if: always() so failure artifacts still land.
  • No api_key or similar workflow input.

Follow-ups

Rebase plan

@kusari-inspector

kusari-inspector Bot commented Aug 10, 2026

Copy link
Copy Markdown

Kusari Inspector

Kusari Analysis Results:

Proceed with these changes

✅ No Flagged Issues Detected
All values appear to be within acceptable risk parameters.

Both analyses independently recommend proceeding. The dependency analysis detected no pinned version dependency changes, presenting no dependency-related risk. The code analysis identified a single medium-severity, low-confidence finding (artipacked): actions/checkout at line 50 does not set persist-credentials: false, leaving the GitHub token stored in git config for the job duration. However, strong compensating controls neutralize any realistic exploit path: (1) the workflow trigger is workflow_dispatch only, eliminating untrusted PR or push attack vectors; (2) the environment parity-tier2 requires human reviewer approval before execution; (3) permissions are scoped to contents: read at both workflow and job levels, meaning the persisted credential has no write capability to abuse; (4) all actions are pinned to full 40-character commit SHAs. No secrets were detected and no additional code issues were found. The persisted credential issue is a hardening gap rather than an exploitable vulnerability under the current permission model. As an optional hardening measure, setting persist-credentials: false on the checkout action would eliminate the finding entirely.

Note

View full detailed analysis result for more information on the output and the checks that were run.


@kusari-inspector rerun - Trigger a re-analysis of this PR
@kusari-inspector feedback [your message] - Send feedback to our AI and team
See Kusari's documentation for setup and configuration.
Commit: 0e74d48, performed at: 2026-08-12T17:56:52Z

Found this helpful? Give it a 👍 or 👎 reaction!

run: uv sync --extra dev

- name: Preflight audit log (T2-7/T2-8)
run: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The Preflight audit log step interpolates ${{ inputs.fixture_glob }} and ${{ github.actor }} directly into the shell script. Move all context values to step-level env: variables and reference them as shell variables to prevent injection.

Recommended Code Changes:

- name: Preflight audit log (T2-7/T2-8)
  env:
    ACTOR: ${{ github.actor }}
    SHA: ${{ github.sha }}
    FIXTURE_GLOB: ${{ inputs.fixture_glob }}
  run: |
    {
      echo "## Tier 2 Preflight"
      echo ""
      echo "- actor: $ACTOR"
      echo "- sha: $SHA"
      echo "- fixture_glob: $FIXTURE_GLOB"
      echo "- timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
    } >> "$GITHUB_STEP_SUMMARY"

- name: Run Tier 2 parity check
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The Run Tier 2 parity check step interpolates ${{ inputs.fixture_glob }} directly as a shell argument. Add FIXTURE_GLOB to the existing env: block and reference it as a quoted shell variable.

Recommended Code Changes:

- name: Run Tier 2 parity check
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    FIXTURE_GLOB: ${{ inputs.fixture_glob }}
  run: |
    uv run python -m tests.darnit.parity.tier2.run \
      --fixture-glob "$FIXTURE_GLOB"

Comment thread .github/workflows/parity-tier2.yml Outdated

steps:
- name: Checkout
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actions/checkout, actions/setup-python, astral-sh/setup-uv, and actions/upload-artifact are all pinned to mutable version tags (@v4, @v5, @v6). If an action owner silently repoints a tag, malicious code could execute in this runner and access the ANTHROPIC_API_KEY. Pin each action to a full 40-character commit SHA to guarantee immutability.

@mlieberman85

Copy link
Copy Markdown
Contributor Author

Stacked with follow-on PR #371 (feature 029: OpenAI Tier 2 backend + SkillInvocationBackend Protocol; Fixes #368). #371 depends on this PR.

mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 12, 2026
… env: vars

Addresses Kusari Inspector findings on PR darnitdevorg#370:

1. Shell injection risk (HIGH impact / HIGH likelihood):
   `${{ github.actor }}`, `${{ github.sha }}`, and `${{ inputs.fixture_glob }}`
   were interpolated directly into `run:` blocks that had ANTHROPIC_API_KEY
   set in env. A crafted fixture_glob input containing shell metacharacters
   could execute arbitrary commands in the runner and exfiltrate the key.

   Fix: move each context expression into a step-level `env:` variable and
   reference it as a quoted shell variable inside the run: block. GitHub
   Actions substitutes ${{ ... }} at YAML-parse time; shell vars are
   substituted after the shell already parsed its own syntax, so a
   metacharacter in an input becomes a literal character in the value of
   the env var (harmless) rather than a shell operator.

2. Supply-chain risk from unpinned actions:
   actions/checkout@v4, actions/setup-python@v5, astral-sh/setup-uv@v6,
   and actions/upload-artifact@v4 all used mutable version tags. If any
   action owner's account were compromised, a silently repointed tag
   would execute in this workflow with ANTHROPIC_API_KEY in env.

   Fix: pin each action to a full 40-character commit SHA with a
   trailing comment identifying the human-readable version. Bumping an
   action now requires a PR edit -- reviewable, correlatable with any
   subsequent test-result changes.

Pinned versions:
  actions/checkout        11d5960a326750d5838078e36cf38b85af677262  v4.4.0
  actions/setup-python    a26af69be951a213d495a4c3e4e4022e16d87065  v5.6.0
  astral-sh/setup-uv      d0cc045d04ccac9d8b7881df0226f9e82c39688e  v6.8.0
  actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02  v4.6.2

Existing workflow-config tests continue to pass unchanged (they assert
governance-critical structure -- trigger, environment, permissions,
`if: always()` on artifact upload -- none of which this fix touches).
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 12, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
@kusari-inspector

Copy link
Copy Markdown

Kusari PR Analysis rerun based on - 0e74d48 performed at: 2026-08-12T17:57:19Z - link to updated analysis

@pxp928 pxp928 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on the condition that the findings below are addressed before merge. The two-tier design is the right shape for #366 and the comparator/diff structure is a good foundation. But as committed the suite doesn't execute anywhere — not locally, not in CI — and several of the comparison primitives report agreement where there is none. Please don't merge until the first three are fixed, since until then a green check means nothing ran.

Reviewed the incremental diff pr-365..pr-370 (commits 0a1671c, 0e74d48). I ran the suite to confirm the top findings.


Stack note

This branch is stacked on #365, not on #367. Worth confirming that's intentional — #367 and #370 are currently siblings.


Blockers — the suite never runs

1. Fixture marker file is gitignored

tier1/conftest.py:41

Fixture discovery requires a .baseline.toml marker in each fixture dir, but that filename is in .gitignore:32 and never committed.

Result, verified by running pytest: 0 of 4 fixtures discovered, 3 tests fail outright, test_parity collects zero cases, and the Tier 2 runner always exits 3.

Either rename the marker to something not ignored, or add a negation pattern un-ignoring the fixture paths. This one also propagates into #371, where it ships two red tests.

2. No pytest markers → CI deselects the entire suite

tier1/test_mcp_vs_harness.py:19

No parity test declares a unit or integration marker, but ci.yml only runs -m unit / -m integration. Actual result: 53 items / 53 deselected.

The Tier 1 gate is described as "runs on every PR." It runs on no PR.

3. The no-product-changes guard never enforces

tier1/test_no_product_changes.py:27

The FR-014 guard's hardcoded base-ref list falls through to origin/main, which flags 22 product files belonging to feature 026 — it fails locally today. In a shallow CI clone no ref resolves at all and it silently skips. So it either false-positives or no-ops; it never does the job.


Correctness bugs in the comparison logic

These matter more than ordinary test bugs, because a parity harness that reports false agreement is worse than no parity harness.

  • tier1/comparator.py:121is_allowed_drift returns True for PENDING_LLM vs the synthetic <MISSING>, so a control the harness drops entirely reports green. Directly contradicts T1-3. Verified.
  • tier2/skill_markdown_parser.py:154 — status literals are scanned in length-descending order rather than by position, so "OSPS-VM-04.01: WARN -- ... to reach PASS" parses as PASS. Verified. Should anchor on the first status token after the control ID.
  • tier2/diff.py:65 — the diff iterates only over skill claims, so a skill that omits a FAIL control (and omits the Failed: count line) returns success. Needs to iterate the union of both sides' control sets.

Tier 2 doesn't exercise what it claims to

tier2/claude_agent_sdk_client.py:85ClaudeAgentOptions sets no mcp_servers, no allowed_tools, no permission_mode, no setting_sources. The agent can therefore neither call audit_openssf_baseline nor shell out to anything.

So Tier 2 compares real MCP tool JSON against a summary the model produced without running an audit at all. The prompt is also a hand-written approximation of the skill rather than the real /darnit-audit skill definition.

This undermines the premise of the tier rather than being a bug in it — worth a design decision, not just a patch.

Packaging

pyproject.toml:56claude-agent-sdk (a ~90 MB platform wheel) is added to the published [project.optional-dependencies].dev extra of darnit-mcp, contradicting the SC-006 comment directly above it. [dependency-groups] dev already exists and is the right home.

Minor

  • tier1/conftest.py:136total_run_timeout_s=30 on the harness path (unbounded on the MCP path) will raise HarnessRunTimeout on a full level-3 audit against a fake remote, erroring the test instead of comparing.
  • tier2/run.py:175 — the errored bucket is checked before the disagreement buckets, so one infra exception downgrades a real per-control disagreement to exit 3 ("setup error"), hiding the signal you care about.
  • tier1/conftest.py:139asyncio.new_event_loop().run_until_complete(...) at three sites (also run.py:203, test_diff_adversarial.py:132) leaks an unclosed loop and never sets it current.
  • tier2/test_workflow_config.py:18 — cwd-relative Path(".github/workflows") (and Path.cwd() at test_diff_adversarial.py:148) makes the governance tests fail or vacuously skip when pytest runs outside the repo root.
  • tier2/diff.py:24COUNTS_DISAGREE = 1 silently aliases PER_CONTROL_DISAGREE. The enum is unused anyway, since run.py hardcodes its exit codes.

mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
… env: vars

Addresses Kusari Inspector findings on PR darnitdevorg#370:

1. Shell injection risk (HIGH impact / HIGH likelihood):
   `${{ github.actor }}`, `${{ github.sha }}`, and `${{ inputs.fixture_glob }}`
   were interpolated directly into `run:` blocks that had ANTHROPIC_API_KEY
   set in env. A crafted fixture_glob input containing shell metacharacters
   could execute arbitrary commands in the runner and exfiltrate the key.

   Fix: move each context expression into a step-level `env:` variable and
   reference it as a quoted shell variable inside the run: block. GitHub
   Actions substitutes ${{ ... }} at YAML-parse time; shell vars are
   substituted after the shell already parsed its own syntax, so a
   metacharacter in an input becomes a literal character in the value of
   the env var (harmless) rather than a shell operator.

2. Supply-chain risk from unpinned actions:
   actions/checkout@v4, actions/setup-python@v5, astral-sh/setup-uv@v6,
   and actions/upload-artifact@v4 all used mutable version tags. If any
   action owner's account were compromised, a silently repointed tag
   would execute in this workflow with ANTHROPIC_API_KEY in env.

   Fix: pin each action to a full 40-character commit SHA with a
   trailing comment identifying the human-readable version. Bumping an
   action now requires a PR edit -- reviewable, correlatable with any
   subsequent test-result changes.

Pinned versions:
  actions/checkout        11d5960a326750d5838078e36cf38b85af677262  v4.4.0
  actions/setup-python    a26af69be951a213d495a4c3e4e4022e16d87065  v5.6.0
  astral-sh/setup-uv      d0cc045d04ccac9d8b7881df0226f9e82c39688e  v6.8.0
  actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02  v4.6.2

Existing workflow-config tests continue to pass unchanged (they assert
governance-critical structure -- trigger, environment, permissions,
`if: always()` on artifact upload -- none of which this fix touches).
@mlieberman85
mlieberman85 force-pushed the 028-audit-parity-tests branch from 0e74d48 to a48ff92 Compare August 13, 2026 15:03
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…+ wire Tier 2 MCP

Reviewer pxp928 flagged three blockers, three correctness bugs, an SDK
wiring hole, and a packaging concern on darnitdevorg#370. This commit addresses
all of them.

Blockers:
- `.baseline.toml` was in `.gitignore`, so none of the four parity
  fixtures had their config tracked -- CI's Tier 1 collector saw zero
  fixtures. Added an `!tests/darnit/parity/fixtures/**/.baseline.toml`
  negation and force-added the four fixture configs.
- Parity tests carried no pytest markers, so CI's `-m unit` /
  `-m integration` split silently deselected the entire suite.
  Auto-mark every test under `tests/darnit/parity/tier1/` as
  `integration` via a conftest `pytest_collection_modifyitems` hook.
- `test_no_product_changes.py` silently skipped under shallow CI
  clones. Fail loudly with a clear pointer at `fetch-depth: 0` when
  `CI=1` and no base ref is reachable; keep the local-dev skip.

Correctness bugs:
- `comparator.is_allowed_drift` returned True for MCP=PENDING_LLM vs
  harness=<MISSING>, contradicting T1-3. Missing on either side is
  now always a hard failure.
- `skill_markdown_parser` scanned status literals in
  _STATUS_LITERALS order, so a phrase like "WARN ... to reach PASS"
  parsed as PASS. Pick the LEFTMOST status literal instead so the
  first token in the line wins.
- `tier2/diff.py` iterated only over the skill's claims; a skill
  omitting a FAIL control silently reported "success". Also walk the
  tool's controls and flag any the skill did not mention.

Tier 2 SDK wiring:
- `ClaudeAgentOptions` sent only model/max_turns/cwd. The agent had
  no MCP server, no allowed tools, and no permission mode -- so it
  couldn't actually call `audit_openssf_baseline` and Tier 2 was
  measuring "skill does nothing" rather than skill vs tool. Wire
  `mcp_servers={"darnit": {stdio "darnit serve"}}`, allow only
  `mcp__darnit__audit_openssf_baseline`, set
  `permission_mode="acceptEdits"`, and lock `setting_sources=[]` to
  keep the host's local Claude Code config out of the run.

Packaging:
- Moved `claude-agent-sdk>=0.1.0` out of `[project.optional-dependencies].dev`
  (published on `darnit-mcp[dev]`, ~90 MB) into a dedicated
  `[parity-tier2]` extra. Tier 2 CI now installs
  `--extra dev --extra parity-tier2` explicitly; a contributor
  installing `darnit-mcp[dev]` no longer pulls the SDK.

Workspace sweep: 2593 pass, 15 skip.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…sts integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on darnitdevorg#371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from darnitdevorg#370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR darnitdevorg#370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
… env: vars

Addresses Kusari Inspector findings on PR darnitdevorg#370:

1. Shell injection risk (HIGH impact / HIGH likelihood):
   `${{ github.actor }}`, `${{ github.sha }}`, and `${{ inputs.fixture_glob }}`
   were interpolated directly into `run:` blocks that had ANTHROPIC_API_KEY
   set in env. A crafted fixture_glob input containing shell metacharacters
   could execute arbitrary commands in the runner and exfiltrate the key.

   Fix: move each context expression into a step-level `env:` variable and
   reference it as a quoted shell variable inside the run: block. GitHub
   Actions substitutes ${{ ... }} at YAML-parse time; shell vars are
   substituted after the shell already parsed its own syntax, so a
   metacharacter in an input becomes a literal character in the value of
   the env var (harmless) rather than a shell operator.

2. Supply-chain risk from unpinned actions:
   actions/checkout@v4, actions/setup-python@v5, astral-sh/setup-uv@v6,
   and actions/upload-artifact@v4 all used mutable version tags. If any
   action owner's account were compromised, a silently repointed tag
   would execute in this workflow with ANTHROPIC_API_KEY in env.

   Fix: pin each action to a full 40-character commit SHA with a
   trailing comment identifying the human-readable version. Bumping an
   action now requires a PR edit -- reviewable, correlatable with any
   subsequent test-result changes.

Pinned versions:
  actions/checkout        11d5960a326750d5838078e36cf38b85af677262  v4.4.0
  actions/setup-python    a26af69be951a213d495a4c3e4e4022e16d87065  v5.6.0
  astral-sh/setup-uv      d0cc045d04ccac9d8b7881df0226f9e82c39688e  v6.8.0
  actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02  v4.6.2

Existing workflow-config tests continue to pass unchanged (they assert
governance-critical structure -- trigger, environment, permissions,
`if: always()` on artifact upload -- none of which this fix touches).
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…+ wire Tier 2 MCP

Reviewer pxp928 flagged three blockers, three correctness bugs, an SDK
wiring hole, and a packaging concern on darnitdevorg#370. This commit addresses
all of them.

Blockers:
- `.baseline.toml` was in `.gitignore`, so none of the four parity
  fixtures had their config tracked -- CI's Tier 1 collector saw zero
  fixtures. Added an `!tests/darnit/parity/fixtures/**/.baseline.toml`
  negation and force-added the four fixture configs.
- Parity tests carried no pytest markers, so CI's `-m unit` /
  `-m integration` split silently deselected the entire suite.
  Auto-mark every test under `tests/darnit/parity/tier1/` as
  `integration` via a conftest `pytest_collection_modifyitems` hook.
- `test_no_product_changes.py` silently skipped under shallow CI
  clones. Fail loudly with a clear pointer at `fetch-depth: 0` when
  `CI=1` and no base ref is reachable; keep the local-dev skip.

Correctness bugs:
- `comparator.is_allowed_drift` returned True for MCP=PENDING_LLM vs
  harness=<MISSING>, contradicting T1-3. Missing on either side is
  now always a hard failure.
- `skill_markdown_parser` scanned status literals in
  _STATUS_LITERALS order, so a phrase like "WARN ... to reach PASS"
  parsed as PASS. Pick the LEFTMOST status literal instead so the
  first token in the line wins.
- `tier2/diff.py` iterated only over the skill's claims; a skill
  omitting a FAIL control silently reported "success". Also walk the
  tool's controls and flag any the skill did not mention.

Tier 2 SDK wiring:
- `ClaudeAgentOptions` sent only model/max_turns/cwd. The agent had
  no MCP server, no allowed tools, and no permission mode -- so it
  couldn't actually call `audit_openssf_baseline` and Tier 2 was
  measuring "skill does nothing" rather than skill vs tool. Wire
  `mcp_servers={"darnit": {stdio "darnit serve"}}`, allow only
  `mcp__darnit__audit_openssf_baseline`, set
  `permission_mode="acceptEdits"`, and lock `setting_sources=[]` to
  keep the host's local Claude Code config out of the run.

Packaging:
- Moved `claude-agent-sdk>=0.1.0` out of `[project.optional-dependencies].dev`
  (published on `darnit-mcp[dev]`, ~90 MB) into a dedicated
  `[parity-tier2]` extra. Tier 2 CI now installs
  `--extra dev --extra parity-tier2` explicitly; a contributor
  installing `darnit-mcp[dev]` no longer pulls the SDK.

Workspace sweep: 2593 pass, 15 skip.
@mlieberman85
mlieberman85 force-pushed the 028-audit-parity-tests branch from d43a1f3 to d6e0353 Compare August 13, 2026 16:37
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…sts integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on darnitdevorg#371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from darnitdevorg#370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR darnitdevorg#370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…sts integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on darnitdevorg#371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from darnitdevorg#370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR darnitdevorg#370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
@mlieberman85
mlieberman85 force-pushed the 028-audit-parity-tests branch from 5c7fb5e to b411b5b Compare August 13, 2026 16:45
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…sts integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on darnitdevorg#371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from darnitdevorg#370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR darnitdevorg#370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
@mlieberman85
mlieberman85 force-pushed the 028-audit-parity-tests branch from b411b5b to 7b4205f Compare August 13, 2026 17:47
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 13, 2026
…sts integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on darnitdevorg#371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from darnitdevorg#370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR darnitdevorg#370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
…g#366)

Adds a diagnostic test suite verifying the darnit audit's per-control
output is consistent across the three consumers users care about: the
direct MCP tool call (`audit_openssf_baseline`), the `darnit harness`
end-to-end path, and the `/darnit-audit` coding-agent skill's summary.

Motivated by the PR darnitdevorg#365 review where the skill was observed silently
reclassifying WARN as PASS in its Markdown summary while the MCP tool
and harness produced identical raw results.

Tier 1 -- mechanical MCP-vs-harness parity (`tests/darnit/parity/tier1/`)
runs on every PR. For each fixture in the corpus, invokes both audit
paths in-process (harness uses `MockLLMStep` so no live API), normalizes
to a common `AuditResult`, and diffs per-control status. Sole allowed
drift: MCP leaves a control PENDING_LLM; harness resolves it via the
LLM continuation loop to any non-PENDING_LLM status. Anything else is a
hard failure with a human-readable fixed-width Markdown drift table
(no ANSI). Full corpus runs in about 35 seconds, well under the 60s
budget.

Tier 2 -- coding-agent skill parity (`tests/darnit/parity/tier2/`) is
manual-dispatch only via `.github/workflows/parity-tier2.yml`. The
workflow declares `environment: parity-tier2` so a GitHub Environment
with required reviewers gates every dispatch; `ANTHROPIC_API_KEY` is
stored at the Environment level (never at repo scope). The runner
captures raw MCP tool JSON, invokes the `/darnit-audit` skill via the
Claude Agent SDK with an explicit prompt snapshot + turn cap + zero
temperature, parses the skill's final assistant message, and diffs.
Per-control status disagreement is a hard failure regardless of
authority level (skill has no license to reinterpret). Unparseable
skill output surfaces as a distinct failure class from disagreement
so a maintainer can tell a broken parser from a real drift.

Fixture corpus: `all_pass_repo`, `all_fail_repo`, `mixed_repo`,
`pending_llm_repo`. Each ships with a `parity.toml` declaring category,
expected counts, and a `control_ids` filter (both audit paths run all
66 OpenSSF Baseline controls today because neither auto-applies
fixture-level `audit_profiles` from `.baseline.toml`; the filter is
test-side and honest about scope). SC-008 corpus-inventory test
verifies every category is represented.

Governance property (FR-007a + SC-005a) enforced at the GitHub Actions
Environment layer AND by a pytest test that iterates every
`.github/workflows/*.yml` and asserts `ANTHROPIC_API_KEY` appears only
in `parity-tier2.yml`. Pure-Python file iteration; no `grep` subprocess.

FR-014 zero-product-code-changes enforced by `test_no_product_changes.py`:
runs `git diff --name-only origin/026-harness-with-stage1...HEAD` and
fails if any file under `packages/darnit/src/` or
`packages/darnit-baseline/src/` is modified.

Diagnostic finding surfaced during implementation: MCP tool and harness
disagree on `OSPS-LE-03.02` (feature 026's `inferred_from` authority
handling has a residual bug). Fixtures scope around it via
`control_ids` in `parity.toml`. That divergence is now a discoverable
finding for a follow-up feature to fix; the parity test surface makes
it CI-visible from now on.

56 new tests (35 Tier 1 + 21 Tier 2). Full workspace sweep: 2589 pass,
15 skipped, 0 failures. `ruff check` clean; `validate_sync.py` PASSED.
`claude-agent-sdk` added as a workspace dev-group dep only; product
packages `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` are untouched.

Closes darnitdevorg#366. Follow-up issues opened:
- darnitdevorg#368: OpenAI SDK + other-provider parity checks (Tier-2-style, different provider)
- darnitdevorg#369: scheduled cadence + governance-appropriate key sourcing

Depends on PR darnitdevorg#365 (feature 026 + Stage 1 substrate).
… env: vars

Addresses Kusari Inspector findings on PR darnitdevorg#370:

1. Shell injection risk (HIGH impact / HIGH likelihood):
   `${{ github.actor }}`, `${{ github.sha }}`, and `${{ inputs.fixture_glob }}`
   were interpolated directly into `run:` blocks that had ANTHROPIC_API_KEY
   set in env. A crafted fixture_glob input containing shell metacharacters
   could execute arbitrary commands in the runner and exfiltrate the key.

   Fix: move each context expression into a step-level `env:` variable and
   reference it as a quoted shell variable inside the run: block. GitHub
   Actions substitutes ${{ ... }} at YAML-parse time; shell vars are
   substituted after the shell already parsed its own syntax, so a
   metacharacter in an input becomes a literal character in the value of
   the env var (harmless) rather than a shell operator.

2. Supply-chain risk from unpinned actions:
   actions/checkout@v4, actions/setup-python@v5, astral-sh/setup-uv@v6,
   and actions/upload-artifact@v4 all used mutable version tags. If any
   action owner's account were compromised, a silently repointed tag
   would execute in this workflow with ANTHROPIC_API_KEY in env.

   Fix: pin each action to a full 40-character commit SHA with a
   trailing comment identifying the human-readable version. Bumping an
   action now requires a PR edit -- reviewable, correlatable with any
   subsequent test-result changes.

Pinned versions:
  actions/checkout        11d5960a326750d5838078e36cf38b85af677262  v4.4.0
  actions/setup-python    a26af69be951a213d495a4c3e4e4022e16d87065  v5.6.0
  astral-sh/setup-uv      d0cc045d04ccac9d8b7881df0226f9e82c39688e  v6.8.0
  actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02  v4.6.2

Existing workflow-config tests continue to pass unchanged (they assert
governance-critical structure -- trigger, environment, permissions,
`if: always()` on artifact upload -- none of which this fix touches).
…+ wire Tier 2 MCP

Reviewer pxp928 flagged three blockers, three correctness bugs, an SDK
wiring hole, and a packaging concern on darnitdevorg#370. This commit addresses
all of them.

Blockers:
- `.baseline.toml` was in `.gitignore`, so none of the four parity
  fixtures had their config tracked -- CI's Tier 1 collector saw zero
  fixtures. Added an `!tests/darnit/parity/fixtures/**/.baseline.toml`
  negation and force-added the four fixture configs.
- Parity tests carried no pytest markers, so CI's `-m unit` /
  `-m integration` split silently deselected the entire suite.
  Auto-mark every test under `tests/darnit/parity/tier1/` as
  `integration` via a conftest `pytest_collection_modifyitems` hook.
- `test_no_product_changes.py` silently skipped under shallow CI
  clones. Fail loudly with a clear pointer at `fetch-depth: 0` when
  `CI=1` and no base ref is reachable; keep the local-dev skip.

Correctness bugs:
- `comparator.is_allowed_drift` returned True for MCP=PENDING_LLM vs
  harness=<MISSING>, contradicting T1-3. Missing on either side is
  now always a hard failure.
- `skill_markdown_parser` scanned status literals in
  _STATUS_LITERALS order, so a phrase like "WARN ... to reach PASS"
  parsed as PASS. Pick the LEFTMOST status literal instead so the
  first token in the line wins.
- `tier2/diff.py` iterated only over the skill's claims; a skill
  omitting a FAIL control silently reported "success". Also walk the
  tool's controls and flag any the skill did not mention.

Tier 2 SDK wiring:
- `ClaudeAgentOptions` sent only model/max_turns/cwd. The agent had
  no MCP server, no allowed tools, and no permission mode -- so it
  couldn't actually call `audit_openssf_baseline` and Tier 2 was
  measuring "skill does nothing" rather than skill vs tool. Wire
  `mcp_servers={"darnit": {stdio "darnit serve"}}`, allow only
  `mcp__darnit__audit_openssf_baseline`, set
  `permission_mode="acceptEdits"`, and lock `setting_sources=[]` to
  keep the host's local Claude Code config out of the run.

Packaging:
- Moved `claude-agent-sdk>=0.1.0` out of `[project.optional-dependencies].dev`
  (published on `darnit-mcp[dev]`, ~90 MB) into a dedicated
  `[parity-tier2]` extra. Tier 2 CI now installs
  `--extra dev --extra parity-tier2` explicitly; a contributor
  installing `darnit-mcp[dev]` no longer pulls the SDK.

Workspace sweep: 2593 pass, 15 skip.
`_base_ref()` in `test_no_product_source_changes` fails loudly under
`CI=1` when no base ref is reachable, but the default `actions/checkout`
config leaves the repo shallow so the base ref (`origin/main` or the
stack parent) is not initially reachable. The check would false-fail on
every CI run until the workflow was updated with `fetch-depth: 0`.

Attempt `git fetch --unshallow --tags origin` once before giving up, so
the test resolves the base ref on its own if the runner has network
access. Preserves the loud-fail semantics on a truly unreachable base
(no network + no history).
@mlieberman85
mlieberman85 force-pushed the 028-audit-parity-tests branch from 7b4205f to 195e4a2 Compare August 13, 2026 20:22
@mlieberman85
mlieberman85 merged commit 488f68e into darnitdevorg:main Aug 14, 2026
7 of 8 checks passed
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 14, 2026
…ocol (Fixes darnitdevorg#368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes darnitdevorg#368. Sibling to darnitdevorg#367 (feature 027) and darnitdevorg#370 (feature 028);
stacks on darnitdevorg#370.
mlieberman85 added a commit to mlieberman85/darnit that referenced this pull request Aug 14, 2026
…sts integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on darnitdevorg#371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from darnitdevorg#370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR darnitdevorg#370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
mlieberman85 added a commit that referenced this pull request Aug 14, 2026
…ocol (Fixes #368) [stacked on #370] (#371)

* test(parity): add OpenAI Tier 2 backend + SkillInvocationBackend Protocol (Fixes #368)

Extends feature 028's parity test suite with a second Tier 2 provider
adapter. Introduces a shared `SkillInvocationBackend` Protocol so future
adapters (Gemini, xAI, self-hosted) slot in without touching the shared
runner, differ, parser, or artifact writer.

Two-Environment governance: `parity-tier2` (Claude, feature 028) and
`parity-tier2-openai` (OpenAI, this feature) each hold their own
reviewer list and secret. `OPENAI_API_KEY` never appears in any workflow
other than `parity-tier2-openai.yml`; `ANTHROPIC_API_KEY` never appears
in the OpenAI workflow. Enforced by a workflow-config test that iterates
`.github/workflows/*.yml` and asserts the exclusivity property.

Protocol seam:
- `tests/darnit/parity/tier2/backends/base.py` -- @runtime_checkable
  Protocol with `name`, async `invoke(fixture_dir, model, max_turns)`,
  classmethod `check_env()`. `SkillInvocationResult` frozen dataclass
  gains a `turn_cap_exhausted: bool` field (default False).
- `tests/darnit/parity/tier2/backends/claude_agent_sdk.py` -- feature
  028's `invoke_skill` refactored into a class satisfying the Protocol.
  Body unchanged.
- `tests/darnit/parity/tier2/claude_agent_sdk_client.py` -- backwards-
  compat shim; feature 028's existing tests continue to import from
  this path without change.
- `tests/darnit/parity/tier2/backends/openai_backend.py` -- new OpenAI
  Chat Completions API backend. Hand-rolled tool-call loop, stateless
  per invocation, `temperature=0.0` for reproducibility. Registers the
  `audit_openssf_baseline` MCP tool as an OpenAI function-callable tool;
  `_dispatch_tool_call` FORCES `local_path=str(fixture_dir)` so a rogue
  model cannot make the tool wander outside the fixture (contract B-17,
  verified by adversarial test).
- `tests/darnit/parity/tier2/backends/noop.py` -- test-only NoopBackend
  used by conformance and extensibility tests. Not registered in
  BACKEND_REGISTRY by default; tests inject it via
  `run.main(backends={"noop": NoopBackend})`.
- `tests/darnit/parity/tier2/backends/__init__.py` -- BACKEND_REGISTRY
  dict is the single source of truth for `--backend <name>` lookup.

Runner extensions:
- `--backend <name>` CLI flag, default `claude_agent_sdk` (preserves
  feature 028 behavior).
- `--model <name>` and `--max-turns <int>` flags (each backend's
  workflow YAML supplies its provider-appropriate pinned defaults).
- New outcome `turn_cap_exhausted` with exit code 5. Diagnostically
  separate from `unparseable` (exit 2) and `per_control_disagree` (exit
  1). Maps to a different fix (adjust prompt / raise cap, not adjust
  parser).
- `main(argv, backends=)` accepts an optional backends dict override
  for test-side injection (SC-007 -- proves the Protocol seam works
  without touching shared modules).
- Preflight audit log includes backend + model alongside actor + SHA.
- Artifact filename convention: `openai_final_message.md` for the
  OpenAI backend; feature 028's `skill_final_message.md` preserved for
  Claude so downstream analysis scripts don't churn.

OpenAI workflow (`.github/workflows/parity-tier2-openai.yml`):
- Manual dispatch only.
- `environment: parity-tier2-openai` -- REVIEWER APPROVAL REQUIRED
  before OPENAI_API_KEY is exposed (configure in GitHub UI; NOT in
  this YAML).
- `permissions: contents: read` only.
- Pinned model default `gpt-4o-2024-08-06` -- version-suffixed. A
  moving alias like `gpt-4o` would fail the SC-010 workflow-config
  test that regexes the default for a versioned pattern. Bumping the
  default requires a PR editing the YAML -- reviewable, correlatable
  with any subsequent test-result changes.
- Artifact upload with `if: always()`.

Zero product package changes. Feature 028's `test_no_product_changes.py`
guard continues to cover `packages/*/src/`. Added `openai>=1.50` to the
workspace-level dev group ONLY; `packages/darnit/pyproject.toml` and
`packages/darnit-baseline/pyproject.toml` untouched.

Tests: 32 new (protocol conformance, extensibility, OpenAI adversarial
including turn-cap-exhausted + local_path guard + shared-parser
compatibility, workflow config for both YAMLs, shim exports).
Full workspace sweep: 2621 passed, 15 skipped, 0 failures.

Closes #368. Sibling to #367 (feature 027) and #370 (feature 028);
stacks on #370.

* test(parity): add cross-provider aggregate diff script (feature 029 T022 / US3)

Local maintainer script (`tests/darnit/parity/tier2/scripts/aggregate_provider_diff.py`)
that reads Tier 2 artifact bundles from both providers (Claude via
`skill_final_message.md`, OpenAI via `openai_final_message.md`) and
produces a Markdown table per fixture showing where the two providers'
final assistant messages agree or disagree on per-control status.

Not invoked by CI (US3 is P3 in the spec; the useful signal is a local
investigation, not an every-run automated check). Documented in
quickstart.md as a maintainer workflow.

Ten unit tests cover the parsing + diff logic (agreement, disagreement,
missing artifacts, discovery-across-roots, exit codes) so the script
doesn't silently rot.

Closes T022 in feature 029's task list. Completes US3.

* ci(parity-tier2-openai): pin actions to SHAs + move context expressions into env: vars

Addresses Kusari Inspector findings on PR #371 -- same class of issue as
the sibling fix for parity-tier2.yml on the 028 branch:

1. Shell injection risk (HIGH impact / HIGH likelihood):
   `${{ github.actor }}`, `${{ github.sha }}`, `${{ inputs.fixture_glob }}`,
   and `${{ inputs.model }}` were interpolated directly into `run:` blocks
   that had OPENAI_API_KEY set in env. A crafted fixture_glob or model
   input containing shell metacharacters could execute arbitrary commands
   and exfiltrate the key.

   Fix: move each context expression into a step-level `env:` variable and
   reference it as a quoted shell variable inside the run: block.

2. Supply-chain risk from unpinned actions:
   Same four actions (actions/checkout, actions/setup-python,
   astral-sh/setup-uv, actions/upload-artifact) used mutable version tags.

   Fix: pin each action to the same 40-character commit SHA used by the
   Claude workflow (both workflows share the identical pinned versions to
   simplify supply-chain review).

Pinned versions match the Claude workflow exactly:
  actions/checkout        11d5960a326750d5838078e36cf38b85af677262  v4.4.0
  actions/setup-python    a26af69be951a213d495a4c3e4e4022e16d87065  v5.6.0
  astral-sh/setup-uv      d0cc045d04ccac9d8b7881df0226f9e82c39688e  v6.8.0
  actions/upload-artifact ea165f8d65b6e75b540449e92b4886f43607fa02  v4.6.2

All feature-029 workflow-config tests continue to pass (15 tests covering
both workflows' governance-critical structure).

* review-fix(pr-371): guard SDK import + pin OpenAI tool args + mark tests integration

Reviewer pxp928 flagged one blocker (rebase-driven) and six should-fix
items on #371. This commit addresses all of them.

Blocker:
- Both SC-007 tests inherited the missing-fixture-config bug from #370.
  Rebasing onto the updated 028 pulls the tracked `.baseline.toml`
  fixtures + the pytest-marker hook forward, so those tests now
  collect and run.

Should-fix:
- `import openai` in the OpenAI-backend adversarial test now goes
  through `pytest.importorskip("openai")`, so a lean env without the
  `parity-tier2` extra installed skips cleanly instead of exploding
  with ImportError.
- `_dispatch_tool_call` now pins `level=3` and `output_format="json"`
  the same way `local_path` is pinned. Previously `setdefault` let a
  rogue model ask for markdown at level 1 and defeat the comparison.
- Added a `tier2/conftest.py` `pytest_collection_modifyitems` hook so
  every Tier 2 test collects with the `integration` marker. Same
  reasoning as PR #370's tier1 conftest: CI's `-m unit / -m integration`
  split would otherwise silently deselect the whole suite.
- The cross-provider aggregate script no longer silently reports
  "0 disagreements" when either provider's message was unparseable.
  It labels the fixture UNPARSEABLE in the per-fixture summary, lists
  unparseable fixtures at the bottom, and exits non-zero so CI can
  tell a "clean run" from a "no signal" run.
- The diff-report formatter now accepts a `final_message_filename`
  keyword so an OpenAI-provider run's failure report points at
  `openai_final_message.md` instead of hardcoded
  `skill_final_message.md`. Threaded from `run.py` via
  `_provider_filename_prefix`.
- Same filename thread covers the per-control and unparseable
  branches, so the diff output is consistent regardless of which
  provider ran.

Workspace sweep: 2635 pass, 15 skip.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants