feat(mediated): implement the constrained replacement executor (CR-OC-001C) - #128
Merged
coreytshaffer merged 9 commits intoAug 1, 2026
Merged
Conversation
…-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>
✅ Deploy Preview for poetic-quokka-0fd859 ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…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>
…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
marked this pull request as ready for review
August 1, 2026 09:53
coreytshaffer
deleted the
cr-oc-001c-constrained-replacement-implementation
branch
August 1, 2026 09:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 ref86d43b7.Hosted run 30686689771 (SUPERSEDED) — the first attempt failed
Run 30686689771 exercised the
windows_executorjob against the branch as it then stood:Every failure is the §10.2 pre-mutation owner gate. The gate compared the target's owner against
GetTokenInformation(TokenUser), butTokenUseridentifies the token's user account, whereasTokenOwneris 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.ReplaceFileWdocuments 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
TokenUserandTokenOwnerthere. 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
TokenOwneramendment merged through PR #129 as8e19cdf, and the implementation correction is now applied on this branch:_TOKEN_OWNER_CLASS = 4, a_TOKEN_OWNERstructure,process_default_owner_sid, theTokenUsermachinery 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
The
TokenOwnercorrection worked at the precondition layer: the 34 blanketmetadata_precondition_failedrefusals 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_propertyis incompatible with the defaultxunit2JUnit 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.tomlis 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
ReplaceFileWchanged exactly three components:The
junit_family=legacycorrection also worked — therecord_propertyincompatibility 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
ReplaceFileWas 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 setSE_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
ReplaceFileWpreserves 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
ReplaceFileWis 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:
SetSecurityInfocall withPROTECTED_DACL_SECURITY_INFORMATIONrather than parsingicaclsoutput.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, andmetadata_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:
The local environment does not reproduce the hosted
ace_count/ace_bytes_or_orderchange, so this is suggestive only and settles nothing until the pair runs on hosted Windows. The decision it will inform:metadata_precondition_failedbefore temporary-file creation. This preserves the exact owner and ordered-ACE invariant rather than weakening it, at a cost in usability.ReplaceFileWcannot 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
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 wasok, that the outcome wasreplacement_verified, or that the proposed bytes reached the target — so a refusal such astemp_creation_failedaccompanied by no metadata differences would also have passed it. The test did recordprotected_replacement_resultinto 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 fixedprotected_post_bytes_mismatchmessage rather than printing values, keeping the diagnostic discipline uniform even though the test bytes are not sensitive.pre_dacl_nonemptyremains 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):
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_configuretime; patching the collected module atpytest_collection_finishshowed 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
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: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
mainat8e19cdf) 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 withmetadata_precondition_failed/not_attemptedbefore temporary-file creation. The exact invariant, theSE_DACL_AUTO_INHERITEDmonotonic 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
5e41d6fPR #130 merged as
5e41d6f, and this branch now incorporates amendedmainby 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 beforeprocess_default_owner_sid(), so an unsupported profile ceases without a needless token query: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:
capture_security; no temp/replace/write/delete; no token query; bytes intact; no artifacts; DACL still unprotected afterwardsok,replacement_verified, exact proposed bytes, complete metadata invariantpre_dacl_nonemptynever leaked into production policyThe 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->Truerather thanFalse->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
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 cpwrote container files asrootso the first Ubuntu restore raisedPermissionError— the source was verified unmutated (hash matched pristine, the failure was on the first write) before continuing; and an ephemeral.pytest_cacheentry 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
True->Trueis one accepted observation, not required platform behaviour. §10.1a acceptsFalse->False,False->True, andTrue->Truealike.Seven paths, no eighth
No dependency added,
pyproject.tomluntouched, andauthz.py,capability_claims.py,request_reservation.py,task_ledger.py,client.py,tc_cli.pyall unmodified.Module split and one-way dependency
The adapter imports no TriageCore module and owns a private
Win32SecurityCaptureof primitive observed facts. The core imports the adapter dynamically, only after its Windows gate, and converts that capture throughsnapshot_from_captureinto the core-ownedSecuritySnapshot— where the three-valued DACL classification, comparison policy, pureReplaceFileWresult classification, the closed vocabularies, and the privacy-gated projection live. No third bridging module.Outcomes derive from reason codes through a single
REASON_TO_OUTCOMEmapping, so an outcome can never be chosen independently of the condition observed.The sixteen-step sequence issues exactly one
ReplaceFileWcall, 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, withdelete_fileabsent and the backup retained.Three defects found by building the contract
SE_DACL_AUTO_INHERITEDstrict equality was unimplementable. A successfulReplaceFileWsets 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.FILE_FLAG_BACKUP_SEMANTICS, so the open failed and reportedcontainment_violationwhere §19 T8 requirestarget_not_regular_file. The adapter now opens with directory-capable flags and the core classifies the opened object.CreateFileW. §7.2 requires rejection "for the target and every ancestor"; the first implementation walked ancestors only.has_reparse_targetis 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:
_TOKEN_OWNER_CLASS = 4and= 1are the same byte length, so the(mtime, size).pycheuristic 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.Local validation on the final text
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.
Three harness defects are recorded in §26 rather than hidden:
DID NOT RAISEmis-scored as unclean (it is the correct kill marker); Windows console decoding blanking pytest output containing BOM/NFD bytes; and an ordering assertion raisingValueErrorinstead 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 · noPYTHONPATHmanipulation.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, orTrue→Trueis acceptable; onlyTrue→Falseor another metadata difference fails.Local validation actually run
🤖 Generated with Claude Code