Skip to content

S2 slice 1: apply-input gate (validate + hash binding + source guards)#287

Merged
PhysShell merged 2 commits into
mainfrom
claude/s2-rewriter-inputs
Jul 16, 2026
Merged

S2 slice 1: apply-input gate (validate + hash binding + source guards)#287
PhysShell merged 2 commits into
mainfrom
claude/s2-rewriter-inputs

Conversation

@PhysShell

@PhysShell PhysShell commented Jul 16, 2026

Copy link
Copy Markdown
Owner

S2 slice 1 — apply-input gate (validate + hash binding + source guards)

First slice of S2 (steps 1–3 of the accepted order). Pure Python; nothing parses C# or writes a byte. It is the untrusted-input gate that must pass before the later-slice Owen.CSharp.Rewriter is allowed to touch source.

validate_apply_inputs(validated_plan, candidates, root) takes the two hash-bound inputs the arbiter mandated — the validated-plan is the sole authority for action; the candidates bundle is the untrusted-but-hash-bound source of the event/source/handler identity + containing type the rewriter's span-node guard will need — and:

  • validates the validated-plan shape (version/operation, one type/file, canonical paths + SHA form, decisions carry only {finding_id, action, file, acquire_span} with an S1-only action);
  • re-validates the candidates via the shared validate_candidates_bundle;
  • binds them (bundle_sha256(candidates) == validated_plan.input_bundle_sha256; target_api / source_files must match);
  • cross-checks each decision against its candidate (full bijection, exact file+span, action ∈ candidate's allowed_actions, convert_acquire only on an inotify_property_changed contract);
  • refuses overlapping convert_acquire spans;
  • guards the source: root-confined + on-disk bytes must match the pristine preimage SHA (STALE SOURCE refusal), never fuzzy relocation.

Returns an apply context once every guard passes; every violation is a controlled ApplyError.

tests/test_fix_apply.py — 18 checks. ruff + mypy (ownlang) + run_tests green.

Next slice: the separate Owen.CSharp.Rewriter (Roslyn span-node identity guard + deterministic in-memory edits + postimage/patch/manifest, steps 4–8), then self-gates + analyzer delta + the 007 target-gate contract.

Frozen (S2-initial): no source-tree write, no auto git apply, no model-authored rewrite data, no fuzzy relocation, no partial apply, no teardown/remove conversion, no using/helper changes, one type/one file, human applies the patch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a strict “apply-input gate” that validates apply plans and candidate inputs before running later rewrites.
    • Added safeguards for malformed, mismatched, stale, inconsistent, or out-of-scope changes, including source integrity and root-confinement checks.
    • Improved handling of automatic conversions and manual-review items, including overlap/spans validation.
  • Bug Fixes

    • Tightened candidate bundle validation to require all identity-related fields.
  • Tests

    • Added comprehensive tests covering successful application context creation and many validation rejection scenarios.

…g + source guards

S2 slice 1 (steps 1-3 of the accepted order): the untrusted-input gate that must pass
before the later-slice Owen.CSharp.Rewriter is allowed to touch a byte of source. Pure
Python; nothing here parses C# or writes files.

validate_apply_inputs(validated_plan, candidates, root) takes the TWO hash-bound inputs
the arbiter mandated — the validated-plan is the sole authority for `action`; the
candidates bundle is the untrusted-but-hash-bound source of the event/source/handler
identity + containing type the rewriter's span-node guard will need. It:

- validates the validated-plan shape (validate_validated_plan): version/operation, one
  type/file, canonical paths + SHA form, decisions carry only {finding_id, action, file,
  acquire_span} with an S1-only action;
- re-validates the candidates via the shared validate_candidates_bundle;
- BINDS them: bundle_sha256(candidates) == validated_plan.input_bundle_sha256, and
  target_api / source_files must match;
- cross-checks every decision against its candidate: full bijection, exact file + span,
  action in that candidate's allowed_actions, convert_acquire only on an
  inotify_property_changed contract;
- refuses overlapping convert_acquire spans;
- guards the source: root-confined (_resolve_source) + the on-disk bytes must match the
  pristine preimage SHA (STALE SOURCE refusal) — never fuzzy relocation.

Returns an apply context (convert_acquire targets with their candidate identities, the
manual_review ids, the source path, the pinned target API) once every guard passes.
Every violation is a controlled ApplyError, never a traceback or a partial apply.

tests/test_fix_apply.py (18 checks): happy path, hash-binding mismatch, target/file/span
disagreement, action-not-allowed, unknown/missing/duplicate decision, overlapping spans,
unknown fields, out-of-scope action, stale preimage SHA, uncontained source. ruff + mypy
(ownlang) + run_tests green.

Next slice: the separate Owen.CSharp.Rewriter (Roslyn span-node identity + deterministic
edits + postimage/patch/manifest, steps 4-8).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 66be0454-08b5-424c-9969-8181cc7fa676

📥 Commits

Reviewing files that changed from the base of the PR and between 1c577d7 and ed575e0.

📒 Files selected for processing (3)
  • ownlang/fix_apply.py
  • ownlang/fix_candidates.py
  • tests/test_fix_apply.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • ownlang/fix_apply.py

📝 Walkthrough

Walkthrough

Adds a strict apply-input gate that validates plans, binds them to candidate bundles, checks decision consistency and edit overlap, verifies confined source preimages, and returns conversion/manual-review context. Tests cover successful application and malformed, stale, inconsistent, or out-of-scope inputs.

Changes

Apply-input validation

Layer / File(s) Summary
Validated-plan contract
ownlang/fix_apply.py
Defines ApplyError and validates plan structure, allowed fields, versions, operations, paths, decisions, actions, and spans.
Candidate binding and source guard
ownlang/fix_apply.py, ownlang/fix_candidates.py
Binds candidate and plan hashes and metadata, validates candidate identity fields, cross-checks decisions, rejects invalid or overlapping conversions, and verifies confined source hashes.
Apply-gate validation coverage
tests/test_fix_apply.py
Adds deterministic acceptance and rejection tests for binding, decision consistency, schema validation, stale sources, overlaps, and root confinement.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ValidatedPlan
  participant ApplyGate
  participant Candidates
  participant SourceFile
  ValidatedPlan->>ApplyGate: validate plan structure
  Candidates->>ApplyGate: provide candidate bundle
  ApplyGate->>Candidates: verify hash and decision metadata
  ApplyGate->>SourceFile: resolve confined path and calculate sha256
  SourceFile-->>ApplyGate: source preimage hash
  ApplyGate-->>ValidatedPlan: return apply context or ApplyError
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description contains the substance, but it does not follow the repository template or include the required sections and checklist items. Rewrite the PR body to use the template headings: Что и зачем, Тип изменения, Как проверено, Связанные issue, and Чеклист.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main change: an S2 apply-input gate with validation, hash binding, and source guards.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/s2-rewriter-inputs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c577d7099

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread ownlang/fix_apply.py
"acquire_span": c["acquire_span"],
"containing_type": c["containing_type"],
"event": c["event"],
"event_identity": c["event_identity"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate event_identity before indexing it

When a hash-bound candidate with convert_acquire omits event_identity, validate_candidates_bundle() still accepts it because that shared validator never requires this field, but this indexing raises KeyError instead of the promised controlled ApplyError. Such a bundle also passes validate_plan(), so malformed JSON can traverse the normal planning flow and crash the apply gate; require the identity field during candidate validation or normalize the missing-field error here.

Useful? React with 👍 / 👎.

Comment thread ownlang/fix_apply.py Outdated
canonical, abs_path = _resolve_source(root, source["path"])
except CollectError as exc:
raise ApplyError(f"source path: {exc}") from exc
if _sha_file(abs_path) != source["sha256"]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Convert source read failures into ApplyError

If the source is a regular file but unreadable by the current user, _resolve_source() succeeds because os.path.isfile() does not check read permission, and this call then lets PermissionError escape; deletion or replacement between resolution and opening has the same result. This violates the gate's ApplyError-only contract and produces an uncontrolled traceback rather than a clean refusal, so the OSError from hashing should be normalized.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_fix_apply.py (1)

196-203: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Exercise an actual root escape.

N/C.cs remains inside root2; this only tests a missing file. Add a canonical in-root symlink pointing outside the root and verify rejection, retaining the missing-file case separately if desired.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_fix_apply.py` around lines 196 - 203, Update the root-confinement
test around _resolve_source to create a regular in-root path whose canonical
resolution escapes root2, such as an in-root symlink targeting a file outside
root2, and assert raises rejects that candidate. Keep the existing missing-file
assertion as a separate case if desired, but ensure the test explicitly
exercises canonical root escape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ownlang/fix_apply.py`:
- Around line 65-68: Update the validation around the selected type in the
plan-processing flow near _field(plan, "selection", ...) and the allowed_types
length check: validate the sole entry’s full_name and file fields, then require
every decision candidate to match that selected type’s identity before applying
it. Keep rejecting plans whose allowed_types does not contain exactly one entry.
- Around line 172-176: Update the source validation flow around _resolve_source
and _sha_file so any OSError raised while reading the source file is caught and
converted into ApplyError, preserving the controlled-refusal behavior used for
CollectError. Keep the existing SHA mismatch handling unchanged.

---

Nitpick comments:
In `@tests/test_fix_apply.py`:
- Around line 196-203: Update the root-confinement test around _resolve_source
to create a regular in-root path whose canonical resolution escapes root2, such
as an in-root symlink targeting a file outside root2, and assert raises rejects
that candidate. Keep the existing missing-file assertion as a separate case if
desired, but ensure the test explicitly exercises canonical root escape.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6427447c-d727-4c23-9b10-33c92651c9db

📥 Commits

Reviewing files that changed from the base of the PR and between 139a756 and 1c577d7.

📒 Files selected for processing (2)
  • ownlang/fix_apply.py
  • tests/test_fix_apply.py

Comment thread ownlang/fix_apply.py
Comment thread ownlang/fix_apply.py Outdated
…y, normalize read OSError

Closes the three S2-slice-1 blockers (arbiter). No new scope.

Blocker 1 — the validated-plan's authority envelope was only shape-checked (one type/one
file) and compared to the candidates by RAW dict equality, so a self-consistent forged
pair could smuggle a wrong type/file, wrong constraints, mismatched selected_findings, or
extra nested fields. validate_apply_inputs now builds the CANONICAL projection of
target_api / selection / source_files from the candidates' known keys and requires the
plan to EQUAL it exactly.

Blocker 2 — the code indexed c["event_identity"], but the shared candidates validator did
not require it, so a hash-bound candidate missing an identity field passed validation and
the apply gate raised a bare KeyError (violating "every violation -> ApplyError").
validate_candidates_bundle now requires the full identity set (event/source/handler +
*_identity + *_identity_kind) as strings, and the apply context carries all of them (not
just the human display) for the future span-node guard.

Blocker 3 — the preimage SHA read (_sha_file) sat AFTER `except CollectError`, so an
OSError between the root-confinement check and the read (file vanished / permission /
replaced) leaked as a traceback. The resolve + read are now under one try with an added
`except OSError -> ApplyError`.

tests/test_fix_apply.py grows to 32 checks: canonical-projection mismatches (wrong
type/file/constraints/selected_findings, unknown nested field in target/selection/source),
missing/wrong-typed candidate identity fields, the full-identity-context assertion, and a
source-read OSError (chmod-0, capability-guarded) -> ApplyError. ruff + mypy (ownlang) +
run_tests green; S1 unaffected by the stricter candidates validator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G
@github-actions

Copy link
Copy Markdown

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PhysShell
PhysShell merged commit 25a3b25 into main Jul 16, 2026
44 checks passed
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