Skip to content

feat(mediated): implement the constrained replacement executor (CR-OC-001C) - #128

Merged
coreytshaffer merged 9 commits into
mainfrom
cr-oc-001c-constrained-replacement-implementation
Aug 1, 2026
Merged

coreytshaffer merged 9 commits into
mainfrom
cr-oc-001c-constrained-replacement-implementation

Conversation

@coreytshaffer

@coreytshaffer coreytshaffer commented Aug 1, 2026 •

Copy link
Copy Markdown
Owner

CR-OC-001C implementation — hosted Windows and final §26 evidence CI passed

Implements the merged CR-OC-001C contract (requirements PR #125, implementation proposal PR #126, §10.1a amendment PR #127) within the accepted seven-path allowlist.

Hosted implementation evidence passed on run 30694001033. The final documentation-only §26 evidence commit passed complete CI on run 30694417116 at head dec98b1, using generated merge ref 86d43b7.

The sections below are a chronological record, kept so the reasoning is auditable. Sections describing earlier hosted runs are marked superseded and describe the state at that time, not the current state. Current state is the §10.2b section and the hosted-evidence section near the end.

Hosted run 30686689771 (SUPERSEDED) — the first attempt failed

Run 30686689771 exercised the windows_executor job against the branch as it then stood:

NTFS verification            PASSED
mandatory executor group     FAILED — 131 passed, 34 failed
all 34 failures uniform      metadata_precondition_failed
structured-result gate       correctly did not run after the failure

Every failure is the §10.2 pre-mutation owner gate. The gate compared the target's owner against GetTokenInformation(TokenUser), but TokenUser identifies the token's user account, whereas TokenOwner is the default owner Windows applies to newly created objects — which is the quantity the gate actually needs, since its purpose is to establish that the temporary file this process creates will carry an owner compatible with the target. ReplaceFileW documents preservation of the replaced file's DACL and several attributes but not the owner, which is why the precondition must reason about the replacement object's real default owner.

Local evidence had been green because the ownership gate passed in the development environment; no persisted evidence established the relationship between TokenUser and TokenOwner there. The hosted run does not disclose SID values and therefore does not establish the exact SID relationship on that runner either — what it established is that the gate reached its closed refusal path while the implementation was comparing against the wrong quantity.

The §10.2a TokenOwner amendment merged through PR #129 as 8e19cdf, and the implementation correction is now applied on this branch: _TOKEN_OWNER_CLASS = 4, a _TOKEN_OWNER structure, process_default_owner_sid, the TokenUser machinery removed entirely, T20 revised into three parts, and M30 killed cleanly by a deterministic instrumented information-class assertion.

Hosted run 30689133402 — the ownership gate is fixed, a different issue remains

pytest 3.10 / 3.11 / 3.12   passed
NTFS verification           passed
mandatory executor group    160 passed / 8 failed / 1 deselected
structured-result gate      skipped after the test failure

The TokenOwner correction worked at the precondition layer: the 34 blanket metadata_precondition_failed refusals are gone, T20's genuine created-file/default-owner equality test passed, and M30 passed.

The eight remaining failures are healthy replacement cases returning metadata_preservation_failed / replacement_may_have_occurred — a post-replacement metadata-comparison problem, not the original ownership-gate problem. Affected: T35, T19, T14, T33, T20's ordinary-replacement control, T1, T28W, T30W.

A second hosted issue: pytest warned that record_property is incompatible with the default xunit2 JUnit family, which does not permit testcase-level <property> elements. The transition gate would therefore have had nothing to read. The Windows invocation now passes -o junit_family=legacy; pyproject.toml is untouched and the Ubuntu jobs are unaffected.

Hosted run 30689442321 — the differing components are now identified

The bounded diagnostic commit resolved the unknown. On an ordinary target a successful ReplaceFileW changed exactly three components:

dacl_auto_inherited, ace_count, ace_bytes_or_order

The junit_family=legacy correction also worked — the record_property incompatibility warning is gone, and the gate did not run only because the mandatory command correctly failed first. The run otherwise repeated the previous pattern (pytest 3.10/3.11/3.12 passed, NTFS passed, 160 passed / 8 failed / 1 deselected).

This is a contract-level stop, not an implementation bug to code around. A successful replacement altered the automatic-inheritance control state, the number of ACEs, and the complete ordered ACE sequence. Microsoft documents ReplaceFileW as preserving the DACL, but its parameter documentation also describes the operation as merging attribute and ACL information into the replacement file; it does not promise byte-identical ACE enumeration in every inheritance environment. Windows automatic inheritance can set SE_DACL_AUTO_INHERITED, materialize inherited ACEs, and order inherited entries after explicit ones.

That inheritance recomputation is the cause remains an inference. The labels do not establish that every new or changed ACE was inherited, nor that effective access was unchanged.

No relaxation is being considered. The ACE comparison is not being loosened, the hosted transition is not being accepted as another exception, unordered or partial comparison is not being adopted, and no claim is made that ReplaceFileW preserves the required DACL invariant. A "same effective permissions" rule would be materially harder to prove safely than exact equality: ACE order affects access decisions, and common effective-rights helpers omit owner rights, privileges, logon-session groups, resource-manager policy, and some inherited-deny cases.

Exact DACL preservation by bare ReplaceFileW is under contract-level investigation and is NOT established.

Paired inheritance control (SUPERSEDED — the hosted result is recorded below)

A second test-only diagnostic adds a paired genuine-NTFS control that decides between the two clean outcomes. Both cases run the same replacement and the same exact comparison, and emit field labels and booleans only — no SID, ACE, access mask, descriptor, SDDL, path, or command output:

  1. Inherited-DACL case — the ordinary target, expected to reproduce the three hosted labels.
  2. Protected-DACL case — inheritance disabled while the effective ACEs are copied in as explicit entries, via a narrow test-side SetSecurityInfo call with PROTECTED_DACL_SECURITY_INFORMATION rather than parsing icacls output.

The protected control records pre_dacl_protected, pre_dacl_nonempty (so a trivially empty DACL cannot pass by comparing equal for the wrong reason), replacement_result, and metadata_differences. A fixture that fails to protect fails rather than skips, since the mandatory group permits zero skips.

Local NTFS result — the protected profile shows no difference at all, not even the accepted monotonic bit:

inherited   pre_dacl_protected=False  nonempty=True  result=ok  differences=dacl_auto_inherited
protected   pre_dacl_protected=True   nonempty=True  result=ok  differences=none

The local environment does not reproduce the hosted ace_count / ace_bytes_or_order change, so this is suggestive only and settles nothing until the pair runs on hosted Windows. The decision it will inform:

  • Protected target passes exact comparison → the safe amendment narrows CR-OC-001C to targets whose DACL is present and protected from inheritance, with unsupported profiles refused as metadata_precondition_failed before temporary-file creation. This preserves the exact owner and ordered-ACE invariant rather than weakening it, at a cost in usability.
  • Protected target still changes → ReplaceFileW cannot satisfy the accepted invariant even in the protected profile, and the implementation stops while the contract either selects a different replacement mechanism or explicitly prepares and verifies the replacement object's security descriptor under a new mutation sequence.

Neither path is being chosen before that control runs hosted.

Hosted run 30690450931 — the inherited case reproduced; the protected control was under-asserted

pytest 3.10 / 3.11 / 3.12   passed
NTFS verification           passed
mandatory group             161 passed / 9 failed / 1 deselected
structured gate             skipped after test failure

The inherited-DACL control reproduced the earlier hosted result exactly — result=metadata_preservation_failed, differences=dacl_auto_inherited,ace_count,ace_bytes_or_order.

The protected-DACL test was not among the failures, and that is not yet evidence. As written it failed only on a metadata label beyond dacl_auto_inherited. It did not assert that the reason code was ok, that the outcome was replacement_verified, or that the proposed bytes reached the target — so a refusal such as temp_creation_failed accompanied by no metadata differences would also have passed it. The test did record protected_replacement_result into JUnit, but the mandatory command failed before the gate or summary exposed that property and no artifact is uploaded, so the value is not recoverable from the log.

What run 30690450931 establishes about the protected profile is only: the fixture was genuinely protected and nonempty, and no unaccepted metadata difference caused a failure. It does not establish that a successful protected replacement preserved the invariant. That is a narrow test defect, not evidence against the protected-DACL approach.

The contract decision therefore remains open. No amendment is proposed and no production change is made.

Strengthened protected control (test-only)

The protected control now requires all of: fixture protected, fixture nonempty, reason_code == "ok", outcome == replacement_verified, the target's bytes equal the proposed bytes, and metadata differences none-or-dacl_auto_inherited-only. The byte check reports a fixed protected_post_bytes_mismatch message rather than printing values, keeping the diagnostic discipline uniform even though the test bytes are not sensitive.

pre_dacl_nonempty remains an evidence-control condition, not a production requirement — its only job is to prove a hosted success was not a trivial empty-ACL comparison.

Each new assertion was verified to actually fail when it should, by forcing the condition under a scratch-only pytest plugin (never committed):

reason_code forced to a refusal   -> protected_replacement_result=temp_creation_failed metadata_differences=none
outcome forced to may-have-occurred -> protected_replacement_outcome_not_verified
expected bytes perturbed          -> protected_post_bytes_mismatch

The first attempt at that verification reported a false pass because the plugin patched a second copy of the module imported under a different name at pytest_configure time; patching the collected module at pytest_collection_finish showed all three assertions firing. An unverified control is exactly the failure mode this commit exists to fix.

Hosted run 30691068391 — the protected-profile amendment path is selected

pytest 3.10 / 3.11 / 3.12   passed
NTFS verification           passed
mandatory group             161 passed / 9 failed / 1 deselected
structured gate             skipped after mandatory failure

The inheritance-enabled control again failed with result=metadata_preservation_failed, differences=dacl_auto_inherited,ace_count,ace_bytes_or_order. The strengthened protected control was collected and was neither failed nor skipped, so every condition it enforces passed on hosted Windows:

pre DACL protected
pre DACL nonempty
reason_code == ok
outcome == replacement_verified
target bytes == proposed bytes
metadata differences == none or dacl_auto_inherited only

Stated narrowly, and this is all it establishes: on the hosted runner the unprotected profile violated the exact invariant, while the present-and-protected profile completed a verified replacement under the exact same comparison. It does not prove inheritance recomputation is the underlying Windows mechanism.

The job remains red because the executor still attempts replacement on ordinary inheritance-enabled targets. That is now the defect to close — not a reason to weaken post-verification.

Amendment PR #130 (documentation-only, three paths, branched from main at 8e19cdf) proposes §10.2b: execution is supported only for targets whose pre-mutation DACL is present, non-NULL, and protected from inheritance; absent, NULL, and inheritance-enabled DACLs refuse with metadata_precondition_failed / not_attempted before temporary-file creation. The exact invariant, the SE_DACL_AUTO_INHERITED monotonic rule, and mandatory post-verification are all unchanged. T21 is revised rather than supplemented (obligations stay at 36); mutants go to 31 (21 Windows, 10 Ubuntu) with the new M31.

§10.2b implemented — amendment merged as 5e41d6f

PR #130 merged as 5e41d6f, and this branch now incorporates amended main by merge (no rebase, no force-push) and implements the §10.2b correction.

Production — one gate in mediated_executor.py, evaluated immediately after the pre-mutation snapshot validates and before process_default_owner_sid(), so an unsupported profile ceases without a needless token query:

DACL absent, NULL, or not protected  ->  metadata_precondition_failed / not_attempted
                                          before temp-file creation and before ReplaceFileW

The executor only inspects and refuses. It never protects, normalizes, repairs, or otherwise alters a target's DACL to make it eligible. ACE count deliberately does not participate — a present, non-NULL, protected DACL with zero ACEs is supported.

Fixtures — healthy Windows targets are placed into the supported profile by construction, disabling inheritance while copying the effective ACEs in as explicit entries and leaving the owner untouched. All fixture preparation lives in the tests; a fixture that fails to protect raises rather than skipping.

Evidence revised as contracted:

T20 part 3 supported-profile control; asserts present + protected pre-state, post owner == pre owner
T21 (a) genuine unprotected NTFS target refuses; cessation at capture_security; no temp/replace/write/delete; no token query; bytes intact; no artifacts; DACL still unprotected afterwards
T21 (b) seam-injected absent and NULL states refuse identically
T21 (c) supported control requires ok, replacement_verified, exact proposed bytes, complete metadata invariant
T21 (c) boundary present + protected + zero ACEs is supported — the deterministic guard that pre_dacl_nonempty never leaked into production policy
T35W confirms the supported profile before replacing

The two temporary diagnostic tests were converted into their contracted T21 homes, not left as parallel controls, and T35W now shares the single difference classifier so the two cannot drift.

One observation worth recording: with a protected fixture the runner now exhibits True->True rather than False->True. Both are accepted under §10.1a — which is exactly why that section refused to require the normalization. That refusal is now load-bearing rather than hypothetical.

Local evidence — observed, not carried forward

Windows focused     149 passed / 1 optional skip
Windows mandatory   173 passed / 1 deselected / 0 skipped   (structured gate PASS)
Ubuntu neutral       96 passed / 54 skipped  + 24 guards + 640-file manifest
Full repository    1664 passed / 6 skipped
Mutants            31/31 killed (21 Windows, 10 Ubuntu), zero survivors

All 31 anchors were pre-validated as unique and CRLF-normalised before any cycle ran; byte-exact restoration and a clean manifest before and after every cycle; JUnit privacy scan reports 0 SIDs, 0 access masks, 0 SDDL.

Two harness problems surfaced and were fixed rather than worked around: docker cp wrote container files as root so the first Ubuntu restore raised PermissionError — the source was verified unmutated (hash matched pristine, the failure was on the first write) before continuing; and an ephemeral .pytest_cache entry invalidated the manifest, so the manifest now covers source only.

The §26 evidence commit has passed complete CI. This PR is ready for review under the final merge gate.

What cannot be claimed

  • Hosted Windows validation HAS now been achieved — for the tested merge ref only. It is not a universal Windows guarantee and is not evidence about any later commit.
  • True->True is one accepted observation, not required platform behaviour. §10.1a accepts False->False, False->True, and True->True alike.
  • The optional symlink probe skipped locally; the hosted supplemental probe passed. Neither contributes to acceptance.
  • CR-OC-001C is not complete, not merged, not accepted, and not runtime-integrated. No runtime module imports either new module, and merge authority has not been granted.
  • Nothing here establishes hostile-writer safety, cross-process exclusion, OpenClaw containment, caller/broker authentication, capability or reservation integration, exactly-once execution, or that observing a postcondition proves this invocation caused it.

Seven paths, no eighth

triage_core/mediated_executor.py         new   policy core
triage_core/mediated_executor_win32.py   new   Windows mechanism adapter
tests/test_mediated_executor.py          new   36-obligation suite
.github/workflows/tests.yml              modified, purely additive
docs/change/requests/CR-OC-001C-…-executor.md   Part III / §26 record
docs/current_backlog.md
docs/change/change_log.md

No dependency added, pyproject.toml untouched, and authz.py, capability_claims.py, request_reservation.py, task_ledger.py, client.py, tc_cli.py all unmodified.

Module split and one-way dependency

The adapter imports no TriageCore module and owns a private Win32SecurityCapture of primitive observed facts. The core imports the adapter dynamically, only after its Windows gate, and converts that capture through snapshot_from_capture into the core-owned SecuritySnapshot — where the three-valued DACL classification, comparison policy, pure ReplaceFileW result classification, the closed vocabularies, and the privacy-gated projection live. No third bridging module.

Outcomes derive from reason codes through a single REASON_TO_OUTCOME mapping, so an outcome can never be chosen independently of the condition observed.

The sixteen-step sequence issues exactly one ReplaceFileW call, with no automatic rollback, no retry, no path-based fallback, no best-effort ACL parsing, and no cleanup once a state is ambiguous — proven by a mechanism call-log instrument asserting the filesystem call log ends at the exact failing observation, with delete_file absent and the backup retained.

Three defects found by building the contract

  1. SE_DACL_AUTO_INHERITED strict equality was unimplementable. A successful ReplaceFileW sets the bit while preserving owner, DACL state, revision, ACE count, and byte-identical ACEs. Amended to the monotonic rule (§10.1a, PR docs(change): correct the CR-OC-001C DACL auto-inheritance requirement #127) before any test hardened it.
  2. Directory targets reported the wrong reason code. A directory cannot be opened without FILE_FLAG_BACKUP_SEMANTICS, so the open failed and reported containment_violation where §19 T8 requires target_not_regular_file. The adapter now opens with directory-capable flags and the core classifies the opened object.
  3. Final-target reparse rejection was missing before CreateFileW. §7.2 requires rejection "for the target and every ancestor"; the first implementation walked ancestors only. has_reparse_target is now a separate, independently-mutable check running before any target handle exists. Exposed by the M3/M4 mutant analysis.

Evidence

31/31 mutants killed through intended behavioural assertions — 10 in an Ubuntu container (triagecore-ubuntu-mutants:24.04, Python 3.12.3), 21 on real NTFS. Zero survivors. Zero syntax/import/collection/fixture/timeout failures counted as kills. Byte-exact restoration verified after every cycle. All 31 were rebound against the final source and killer text — the shared Windows fixture and several designated killers changed with §10.2b, so a complete rebinding was cleaner than function-equivalence arguments. M31 is new.

Two harness defects were found during this pass, both capable of producing silently false evidence:

  • Stale bytecode — _TOKEN_OWNER_CLASS = 4 and = 1 are the same byte length, so the (mtime, size) .pyc heuristic treated the cache as valid after a fast restore and kept serving the mutated constant. Fixed with -B, PYTHONDONTWRITEBYTECODE, and a cache purge around every mutation.
  • CRLF anchors — git checked these files out with CRLF, and the harness reads bytes without universal-newline translation, so every multi-line anchor silently failed to match and its mutant was skipped rather than exercised. Surfaced only because the harness's silent anchor-failure path had just been made to report. All 20 Windows mutants were rerun after the fix, not just the affected one.

Local validation on the final text

Windows focused   149 passed / 1 optional skip
Windows mandatory 173 passed / 1 deselected / 0 skipped   (structured gate PASS)
Ubuntu             96 passed / 49 deselected / 0 skipped  + 24 guards + 640-file manifest
Full repository  1664 passed / 6 skipped

Evidence rebinding: because the healthy tree changed after some early cycles, every mutant executed before the final rebaseline was rerun against the final production and killer text — ten Windows mutants and all ten Ubuntu mutants, the latter after syncing the container and regenerating its 640-file manifest.

All 36 obligations have a designated test, machine-checked by a ledger test that parses the suite and asserts coverage of exactly 1–36 with no strays.

Ubuntu     96 passed / 49 deselected / 0 skipped  + 24 guards + 640-file manifest verified
Windows   149 passed / 1 optional symlink skip
Mandatory 173 passed / 1 deselected / 0 skipped  + structured-result gate PASS
Full repo 1664 passed / 6 skipped

Three harness defects are recorded in §26 rather than hidden: DID NOT RAISE mis-scored as unclean (it is the correct kill marker); Windows console decoding blanking pytest output containing BOM/NFD bytes; and an ordering assertion raising ValueError instead of asserting. An evidence-integrity incident is also recorded plainly — a stale baseline misdiagnosed as file corruption briefly reverted an authorized fix until the healthy suite caught it.

Windows CI job

Additive only. The Ubuntu matrix is byte-identical — the diff contains zero removed lines.

windows-latest · Python 3.12 · pip install -e ".[dev]" with no fallback · permissions: contents: read · NTFS verification that fails closed · JUnit under $env:RUNNER_TEMP · gate fails on missing, unparseable, or any mandatory skip · leaf-suite XPath so counters cannot double-count · non-gating symlink probe · bounded summary only · no artifact upload · no PYTHONPATH manipulation.

The hosted job must establish: NTFS verified, mandatory tests > 0, failures 0, errors 0, skipped 0, exactly one T35[W] testcase, exactly one accepted transition property, bounded summary only, no raw XML or sensitive path disclosure. A hosted transition of False→False, False→True, or True→True is acceptable; only True→False or another metadata difference fails.

Local validation actually run

git diff --cached --check: clean
seven-path allowlist audit: 7 of 7, zero outside
remain-unmodified audit: all unmodified
artifact scan: no harness, evidence log, JUnit XML, Docker material, or .tcx-* in the repository
Ubuntu job semantic + textual equivalence: identical, zero removed lines

🤖 Generated with Claude Code

…-001C)

Add the platform-neutral policy core and a private Windows mechanism
adapter, the 36-obligation test suite, and one additive windows_executor
CI job. The dependency is one-way: the adapter imports no TriageCore
module and owns a private Win32SecurityCapture; the core imports it
dynamically only after its Windows gate and converts that capture into
the core-owned SecuritySnapshot, where DACL classification, comparison
policy, ReplaceFileW result classification, the closed vocabularies, and
the privacy-gated projection live. Outcomes derive from reason codes
through a single mapping, so an outcome cannot be chosen independently of
the condition observed.

The sixteen-step sequence issues exactly one ReplaceFileW call, with no
rollback, retry, path fallback, best-effort ACL parsing, or cleanup once
a state is ambiguous.

Building the contract discovered three defects: SE_DACL_AUTO_INHERITED
strict equality was unimplementable against real NTFS (amended to the
monotonic rule); a directory target reported the wrong reason code until
the adapter opened with directory-capable flags and the core classified
the opened object; and reparse rejection required for the target as well
as every ancestor covered only ancestors, so a reparse target was
rejected after the handle existed.

Evidence: 29/29 designated mutants killed through intended behavioural
assertions (10 Ubuntu, 19 Windows), zero survivors, byte-exact
restoration after every cycle, and every mutant executed before the
final rebaseline rerun against the final production and killer text.
Local Ubuntu 95/47/0 plus 24 guards; Windows 141 passed; mandatory group
165 passed / 1 deselected / 0 skipped with the structured-result gate
passing.

Hosted Windows CI has never run, so all Windows evidence here is local
and supplemental and section 21 acceptance is not satisfied. CR-OC-001C
is not complete, not merged, and not runtime-integrated; no runtime
module imports either new module.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@netlify

netlify Bot commented Aug 1, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for poetic-quokka-0fd859 ready!

Name Link
🔨 Latest commit dec98b1
🔍 Latest deploy log https://app.netlify.com/projects/poetic-quokka-0fd859/deploys/6a6dc08567a4ef00087771f7
😎 Deploy Preview https://deploy-preview-128--poetic-quokka-0fd859.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

coreytshaffer and others added 5 commits July 31, 2026 23:37
…d-replacement-implementation

# Conflicts:
#	docs/change/change_log.md
#	docs/current_backlog.md
…10.2a)

Implements the section 10.2a amendment merged as 8e19cdf.

Adapter: _TOKEN_OWNER_CLASS = 4 and a _TOKEN_OWNER structure replace the
TokenUser machinery, which is removed entirely rather than left dormant.
process_owner_sid becomes process_default_owner_sid and decodes the buffer
as _TOKEN_OWNER, passing Owner to the canonical SID conversion. Both
GetTokenInformation calls, the sizing probe and the retrieval, pass
TokenOwner.

Core: the ownership gate compares the captured target owner against the
current token default owner. The filesystem-operation instrumentation set
used by the cessation tests is updated to the new name.

T20 becomes three parts as the amendment requires, without adding a
thirty-seventh obligation: an injected incompatible default owner refuses
before mutation; a genuinely created file's owner equals the token default
owner; and an ordinary replacement passes the gate and preserves owner
equality. Neither SID value is attached to any assertion message.

M30 is added with a deterministic killer that records the information
class passed to GetTokenInformation and asserts both calls request class
4, using a narrowly fake _ADV. It does not depend on the two SIDs
differing on the machine under test, which is how the original defect
escaped local evidence. A companion test asserts no dormant TokenUser
machinery remains.

Evidence on the final text: 30/30 mutants killed, 20 Windows and 10
Ubuntu, byte-exact restoration after every cycle. Windows focused 144
passed / 1 optional skip; mandatory group 168 passed / 1 deselected / 0
skipped with the structured gate passing; Ubuntu 96 passed / 49
deselected / 0 skipped plus 24 guards and a verified 640-file manifest;
full repository suite 1659 passed / 6 skipped.

Hosted windows_executor has not yet run against this correction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Unit

Bounded diagnostic pass. Tests and workflow only; no production or
contract change.

Hosted run 30689133402 showed the TokenOwner correction fixed the
ownership gate (the 34 blanket precondition refusals are gone, T20 genuine
created-file equality passed, M30 passed), but eight healthy replacement
cases now fail post-verification with metadata_preservation_failed. The
log does not reveal which participating component differs.

T35W now captures the post snapshot BEFORE any outcome assertion, so a
metadata_preservation_failed result is classified instead of aborting with
the component unknown. It computes labels first and fails explicitly with
field names only: owner, dacl_state, dacl_present, dacl_protected,
dacl_auto_inherited, acl_revision, ace_count, ace_bytes_or_order, or
post_capture_failed. No SID, ACE, descriptor, path, or control-bit value
is emitted.

The Windows invocation adds -o junit_family=legacy. Pytest warned that
record_property is incompatible with the default xunit2 family, which does
not permit testcase-level property elements, so the transition gate would
have had nothing to read. Applied to the additive windows_executor command
only; pyproject.toml is untouched and the Ubuntu jobs are unaffected.

Verified locally under legacy: exactly one T35W testcase, exactly one
transition property, and the leaf-suite parser still reports
mandatory_tests=168 mandatory_skipped=0. Focused suite 144 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hosted run 30689442321 identified the components a successful ReplaceFileW
changes on an ordinary target: dacl_auto_inherited, ace_count, and
ace_bytes_or_order. That is consistent with inheritance being recomputed or
canonicalized during replacement, but the labels do not establish that every
new or changed ACE was inherited, nor that effective access was unchanged.

Adds a paired genuine-NTFS control to decide whether exact ordered-ACE
preservation is achievable under bare ReplaceFileW at all:

  1. inherited-DACL case -- the ordinary target
  2. protected-DACL case -- inheritance disabled while the effective ACEs are
     copied in as explicit entries

The protected fixture uses a narrow test-side SetSecurityInfo call with
PROTECTED_DACL_SECURITY_INFORMATION rather than parsing icacls output, so no
external command output can reach pytest or JUnit. It records
pre_dacl_protected and pre_dacl_nonempty as booleans, because a protected but
empty DACL would compare equal trivially and make the control meaningless. A
fixture that fails to protect fails rather than skips, since the mandatory
group permits zero skips.

Both cases emit field labels and booleans only. No SID, ACE, access mask,
descriptor, SDDL, path, or command output enters pytest output or JUnit.
Failure is keyed to labels beyond dacl_auto_inherited, which is the
contract-accepted monotonic transition (CR 10.1a).

Test-only. No production, workflow, or contract change. Exact DACL
preservation by bare ReplaceFileW remains under investigation and is not
established.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hosted run 30690450931 reproduced the inherited-DACL result exactly
(dacl_auto_inherited, ace_count, ace_bytes_or_order) but the protected-DACL
test was absent from the failures for a reason that is not yet evidence: it
failed only on a metadata label beyond dacl_auto_inherited.

As written, a refusal such as temp_creation_failed accompanied by no metadata
differences would also have passed it. The recorded protected_replacement_result
property was not recoverable, because the mandatory command failed before the
gate or summary exposed it and no artifact is uploaded.

The control now requires all of: fixture protected, fixture nonempty,
reason_code == "ok", outcome == replacement_verified, the target's bytes equal
the proposed bytes, and metadata differences none or dacl_auto_inherited only.
The byte check reports a fixed message rather than printing values, keeping the
diagnostic discipline uniform.

pre_dacl_nonempty remains an evidence-control condition, not a production
requirement: it proves a hosted success was not a trivial empty-ACL comparison.

Each new assertion was verified to fail when it should, by forcing the
condition under a scratch-only plugin that is not committed.

Test-only. No production, workflow, backlog, change-log, or CR-document change.
The contract decision remains open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer added a commit that referenced this pull request Aug 1, 2026
1. T20 part 3 still described an unrestricted healthy replacement -- "an
   ordinary target created by this process reaches replacement_verified" --
   which under section 10.2b either permitted or required a healthy result
   without establishing the supported DACL profile. It now reads: a target
   created by this process, then placed into a present, non-NULL, protected
   DACL profile without changing its owner, reaches replacement_verified,
   and post owner equals pre owner. The section 10.2a change-log lineage
   entry carries the same qualifier, with a note that the original wording
   would be ambiguous under the amended contract.

2. Section 1's implementation status said "Proposed, not authorized", which
   has been false since implementation was explicitly authorized. It now
   records that implementation is authorized and in progress on draft
   PR #128; not merged, not accepted, not runtime-integrated; and not yet
   conformant with section 10.2b, since the supported-profile gate, revised
   fixtures, T21 evidence, and M31 remain pending. The adjacent authority
   and approval-gate bullets are adjusted so they no longer deny an
   authority that was in fact granted, while still recording that it came
   from a separate explicit approval bounded to the section 25.1 seven-path
   allowlist, and that it is not merge authority.

3. The gate order is now explicit: the supported-profile gate is evaluated
   immediately after validating the pre-mutation security snapshot and
   before the TokenOwner ownership gate, despite the order the two bullets
   are written in. That gives T21's absent, NULL, and unprotected cases a
   precise cessation point and avoids querying the token's default owner
   for a profile already known to be unsupported.

Section 22 needed no change: its "not authorized" line is scoped to the
requirements-era five-path list and is immediately followed by the existing
annotation recording that section 25.1 supersedes it. The backlog needed no
change: its section 10.2a summary does not enumerate T20's three parts and
never carried the ambiguous phrase.

Documentation only, within the same three-path boundary. No code, tests,
workflow, dependency, fixture, or configuration changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer added a commit that referenced this pull request Aug 1, 2026
The lineage sentence said each amendment was "merged as a documentation-only
change before the implementation was corrected to match". That is true of
10.1a and 10.2a but not of 10.2b, whose implementation correction has not
happened yet -- as the very next status bullet correctly states.

Replaced with future-neutral wording: implementation is corrected only after
the corresponding documentation-only amendment merges, and the 10.2b
implementation correction remains pending on draft PR #128.

Documentation only, within the same three-path boundary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coreytshaffer and others added 3 commits August 1, 2026 02:04
…d-replacement-implementation

# Conflicts:
#	docs/current_backlog.md
Implements the amendment merged as 5e41d6f (PR #130).

Production (triage_core/mediated_executor.py):

  The supported-profile gate is evaluated immediately after the pre-mutation
  security snapshot validates and BEFORE the TokenOwner ownership gate, so an
  already-unsupported profile ceases without a needless token query. A DACL
  that is absent, NULL, or not protected from inheritance is refused as
  metadata_precondition_failed / not_attempted, before temporary-file creation
  and before ReplaceFileW.

  The executor only INSPECTS and REFUSES. It never protects, normalizes,
  repairs, or otherwise alters a target's DACL to make it eligible.

  ACE count deliberately does not participate: a present, non-NULL, protected
  DACL with zero ACEs is supported.

Tests (tests/test_mediated_executor.py):

  Healthy Windows fixtures are placed into the supported profile by
  construction via workspace(protect=True) and prepare_supported_target(),
  which disable inheritance while copying the effective ACEs in as explicit
  entries and leave the owner untouched. Fixture preparation lives entirely in
  the tests. A fixture that fails to protect raises rather than skipping.

  T20 part 3 is now a supported-profile control asserting the pre-state is
  present and protected, and that post owner equals pre owner.

  T21 gains: a genuine unprotected NTFS target refusing before any mutation
  with the cessation point at capture_security, no temp/replace/write/delete,
  no token query, bytes intact, no artifacts, and the DACL still unprotected
  afterwards (the executor did not repair it); seam-injected absent and NULL
  states refusing identically; a present-protected zero-ACE DACL proving
  pre_dacl_nonempty never leaked into production policy; and the
  supported-profile control requiring ok, replacement_verified, exact proposed
  bytes, and the complete metadata invariant.

  T35W now confirms the supported profile before replacing. With a protected
  fixture the runner exhibits True->True rather than False->True -- both
  accepted under 10.1a, which is why that section refused to require the
  normalization.

  The two temporary diagnostic tests are converted into their contracted T21
  homes rather than left as parallel controls, and T35W now shares the single
  difference classifier so the two cannot drift.

Evidence, observed rather than carried forward:

  Windows focused    149 passed / 1 optional skip
  Windows mandatory  173 passed / 1 deselected / 0 skipped, gate PASS
  Ubuntu neutral      96 passed / 54 skipped, guards 24 passed
  Full repository   1664 passed / 6 skipped
  Mutants            31/31 killed (21 Windows, 10 Ubuntu), zero survivors

  All 31 anchors pre-validated as unique and CRLF-normalised before any cycle;
  byte-exact restoration and a clean 640-file manifest before and after every
  Ubuntu cycle; JUnit privacy scan reports zero SIDs, access masks, and SDDL.

No dependency, pyproject.toml, runtime-integration, or eighth-path change.
The six named modules remain unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ence

Section 26 claimed the branch was unpushed and that hosted Windows CI had
never run. Both were live contradictions. Documentation only.

Status (sections 1 and 26): the implementation now conforms to section 10.2b,
local and hosted validation have passed, and it remains unmerged, not
accepted, and not runtime-integrated, with merge authority not granted.

Mutant record (26.5): 29 total / 19 Windows becomes

  31 total -- 21 Windows, 10 Ubuntu
  31 clean behavioural kills
   0 survivors
   0 invalid kills

All 31 were rerun against the final text, since 10.2b changed the shared
Windows fixture and several designated killers, and all 31 anchors were
pre-validated as present exactly once before any cycle ran. Section 25.9's
reviewer command set is corrected from 27 variants to 31 -- a concrete
instruction, not historical prose -- and now also carries the two harness
requirements the implementation proved necessary.

Validation (26.6), observed rather than preserved:

  Windows focused      149 passed / 1 optional skip
  Windows mandatory    173 passed / 1 deselected / 0 skipped
  structured gate      PASS
  recorded transition  True->True
  Ubuntu neutral        96 passed / 54 skipped
  Ubuntu guards         24 passed
  Ubuntu manifest       640 source files verified
  Full repository     1664 passed / 6 skipped
  Mutants               31/31 killed

The Ubuntu 54-skipped count is explicitly distinguished from the hosted
zero-skip metric: it is what running the whole Windows-oriented executor file
on Ubuntu produces, since every [W] obligation is windows_only.

Hosted evidence (new 26.6a): run 30694001033, head 257966b, checked-out
generated merge ref c5e8a9e, base 5e41d6f, Windows Server 2025 build
10.0.26100, image windows-2025-vs2026 version 20260714.173.1, Python 3.12.10,
NTFS, 173 mandatory tests, 0 skipped, gate PASS, True->True, symlink probe
pass, all four jobs green. The merge ref was verified through the commits API
rather than assumed, because the run's headSha reports the association and not
the checkout. This satisfies the hosted portions of sections 21 and 25.7 for
that tested tree only -- not a universal Windows guarantee, and not evidence
about any later commit.

Discoveries (26.2): three becomes five, adding the two defects only hosted CI
found -- the TokenUser/TokenOwner quantity error, and the failure of a
successful ReplaceFileW to preserve the invariant on inheritance-enabled
targets.

Harness corrections (26.3): three becomes seven, cumulatively, adding four
evidence-integrity faults -- stale bytecode from same-length mutations; CRLF
byte-mode anchors silently skipping multi-line mutants; docker cp creating
root-owned files, where hashes proved no mutation had landed before ownership
was corrected; and .pytest_cache invalidating the manifest, now narrowed to
the 640 source files it is meant to protect.

Non-claims (26.7): hosted validation achieved for the tested merge ref; the
optional probe skipped locally while the hosted supplemental probe passed;
True->True is one accepted observation, not required platform behaviour; and
nothing establishes hostile-writer safety, cross-process exclusion, OpenClaw
containment, authorization, reservations, exactly-once execution, or causation
from observation.

Warning count (26.8): stale 40 becomes the observed hosted 46, retaining the
note that registering markers would require the unauthorized pyproject.toml
path.

The superseded implementation change-log entry now points forward to current
counts and status rather than leaving them ambiguous.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coreytshaffer
coreytshaffer marked this pull request as ready for review August 1, 2026 09:53
@coreytshaffer
coreytshaffer merged commit f65a864 into main Aug 1, 2026
8 checks passed
@coreytshaffer
coreytshaffer deleted the cr-oc-001c-constrained-replacement-implementation branch August 1, 2026 09:54
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