Skip to content

feat(OMN-16485): pre-mutation lane-ownership gate — cross-lane gh close/cancel/dispatch fails closed on the shared identity - #2028

Merged
jonahgabriel merged 1 commit into
devfrom
jonah/omn-16485-pr-ownership-guard
Aug 24, 2026
Merged

jonahgabriel merged 1 commit into
devfrom
jonah/omn-16485-pr-ownership-guard

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

OMN-16485 — pre-mutation lane-ownership gate for destructive gh verbs

Closes friction-report F-11 (Urgent): every concurrent lane on this host mutates GitHub through ONE shared gh identity (jonahgabriel), so per-command attribution is structurally INDETERMINATE and nothing mechanically stops a lane from closing a peer lane's PR. Observed >=5 times in 48h — omniclaude#2019 was authored by andywu42 and closed by jonahgabriel; a duplicate concurrent workflow_dispatch fired ~19s after a peer lane's on 2026-08-20T00:52Z because neither lane could see the other.

The mechanism existed and was never wired

plugins/onex/hooks/lib/pr_claim_registry.py (576 lines) already shipped canonical PR keys, atomic os.link acquire, and heartbeat/two-condition expiry — with zero production callers and zero tests. Textbook Operating Rule #5: a detector that is not a gate gets ignored. This PR wires it as the gate instead of inventing a second vocabulary (the OMN-15483 net-negative precedent).

What ships

1. plugins/onex/hooks/lib/pr_ownership_guard.py — pure decision core. No network I/O; ownership resolves from the filesystem claims registry + locally-resolved lane identity, safe for the PreToolUse hot path. Quote-tracking tokenizer so a commit message containing the literal text gh pr close is never read as a mutation.

Two mutation classes with deliberately different verdicts:

  • ownership (gh pr close/reopen, gh api -X PATCH .../pulls/<n> with state=closed) — FAIL-CLOSED. Absent, expired, unreadable, or lane-less claim REFUSES. "Nobody claimed it" is never read as "therefore anyone may." The refusal names the exact one-line claim command, so the escape hatch IS the act of producing the missing attribution record.
  • exclusivity (gh workflow run, gh run cancel) — first-writer-wins. An active peer claim refuses; otherwise the mutation is allowed AND the claim is recorded, which is what makes the racing second lane refuse.

Lane identity resolves deterministically: ONEX_LANE_ID/agent-name env → worktree path under the worktrees root (wt:<ticket>/<repo>, per Operating Rule #9 a real per-lane discriminator) → session id → None = INDETERMINATE = refuse.

2. plugins/onex/hooks/scripts/pre_tool_use_pr_ownership_guard.sh — PreToolUse Bash wrapper. Cheap grep pre-filter so commands with no guarded verb never invoke Python at all — a guard bug cannot brick unrelated Bash traffic. A command that DOES contain a guarded verb and whose evaluation errors is BLOCKED, not allowed. Gated by the existing BASH_GUARD bit (a new bit needs an omnibase_core EnumHookBit change + release chain; all 60 default-mask bits are allocated; pre_tool_use_bash_guard.sh's own header records gh-PR-mutation blocking as its historical scope).

3. pr_claim_registry gains lane_id in the claim schema (the field the guard reads) plus its first-ever test coverage. A claim without a lane proves someone holds the target but not which lane — INDETERMINATE, cannot authorize.

4. scripts/pr_claim_registry_cli.py gains the claim command — the escape hatch every refusal message points at. list now shows the owning lane.

Registration + enforcement, not doctrine

  • hooks.json v1.10.0 → v1.11.0: guard appended to the PreToolUse Bash matcher group (after the worktree guard), preserving the OMN-16471 lane-open and OMN-16478 lane-liveness carve-outs landed mid-flight.
  • tests/hooks/test_hooks_baseline_omn_13244.py asserts the EXACT five-command PreToolUse set, so a silently-dropped guard fails CI.
  • Wrapper classified in plugins/distribution_manifest.yaml (Phase-0 classification gate, 273/273 OK).

Acceptance criteria → tests (all falsifiable)

  1. Cross-lane gh pr close refused, refusal names the owner — test_peer_lane_close_is_refused_and_names_the_owner, test_the_observed_incident_is_refused (the omniclaude#2019 shape).
  2. INDETERMINATE fails closed — test_unresolvable_lane_fails_closed, test_unreadable_claim_fails_closed, test_laneless_claim_fails_closed, test_evaluate_fails_closed_on_corrupt_claim_file.
  3. The gate discriminates — test_owner_may_close_its_own_pr, test_evaluate_allows_owner_and_refuses_peer.
  4. Expired claim does not strand a PR — test_expired_claim_does_not_strand_the_pr_forever (re-claim succeeds after both expiry conditions).
  5. Duplicate concurrent dispatch refused cross-lane, same-lane re-dispatch allowed — test_racing_peer_dispatch_is_refused, test_same_lane_redispatch_is_allowed.
  6. Registry coverage — test_registry_persists_lane_id, test_registry_refuses_second_lane_while_claim_is_active, test_registry_release_is_scoped_to_the_holding_run.

Also fixed in passing: _claim_path() read the module-level CLAIMS_DIR global directly, which is None until first use — the function raised TypeError on first call. Now resolves lazily via _get_claims_dir().

dod_evidence

$ uv run pytest tests/unit/hooks/test_pr_ownership_guard.py tests/hooks/test_hooks_baseline_omn_13244.py -q -p no:randomly
47 passed in 0.10s

$ uv run pytest tests/hooks/ tests/unit/hooks/test_pr_ownership_guard.py -q -p no:randomly
3715 passed, 23 skipped in 191.73s

$ uv run mypy --strict plugins/onex/hooks/lib/pr_ownership_guard.py plugins/onex/hooks/lib/pr_claim_registry.py
Success: no issues found in 2 source files

$ uv run python scripts/validation/validate_distribution_classification.py
validate_distribution_classification: OK — 273 discovered component(s) all classified (273 manifest entries).

$ uv run pre-commit run --files <8 changed files>
(no Failed hooks)

$ shellcheck plugins/onex/hooks/scripts/pre_tool_use_pr_ownership_guard.sh
(SC1091 info only — same shape as the sibling OMN-15213/OMN-16471 wrappers)

44 new unit tests. Governed pre-push selector ran and passed on push (secret detection, enum governance, __all__ exports, DoD advisory). No PREPUSH_FULL_SUITE, no ENABLE_SMART_TESTS=off, no --no-verify.

Explicitly out of scope (Phase 2, separate ticket)

Per-lane GitHub App installation tokens — the credential-plane change that makes mutations attributable at GitHub (timeline.actor.login resolving to the lane). Needs an org-level App install, Infisical-held private key, short-TTL per-lane token minting, and coordinated shared-PAT retirement (with OMN-16373). Phase 1 makes mutations refusable and locally attributable today without blocking on any of that.

Residual, stated plainly

This guard binds the local Bash chokepoint every interactive lane uses. A lane calling the GitHub API from Python code, or a process outside the Claude hook surface, is not intercepted — that is what Phase 2's per-lane credentials close. Co-located lanes sharing one worktree collapse to one lane id (deliberate under-block: it can permit a mutation between co-located lanes, never blocks an unrelated lane's own work).

Lane provenance: this work was started by the friction-f11-lane-identity lane (ledger CLAIM 2026-08-24T12:59:42Z), which died in the session-limit outage with the work uncommitted; the friction-f11-reconcile lane superseded the claim (ledger 15:05:25Z), reviewed, rebased onto dev across the OMN-16471/OMN-16478 hook merges, and landed it.

Refs: OMN-16485; omni_home/docs/tracking/2026-08-24-system-friction-report.md §F-11. Precedents: OMN-15483 (extend the shipped vocabulary), OMN-14330 (carve-out registration), OMN-13244 (baseline assertion). Siblings landed mid-flight: OMN-16471, OMN-16478.

Evidence-Ticket: OMN-16485
Evidence-Source: OCC#7012

…verbs

Wires the zero-caller pr_claim_registry as a PreToolUse Bash guard: gh pr
close/reopen and api-PATCH state=closed fail closed on absent/expired/
unreadable/lane-less claims; gh workflow run / gh run cancel are
first-writer-wins exclusivity. Lane identity resolves ONEX_LANE_ID ->
worktree path -> session id, INDETERMINATE refuses. Registry gains
lane_id + tests; CLI gains the claim escape-hatch command.
@jonahgabriel
jonahgabriel enabled auto-merge (squash) August 24, 2026 15:16
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 14 minutes.

View limit details

Limit details: You’ve used the included review currently available. Your 123 included PR review attempts over the past 7 days set your current allowance 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).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e9add41-cec1-47c9-b1f4-a5cecedab2b1

📥 Commits

Reviewing files that changed from the base of the PR and between 85b7a26 and 4cf30ed.

📒 Files selected for processing (9)
  • .secrets.baseline
  • plugins/distribution_manifest.yaml
  • plugins/onex/hooks/hooks.json
  • plugins/onex/hooks/lib/pr_claim_registry.py
  • plugins/onex/hooks/lib/pr_ownership_guard.py
  • plugins/onex/hooks/scripts/pre_tool_use_pr_ownership_guard.sh
  • scripts/pr_claim_registry_cli.py
  • tests/hooks/test_hooks_baseline_omn_13244.py
  • tests/unit/hooks/test_pr_ownership_guard.py

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

@jonahgabriel
jonahgabriel disabled auto-merge August 24, 2026 15:16
jonahgabriel pushed a commit to OmniNode-ai/onex_change_control that referenced this pull request Aug 24, 2026
…iclaude#2028 (#7012)

* evidence(OMN-16485): author OCC companion for OmniNode-ai/omniclaude#2028

OCC companion by node_pr_lifecycle_fix_effect (OMN-13317 F1 / OMN-13990 / OMN-14285). Product PR head 4cf30edcba6c459bc9536b140db444ca4995330e.

* evidence(OMN-16485): self-bind OCC#7012 + rebind contract_sha256

---------

Co-authored-by: omnimarket-bot <bot@omninode.ai>
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Hostile Reviewer — DEGRADED (informational)

Blocking findings (critical/error): 0
Total findings: 0
Models succeeded: none

Note: All reviewer models failed or were unavailable. Degraded results are informational during the pilot phase (OMN-8524) and do not block merge.


Gate semantics (pilot phase)

Verdict Meaning Blocks merge?
passed No critical/error findings No
blocked CRITICAL or ERROR findings found Yes
degraded Reviewer ran but every model failed mid-run No (pilot)
infra_error cli_review failed to run at all (install/crash) after 2 attempts Yes

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

@jonahgabriel
jonahgabriel merged commit 4d4edb6 into dev Aug 24, 2026
258 of 301 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-16485-pr-ownership-guard branch August 24, 2026 16:00
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.

1 participant