Skip to content

fix(OMN-16071): append-only guard rejects in-place writes by git status, not path membership - #2086

Merged
jonahgabriel merged 1 commit into
devfrom
jonah/omn-16071-defect1-append-only-writer
Aug 17, 2026
Merged

jonahgabriel merged 1 commit into
devfrom
jonah/omn-16071-defect1-append-only-writer

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

The defect (OMN-16071 defect 1)

Both OCC companion producers' final pre-push append-only guard
(OccCompanionEmitter._assert_append_only in node_pr_lifecycle_fix_effect,
HandlerOccCompanionEffect._assert_append_only in node_occ_companion_effect)
only rejected deletions and paths outside the writer's own allowlist. A
path the writer's own plan already lists as "allowed" could still be
modified in place (git status M) and pass this guard silently — that is
exactly the mutate-in-place defect named in the ticket title, and observed
live on the autobind companion for OmniNode-ai/omninode_infra#906, which
rewrote drift/dod_receipts/OMN-16029/dod-occ-evidence-admissibility-validator/command.yaml,
replacing PR #900's landed evidence with #906's.

This is why two separate upstream point-fixes were needed to compensate
rather than the last-line-of-defense assertion catching it directly:
OMN-15485's assert_append_only_emissions (a compute-plan-level guard in
node_occ_companion_compute) and OMN-15785's ticket-scoped skip guard in
the legacy emitter. Neither of those changed what the actual git diff
assertion checks — membership in allowed_paths, never the git status
letter.

The fix

Both _assert_append_only methods now require git status A (added)
unconditionally for every changed path, in addition to the existing
membership/forbidden-surface/deletion checks. This matches the hosted
OCC Append-Only Gate's own semantics
(omnibase_core.validation.validator_occ_append_only.evaluate_append_only,
which flags any M/D/R/C status and requires corrections to be
net-new .supersede.<NNNN>.yaml files) directly against the git status
letter, rather than trusting the writer's own claim that a path is
"allowed."

Verification

RED-first: added a test to each producer's existing append-only suite
proving a status-M write to a path within the writer's own
allowed_paths previously sailed through undetected (DID NOT RAISE).
Confirmed both new tests failed against the pre-fix code before making the
change.

GREEN after fix: both new tests now raise append-only violation; all
pre-existing coverage remains green — the add/delete/rename/out-of-set/
forbidden-surface cases in test_append_only_guard_omn_14785.py,
test_append_only_forbidden_surfaces_omn_14941.py,
test_occ_companion_emitter_friction_omn_14741.py::TestF01AppendOnly, and
the compute-plan-level test_occ_append_only_producer_omn_15485.py (which
exercises the sibling OMN-15485 mechanism this PR complements, not
duplicates) all still pass.

Local gates: ruff format --check clean, ruff check clean, mypy --strict
clean on both changed source files, pre-commit run clean (0 failures) on
the four changed files, full local pytest suite (pre-push governed selector
escalated) 7619 passed / 22 skipped / 1 xfailed / 0 failed.

Scope

This closes defect 1 as scoped by the ticket: the mutate-in-place receipt
writer. It does not touch defect 2 (already fixed by #2081) and does not
touch the related-but-distinct autobind in-place re-mint anti-pattern
family on the OCC side (a separate ticket) — this PR is confined to the two
producer modules living in this repo.

Evidence-Ticket: OMN-16071
Evidence-Source: OCC#6606

… by git status, not just path membership

Both OCC companion producers' final pre-push append-only assertion
(OccCompanionEmitter._assert_append_only in node_pr_lifecycle_fix_effect,
HandlerOccCompanionEffect._assert_append_only in node_occ_companion_effect)
only rejected deletions and paths outside the writer's own allowlist. A
path already listed as "allowed" by the writer's own plan could still be
modified in place (git status M) and pass silently -- exactly the
mutate-in-place defect named in the ticket title, and the reason two
separate upstream point-fixes (OMN-15485's compute-plan
assert_append_only_emissions, OMN-15785's legacy-emitter skip guard) were
needed to compensate rather than this last-line-of-defense catching it
directly.

Both guards now require git status "A" unconditionally for every changed
path, matching the hosted OCC Append-Only Gate's own semantics
(evaluate_append_only, which flags any M/D/R/C status and requires
corrections to be net-new .supersede.<NNNN>.yaml files) rather than
trusting the writer's own membership claim.

RED-first: added a test to each producer's suite proving a status-M write
to a path within the writer's own allowed_paths previously sailed through
undetected (DID NOT RAISE); both now raise "append-only violation" after
the fix. Existing add/delete/out-of-set/forbidden-surface coverage in
test_append_only_guard_omn_14785.py, test_append_only_forbidden_surfaces_omn_14941.py,
test_occ_companion_emitter_friction_omn_14741.py::TestF01AppendOnly, and
test_occ_append_only_producer_omn_15485.py all remain green (48/48 in the
directly affected suites, 531/531 across the three touched node packages).

Evidence-Ticket: OMN-16071
@jonahgabriel
jonahgabriel enabled auto-merge (squash) August 17, 2026 11:28
@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 55 minutes

Limit details: You’ve used all 1 included review currently available under your plan. You completed 112 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3936be42-60c7-4269-9718-d5da771d3774

📥 Commits

Reviewing files that changed from the base of the PR and between 8f87a3b and b9b705e.

📒 Files selected for processing (4)
  • src/omnimarket/nodes/node_occ_companion_effect/handlers/handler_occ_companion_effect.py
  • src/omnimarket/nodes/node_pr_lifecycle_fix_effect/handlers/occ_companion_emitter.py
  • tests/unit/nodes/node_occ_companion_effect/test_append_only_guard_omn_14785.py
  • tests/unit/nodes/node_pr_lifecycle_fix_effect/test_occ_companion_emitter_friction_omn_14741.py

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

⚠️ Architectural Review — WARNINGS

Errors: 0
Warnings: 1

What this checks

Rule Description
ARCH-TOPIC-001 No hardcoded Kafka topic strings in handler code
ARCH-DI-001 No event_bus=None bypass
ARCH-DI-002 No direct Handler instantiation outside workflow_runner/adapters
ARCH-DI-003 No reinvented DI containers
ARCH-TOPIC-002 contract.yaml topics follow `onex.{cmd

ARCH-DI-002: Direct Handler instantiation (warning)\n\n\nsrc/omnimarket/nodes/node_occ_companion_effect/handlers/handler_occ_companion_effect.py:215: self._state_handler = state_handler or HandlerOccStateEffect()\n\n\nHandler classes should be wired only in workflow_runner.py or adapter modules. Direct instantiation elsewhere bypasses the contract system.\n\n

Static architectural lint — no model inference (OMN-14176).

@github-actions

Copy link
Copy Markdown

✅ Hostile Reviewer — PASSED

Blocking findings (critical/error): 0
Total findings: 0
Models succeeded: qwen3-review,qwen3-review-b


Gate semantics

Verdict Meaning Blocks merge?
passed >=2 models succeeded, no critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded Fewer than 2 models succeeded (infra unavailable/timeout) Yes (OMN-15110)

Powered by omniintelligence.review_pairing.cli_review — node-based adversarial review via HandlerLlmCliSubprocess (OMN-8524)

onexbot-occ-writer Bot pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 17, 2026
jonahgabriel added a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 17, 2026
node_occ_companion_effect's own pass-2 (self-bind + Evidence-Source stamp)
never reached this ticket: its automated retry aborted with an
append-only violation on contracts/OMN-16071.yaml -- omnimarket #2086's
own hardened _assert_append_only guard correctly refusing a stale-based
re-render, caused by heavy same-day concurrent OCC contract activity on
this exact ticket (occ#6550, occ#6590 both landed today). That guard did
its job: nothing corrupt was pushed. Hand-authoring the missing self-bind
entry + receipt here, mirroring the occ-self-bind-pr-6550/-6590 precedent
on this same contract exactly, so occ-preflight/eligibility and
verify/verify on OCC#6606 do not fail closed with pr_ticket_mismatch.

Pure append relative to the current dev tip (verified byte-identical to
this branch's own pass-1 commit before appending -- not a repeat of the
race): validator_occ_append_only.evaluate_append_only -> ok=True, zero
violations. validate_occ_merge_eligibility -> eligible=True. yamlfmt
-lint clean (hashes computed against the yamlfmt-stable bytes). Local
pre-commit on both changed files: 0 failures.

Evidence-Ticket: OMN-16071
jonahgabriel added a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 17, 2026
…6606)

* evidence: OCC companion pass 1 for OmniNode-ai/omnimarket#2086

* evidence(OMN-16071): self-bind OCC#6606 for OmniNode-ai/omnimarket#2086

node_occ_companion_effect's own pass-2 (self-bind + Evidence-Source stamp)
never reached this ticket: its automated retry aborted with an
append-only violation on contracts/OMN-16071.yaml -- omnimarket #2086's
own hardened _assert_append_only guard correctly refusing a stale-based
re-render, caused by heavy same-day concurrent OCC contract activity on
this exact ticket (occ#6550, occ#6590 both landed today). That guard did
its job: nothing corrupt was pushed. Hand-authoring the missing self-bind
entry + receipt here, mirroring the occ-self-bind-pr-6550/-6590 precedent
on this same contract exactly, so occ-preflight/eligibility and
verify/verify on OCC#6606 do not fail closed with pr_ticket_mismatch.

Pure append relative to the current dev tip (verified byte-identical to
this branch's own pass-1 commit before appending -- not a repeat of the
race): validator_occ_append_only.evaluate_append_only -> ok=True, zero
violations. validate_occ_merge_eligibility -> eligible=True. yamlfmt
-lint clean (hashes computed against the yamlfmt-stable bytes). Local
pre-commit on both changed files: 0 failures.

Evidence-Ticket: OMN-16071

* fix(OMN-16071): rebind pr-2086 downstream receipt's stale contract_sha256

The automated node_occ_companion_compute pass-1 minted
drift/dod_receipts/OMN-16071/dod-OmniNode-ai-omnimarket-pr-2086/command.yaml
with a legacy contract_sha256-only binding (no contract_entry_sha256,
and evidence_item_id "dod-OmniNode-ai-omnimarket-pr-2086" is not itself
declared as a dod_evidence item in contracts/OMN-16071.yaml -- a
pre-existing gap in the compute plan's "second product PR on an
already-companioned ticket" handling, unrelated to this fix). A
legacy-only binding goes stale on every later contract append regardless
of which entry changed (docs/standards/receipt_hashing_and_supersession.md
section 2) -- and the prior commit on this branch (occ-self-bind-pr-6606)
appended a new entry, staling this receipt's whole-file hash. Rebinding it
to the current contract hash is a pure edit of a file this same open,
unmerged PR (OCC#6606) already introduced -- it does not exist on dev, so
it is still a net "A" relative to the merge base regardless of how many
times it is edited before merge; not a mutate-in-place violation.

scripts/validation/check_receipt_hardening.py run locally against every
receipt in this PR's diff: 0 violations (was 1). validate_occ_merge_eligibility
-> eligible=True. validator_occ_append_only.evaluate_append_only -> ok=True.

Evidence-Ticket: OMN-16071

---------

Co-authored-by: node-occ-companion-effect <occ-companion-effect@omninode.ai>
Co-authored-by: Jonah Gray <jonah@omninode.ai>
@jonahgabriel
jonahgabriel merged commit 7bfc33b into dev Aug 17, 2026
193 of 222 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-16071-defect1-append-only-writer branch August 17, 2026 21:06
jonahgabriel added a commit that referenced this pull request Aug 23, 2026
…t growth, net-new for reused receipts (#2129)

* fix(OMN-16356): OCC append-only guard is content-verified for contract growth, net-new for reused receipts

The local pre-push append-only guard in both OCC companion producers
(OccCompanionEmitter and HandlerOccCompanionEffect) was hardened by
OMN-16071's PR #2086 to reject ANY non-'A' git status unconditionally,
including the ticket contract's own doctrinally-sanctioned growth (new
dod_evidence rows appended by _ensure_base_dod_evidence /
_append_self_bind_evidence). That made the local guard STRICTER than the
hosted OCC Append-Only Gate itself (validator_occ_append_only.evaluate_append_only,
which is per-entry: a new id is always allowed, only a removed/altered
existing id is a violation), and blocked every legitimate second companion
for an already-companioned ticket.

Live 2026-08-23: omnimarket#2124/OMN-15800, omnibase_infra#2790/OMN-15468,
and onex_change_control#6926/OMN-16413 all hit this regression across both
producers. A fourth failure (omnibase_infra#2766/OMN-16115) hit a related
but distinct case: a re-mint for a PR whose OWN downstream/CI receipts had
already merged under a stale/inherited Evidence-Source stamp (OMN-16386).

Fixes:
- _assert_append_only in both producers now re-verifies a contract-path 'M'
  against the SAME canonical evaluate_append_only judgment the hosted gate
  uses, instead of a blanket per-file status check. Receipts remain fully
  immutable (any non-'A' status still unconditionally rejected).
- OccCompanionEmitter's downstream/CI receipt writes now check for a prior
  merge before writing (mirroring the existing OMN-15785 admissibility-
  receipt guard), so a stale-Evidence-Source re-mint never reopens its own
  already-merged receipts for write.

Regression tests reproduce all four live failure shapes and are proven
RED-vs-EXISTS-but-WRONG against the pre-fix code.

Evidence-Ticket: OMN-16356

* fix(OMN-16356): pop PREPUSH_ALLOW_LOCAL_FULL_SUITE before the host-identity guard's refusal test

test_guard_refuses_full_suite_escalation_on_non_200_host copies the ambient
os.environ wholesale, unlike the file's own _run_hook helper which already
pops PREPUSH_ALLOW_LOCAL_FULL_SUITE (and other known-leaky vars) for the
identical reason. An ambient PREPUSH_ALLOW_LOCAL_FULL_SUITE=1 -- set by
whoever is running this suite, e.g. a developer using the documented
DEGRADED-HOST OVERRIDE to push past a loaded-host refusal moments earlier --
routes the hook through the override branch instead of the refusal this
test asserts, a false negative discovered live while pushing the OMN-16356
OCC append-only guard fix under a legitimately loaded host.

Evidence-Ticket: OMN-16356
jonahgabriel added a commit that referenced this pull request Aug 29, 2026
…filename shape (#2200)

* fix(OMN-16071): the OCC companion writer is add-only by path, not by filename shape

Defect 1, the defect this ticket is titled after, is the last open half of
OMN-16071 (Defect 2 landed in #2081/#2086). Its AC is literal: "the autobind
path must be strictly add-only -- never open an existing receipt file for
write." Every prior PR in this ticket's chain hardened the GUARD; none touched
the WRITER, which is why the ticket carries a standing DO-NOT-FLIP.

WHY THE EXISTING MECHANISM DID NOT CATCH IT. `assert_append_only_emissions`
(OMN-15485) is the producer-side invariant meant to make the class unreachable
from any emission site. It carried one exemption:

    if _SUPERSEDE_FILENAME_RE.search(filename):
        continue

That asks "is this filename shaped like a correction?", never "is this path
already merged?" -- and filename shape is not evidence of novelty. The merged
path files every rebind at `<check_type>.supersede.<pr_number>.yaml`, keyed to
the PRODUCT PR number, which is CONSTANT across every authoring pass for that
same PR. So the 2nd, 3rd ... pass renders the identical paths and the guard
waves each one through while they are already merged bytes on dev.

It could not have known better. `ModelOccContractState` carried
`existing_entry_ids` -- a set of DIRECTORIES -- and nothing about which FILES
are merged, so the blanket exemption was the only way to let a legal first
correction through.

LIVE, from this ticket's own record: OCC#6616 (companion for
omninode_infra#925) re-rendered eight `.supersede.925.yaml` files an earlier
pass for that same PR#925 had merged, bumping `created_at` 07:19:22Z ->
17:14:25Z in place, alongside the base receipt. Reproductions #1-#4 in the
ticket are the same shape through both producers.

WHAT CHANGED

* `ModelOccContractState.merged_receipt_paths` gives the pure planner
  file-level truth. Defaults to `()`, which reproduces the prior semantics
  byte-for-byte, so nothing in the merged corpus moves until the read-EFFECT
  populates it.
* `assert_append_only_emissions` refuses any emission at a path in that set,
  BEFORE the directory rule and regardless of filename -- the same judgment
  `evaluate_append_only` makes off git status. The supersede exemption survives
  only for paths nothing has merged.
* `_next_supersede_path` allocates a fresh generation keyed to the OCC
  companion PR authoring the rebind when the product-PR-keyed name is taken,
  and only for the keys that actually collide. The suffix stays a SINGLE
  integer because both this producer's `_SUPERSEDE_FILENAME_RE` and
  omnibase_core's `_SUPERSEDE_SUFFIX_RE` parse `.supersede.<NNNN>.yaml`
  strictly -- a compound `.supersede.925.6616.yaml` would be filed where
  nothing resolves it. The record's `replacement.pr_number` still names the
  PRODUCT PR, which is what `resolve_supersession`'s tier-1 selection keys on;
  the generation is provenance, never the binding. With no OCC PR number yet
  (pass 1) the emission is OMITTED and logged, not written -- the merged record
  already binds this same product PR with the same commit_sha / branch / probe,
  so suppressing the re-mint loses no evidence while opening it for write is
  the defect.
* Born path (`OccCompanionEmitter`): the slot receipt's skip was keyed to
  `contract_path.is_file()` -- a question about `contracts/<ticket>.yaml` used
  to decide whether to open a receipt under `drift/dod_receipts/`. Two
  different files; its siblings in the same loop already ask the direct
  question. It now also reads the receipt path itself. The contract half is
  RETAINED deliberately: minting a slot receipt into a pre-existing contract
  that does not declare that item would trade an append-only violation for an
  orphan receipt.
* The read-EFFECT lists the merged receipt files, scoped to `existing_entry_ids`
  (1-10 dirs in practice, not the ticket's whole tree). FAILS SOFT to `()`:
  a listing hiccup degrades to the shipped semantics rather than refusing a
  mint, and the write-EFFECT's pre-push `_assert_append_only` plus the hosted
  gate remain fail-closed behind it.

PROOF. Two new gates, 13 cases, RED-proven against dev tip b7eef36. The control
is stated precisely because a sloppier one would prove nothing: `merged_receipt_paths`
is a NEW member of a `extra="forbid"` model, so all 10 compute legs against a plain
`git checkout origin/dev -- src/` fail on `extra_forbidden` -- a schema error, which
says nothing about the guard. Isolating control, measured: dev `src/` with ONLY
`events/occ_companion.py` restored from this commit (the field, none of the logic)
-> **6 failed / 4 passed**. The 6 fail behaviourally on the shipped producer: it
re-emits the already-merged paths. The 4 controls stay green under that same
control (empty `merged_receipt_paths` is a no-op, a first supersede is still legal,
a plain receipt in a frozen dir is still refused), so the fix is not passing by
refusing everything.

The born-path leg needs no such isolation -- it drives `_emit_companion_sync`
directly. Against a plain dev `src/`: **1 failed / 2 passed**, the failure being
the writer rewriting a merged receipt's bytes, and the 2 passes being controls
that hold both directions (contract + receipt present still skips per OMN-15785;
a fresh ticket still mints, so the fix cannot pass by never writing and trading
the violation for a MISSING_RECEIPT companion). The consumer leg writes both
passes' real output to one tree and asks omnibase_core's `resolve_supersession`
the question occ-preflight asks. All 24 cases in the three touched gate files pass
on this commit.

`test_occ_append_only_producer_omn_15485.py`'s falsifier docstring claimed
"filename shape is what decides" for the whole invariant; it only ever
exercised the directory rule. Corrected in place rather than left to be read at
face value -- that reading is how the exemption went unexamined.

Re-measured after the rebase onto b7eef36, not carried over: 746 passed /
30 skipped across node_occ_companion_compute + node_pr_lifecycle_fix_effect +
node_occ_companion_effect + node_occ_state_effect with OCC_REPO_DIR set.
6178 passed / 38 skipped / 1 xfailed on the full unit suite. mypy --strict clean
across 3077 source files, ruff clean, pre-commit run --all-files clean.

KNOWN INERTNESS, stated rather than implied: node_occ_companion_compute and the
born-path emitter run in the .201 dev-lane effects runtime, not product-repo
CI. This code takes live effect at the next dev-lane refresh, so the live-corpus
proof for this fix is ARMED, not yet observed.

Closes OMN-16116 as a duplicate of this defect.

* fix(OMN-16071): the supersede-skip log names which of its two causes fired

CodeRabbit, correctly, on omnimarket#2200: `_next_supersede_path` returns
`None` for two distinct reasons and the log asserted the first one
unconditionally.

* Pass 1 has no `occ_pr_number` to key a fresh generation with, and pass 2 of
  the same mint WILL file it.
* A key whose product-PR-keyed AND OCC-PR-keyed generations are both already
  merged has nothing left to add, and no later pass will ever emit it.

The operator responses differ. Claiming "no OCC PR number is available yet" in
the second case sends a reader hunting for a number that is set, and promises a
pass that will never come.

RED-proven, not assumed: the new case fails against the shipped log text, and
its captured output is the false claim itself --

  skipping supersession for OMN-15922/dod-deploy-assessment - its
  product-PR-keyed path is already merged and no OCC PR number is available yet
  ... (emitted with occ_pr_number=6616 SET)

Both directions are pinned: the exhausted-generations branch must say "ALREADY
merged" / "no later pass will emit it" and must NOT say "no OCC PR number is
available yet"; the pass-1 branch must say the inverse. A one-directional
assertion would pass on a log that always said both.

747 passed / 30 skipped across the four affected node suites. ruff, mypy
--strict (3077 files) clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant