feat: exclusive-access gate -- refuse WS/CF schema changes while FLEx holds the project - #343
Merged
Merged
Conversation
…ess (#93) Carries forward what #93 left unbuilt: the requires_exclusive_access gate for writing-system and custom-field changes while FLEx holds the project, a valid live test for the custom-field hazard, the remaining undo and shared-mode doc corrections, SyncForeignChanges for write-enabled runs, and the unfiled upstream issues. Programmatic undo is out of scope. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, quickstart Plan for the requires_exclusive_access gate (successor to #93). LCM source settles the FLEx-undo question: reconciled peer writes land on the non-undoable stack, so the spec's Section 2 is amended from "unknown". The MCP runs undoable=True under flexicon 4.11, so read-back sync uses SaveChanges(), and read-only sessions cannot sync. The domain gate and the plan QC gate (one blocking count site, now scheduled) are recorded in reviews/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the project
Phases 1-4 of specs/exclusive-access-gate (MVP: US1 + US2).
A write-enabled run_module whose script changes writing systems or custom
fields is refused with the new code requires_exclusive_access when the
access probe says FieldWorks holds the project in shared mode (open_shared)
or cannot tell (unknown). The refusal comes before the confirmation gate,
the backup and any subprocess. open_exclusive / held_by_other keep their
project_locked refusal.
- server/exclusive_access.py: EXCLUSIVE_ONLY_OPERATIONS (one table for the
detector, the refusal and the docs) and detect_exclusive_only_operations
(certifier rows for wrapper calls + one AST pass for raw LCM names,
receiver-scoped generic methods and schema-property assignments).
Untyped-receiver rows match wrapper names by name, except Create/Delete,
which are mutating on ~50 other Operations classes and on every raw LCM
factory.
- handle_run_module: detect on the final (post-auto-fix) code, force the
probe on a match, refuse before confirmation.
- validate_only: project_lock.exclusive_access = {required, operations,
blocking}; blocking is true on unknown (the real run refuses it, FR-009),
null only when the probe raised.
- Contract: RequiresExclusiveAccessDetail, union 46 -> 47, golden fixture,
TOOL-CONTRACT.md row and counts (stale "44 codes" fixed).
- US2: specific assistance hint; open_shared open failures no longer say
"Close FieldWorks and retry"; the open_shared advisory points at the gate.
Pattern audit ("close FieldWorks" on a shared project), full table in
specs/exclusive-access-gate/reviews/pattern-audit-close-fieldworks.md:
- execution.py _diagnose_project_open_error generic hint: FIXED for
open_shared; kept for free / failed probe.
- write_ladder.py refusal fallback: reworded (open_exclusive/held_by_other
only; no longer assumes FieldWorks is the holder).
- write_ladder.py open_shared advisory: FIXED (points at the gate).
- project_discovery.py:398: kept, branch runs only with sharing OFF.
- project_discovery.py:419: comment only.
- teardown_recovery.py:180: kept, abandoned machine-wide WS-store mutex,
unrelated to the project lock.
Tests: tests/test_exclusive_access_detect.py (49), test_exclusive_access_gate.py
(23), contract/diagnosis/advisory updates. Full offline suite: 4774 passed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…shipped Phase 5 of specs/exclusive-access-gate (US4). - workflow-summary.md: Stage 6 is "Inspect" (logs, session history); the undo tool, undo stack and "undo entry recorded" claims are gone; the pre-write backup is named as the safety net. - FLEXTOOLS-STYLE-GUIDE.md: the "undo_last_operation can reverse a write" layer is replaced by "there is no undo", linking RECOVERY.md. - SHARED-MODE.md: "Close FLEx for these" says the server refuses these with requires_exclusive_access and gives the close / re-submit / reopen steps; rows name every wrapper method in EXCLUSIVE_ONLY_OPERATIONS; new "Seeing an MCP change in FLEx" (navigate away and back, F5 alone is not enough, #96); "Undo" gains the four facts (no MCP undo, FLEx opens with an empty undo history, FLEx records peer writes as non-undoable, programmatic undo not built) with LCM citations. - tool_definitions.py (flextools_start): no longer claims writes land in FLEx's Ctrl+Z menu. - tests/test_docs_no_undo_claims.py: the tool may be named only as removed; no "can reverse a write"; SHARED-MODE.md phrases; one row per category. docs/archive/ is excluded as history. - WS evidence path corrected to specs/shared-mode-access/ (not archived). validate_integrity.py server: clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Offline parts of phases 7-9 of specs/exclusive-access-gate. - evidence/live_cf_peer.py (T029): Class B harness. dry / add / check modes; asserts Sena 3; peer-adds the zzExclTest custom-field definition via FieldDescription.UpdateCustomField inside a non-undoable task, writes no data. Not yet run (needs FLEx open; T030 is needs-human). - issues/filing-ledger.md (T032-T034): the four flexicon hazards are already fixed (cb1d355 #285, 287bb20 #233/#234, b3a5bb9 #236); the CreateField CheckNotProcessingDataChanges comment is still new. Archived drafts triaged: 8 still new (flexicon-5 retargeted to FlexToolsMCP), 5 fixed or duplicate. Nothing filed; T035 waits for the maintainer. - SHARED-MODE.md (T037): the save-conflict hazard is described as fixed in flexicon 4.6.0 (HeadlessLcmUI default), replacing "upstream issue not yet filed". - reviews/sweep-detector.md (T039): no confirmed sibling in the detector; pre-existing write-gate siblings in validators.py logged as filing candidates. - CHANGELOG.md (T038): Tool contract and Other entries. Full offline gate (T040): 4829 passed; goldens up to date; integrity clean; pre-commit clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lowed error after_implement lex-qc gate (reviews/implement-qc.md, 84/100): - N1: exclusive_access.REFUSING_VERDICTS -> GATED_VERDICTS; it had the opposite meaning of write_ladder.REFUSING_VERDICTS on adjacent lines. - N6: validate_only's probe-unavailable exclusive_access branch logs at debug instead of passing silently. B2 (pattern audit in the b52a7de body) confirmed. B1 (live evidence) stays open: needs FieldWorks open on Sena 3 and the maintainer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… run time FR-002b amendment (maintainer-approved 2026-10-01). WritingSystems.Ensure() writes only when the tag is not already active in the requested category, so refusing every Ensure() made users close FieldWorks for no-ops. Two layers: 1. Before the run, the project's ACTIVE lists (CurVernWss / CurAnalysisWss) are streamed from the .fwdata -- pure filesystem, no project open, 156 ms on Sena 3's 53 MB file (an earlier LCM-subprocess snapshot took ~4 s and was replaced at the maintainer's suggestion). The .ldml store is deliberately not used: store-present tags can be inactive. plan_conditional: literal + active -> run; literal + would add -> refuse now (stage: preflight) with the reason; non-literal or unreadable file -> deferred to layer 2. Any unconditional match, or no guard in the installed flexicon, refuses as before. 2. During the run, flexicon's peer schema guard (capability "peer-schema-guard", flexicon branch feat/peer-schema-guard 076a239) raises FP_ExclusiveAccessRequiredError before writing; mapped to requires_exclusive_access with stage: runtime. The runner's PEER_SCHEMA_GUARD line (off / on / required) is patched after the probe; "required" stops the run before user code if the guard is missing, so a server/subprocess flexicon mismatch fails closed. The guard is "on" for every write-enabled run on a gated verdict as a backstop. Also: ws.ensure table row (conditional=True); detail model gains `stage`, match model gains `conditional`; success result carries exclusive_access; warning when Ensure() follows other writes; validate_only makes the same plan from the same file read (blocking / conditional / deferred_to_runtime / notes). Docs: SHARED-MODE "Ensure() is checked first", TOOL-CONTRACT row, spec FR-002b, data-model, contract, CHANGELOG. Golden regenerated. Earlier work in this commit's tree (evidence): live-gate.md V1, V2, V4, V5 (sync-at-open NO-SHIP), V7 dry, V8a-d, plus the live driver and snippets. Tests: detector 68, gate 39; full offline suite 4865 passed; goldens up to date; integrity clean; pre-commit clean. Live V8 (Sena 3, FieldWorks PID 27984, sharing on): a) active Ensure ran, created=False; b) literal new tag refused preflight with reason; c) the same tag via a variable refused at run time, nothing written; d) released flexicon (no guard) refused preflight. WS lists and .ldml mtimes unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…mtime) The conditional-Ensure() read runs only for write-enabled runs whose only exclusive-only calls are Ensure() while FieldWorks holds the project, but it no longer re-reads an unchanged file: _ACTIVE_WS_CACHE keys each .fwdata path on (size, mtime_ns), so a hit costs one stat(). Any FLEx save or MCP write changes the file and forces a fresh read, so a writing system added since the last read is never refused from a stale entry (the case a once-per-session cache would get wrong). Unsaved FLEx state is invisible either way; the runtime peer schema guard covers it. Failed reads are not cached. Tests: hit (one read for two calls), changed file re-read, failed read not cached, missing .fwdata. Live V8a recheck on Sena 3: allowed, guard on. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cleanup - flexicon main pulled to 04786b0 (#601 merged): MCP offline suite 4869 passed. flexicon offline: the guard tests pass; 47 pre-existing failures (575/577/581/docstring ratchet) fail identically at ad4a1c2, main before the merge. - Live re-run happened with FieldWorks closed (verdict free), so the gate correctly did not apply and V8b created qaa-x-zzexcl in Sena 3. It was removed with WritingSystems.Delete (op-113850747); WS lists verified. An .ldml and an idchangelog <Add> remain (ledger flexicon-6), pending the maintainer. V8a doubled as the corrected V3 (FLEx closed, active tag: passes the gate, writes nothing). - live_gate_driver.py gains `--expect <verdict>` so a step refuses to run in the wrong project state. cleanup_zzexcl_ws.py added. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
V3: Ensure('en', analysis) with FLEx closed passed the gate and wrote
nothing; FLEx reopened cleanly without qaa-x-zzexcl. V6: after reopen the
zzExclTest gloss zzgloss-v4 (MCP peer write) is present and Undo is empty.
In-session navigate-away/F5 and peer-write-not-undoable were not re-tested
this round (FLEx had been reopened); docs keep citing #96 and research R4.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Per the maintainer: a fresh FLEx start always shows the current data; the navigate-away-and-back refresh (#96) applies only to a change made while FLEx was open. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…28 note V7 (Sena 3 held by FieldWorks PID 6468, sharing on; Claude-Swahili open in another FLEx and untouched): a peer added the zzExclTest custom-field definition (FieldDescription.UpdateCustomField in a non-undoable task, no data) and committed. A fresh peer did not see it, the master's Custom Fields dialog did not list it, the .fwdata was untouched, and after FLEx closed it was still absent: silently lost, confirmed live (stronger than R9's "visible, then gone"). SHARED-MODE.md and the cf.* evidence updated; live_cf_peer.py: .NET imports moved after FLExInitialize. Cleanup with Sena 3 closed: qaa-x-zzexcl.ldml deleted and its one idchangelog <Add> removed (maintainer-approved; encoding/CRLF kept; copies in the session scratchpad); zzExclTest entry deleted (op-092650017, verified absent). T028: shared_mode_read_back notes (runner advisory + runtime primer) replace "untested" with the V5 result, read correctly: the read-back saw the write only because FLEx was idle and the master had already flushed the .fwdata; it is not a guarantee, and SaveChanges() in the reader made no difference. The V5 evidence's "reaches disk at commit time" is corrected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cleanup No zzExclTest custom field or entry; writing systems seh, seh-fonipa-x-etic / en, pt with no qaa-x-zzexcl. All live steps V1-V8 complete. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 2, 2026
Closed
flexicon#602-#609 and FlexToolsMCP#347-#352, filed 2026-10-02 with the maintainer's OK; ledger rows carry the issue numbers. #350 (unguarded coll.Add through a loop over LCM collections passes the write gate) was reproduced offline before filing; #352 (compound modifyAllowed guard rejected) and flexicon#609 (47 pre-existing offline failures on main) are new rows. The V7 upstream row is not applicable (V7 ran). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in unknown_method (#306/#338/#339/#342), project_locked holder and next_steps (#315/#336), and the shared-mode-access archive move (#341). Conflict resolution keeps both sides: the requires_exclusive_access row and model sit next to main's project_locked next_steps and unknown_method; the open_shared project-open hint (FR-011) now runs after main's own-worker holder check. The error-code count moves to 48 everywhere it is stated (TOOL-CONTRACT.md, response_models.py, both count tests, CHANGELOG). Evidence paths that pointed at specs/shared-mode-access/evidence/ now point at specs/_archive/shared-mode-access/evidence/. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e (T041) Records phase 10 and the origin/main merge in .spec-context.json, the 48-code count, and that T048 (flexicon release) is still open: flexicon#601 is merged but unreleased, so the pyflexicon floor and the indexes stay as they are. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ms receiver A writing-system Create or Delete called through a helper's project parameter (p.WritingSystems.Delete), or through a local alias of the facade attribute (w = p.WritingSystems; for w in [project.WritingSystems]), left only an unresolved_receiver row, and the generic-name exclusion dropped it. Under open_shared/unknown the run went ahead and could crash the FieldWorks holding the project. An AST layer now matches the generic names when the receiver is the facade attribute or bound from it; unrelated receivers and GetAll() results still do not match. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h, T041 push status Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 2, 2026
Brings in #354 (write-gate fail-opens), #353 (surface recipes), #355 (docs), #356 (preflight retry loops). Conflict resolution keeps both sides: unknown_import (#305) and requires_exclusive_access sit side by side and the error-code count moves to 49 everywhere (response_models, both count tests, TOOL-CONTRACT, CHANGELOG). execution.py keeps main's #334 assistance-log ordering with the gate's requires_exclusive_access key. test_issue55 ladder test stubs the exclusive-access detector so it still exercises Rung 3 (gate covered in test_exclusive_access_gate.py). Full offline suite: 5273 passed, 4 skipped.
MattGyverLee
marked this pull request as ready for review
October 2, 2026 18:42
MattGyverLee
added a commit
that referenced
this pull request
Oct 2, 2026
…360) * fix(exclusive-access): review findings 2-4 on the gate - ensure_call_args: *args/**kwargs unpacking -> unresolved (None), so the call defers to the runtime peer guard instead of being judged against the wrong category's active list with a defaulted is_vernacular=True. - _wrapper_matches: a conditional Ensure() on an unresolved_receiver row no longer matches by method name alone; it needs receiver evidence (resolved WritingSystemOperations class or a .WritingSystems facade, the latter matched by the AST facade pass). - Facade aliases are swept in source order: rebinding a name to a non-facade value drops the alias, and function/lambda parameters shadow inherited module aliases. * fix(exclusive-access): runtime refusal carries the documented envelope Finding 1 on the #343 review: a runtime peer-guard refusal (FP_ExclusiveAccessRequiredError / PeerSchemaGuardUnavailable) previously produced error_code + stage: runtime but no operations, verdict, flat message, or nested error object, so the published requires_exclusive_access envelope could not be validated from it. - _diagnose_exclusive_access_runtime_error now takes the conditional matches the preflight plan let through plus the probe verdict/holder, and the call site passes them from _exclusive_matches/_decision/_access. The message names the refused operations, build_refusal-style. - The diagnosis promotion builds the dual envelope mirroring error_response(): flat message + nested error {code, message, ...detail}, with guidance/verdict/holder/operations/remedy/stage at top level. The raw .NET text moves to raw_error; messages and backup status untouched. - Downstream string assumptions hardened: _error_message_text() coerces the nested error object for _log_operation_failure and the pattern tracker's record_operation (the latter crashed the handler with TypeError when a tracker was initialized -- caught only by the full-suite run, not by the gate tests alone). Finding 2-4 (unresolved Ensure needs receiver evidence; **kwargs/*args unresolved; facade aliases respect rebinding/shadowing) are in the preceding commit. --------- Co-authored-by: Muse <muse@local>
MattGyverLee
added a commit
that referenced
this pull request
Oct 5, 2026
All 48 tasks complete: the gate merged as 8a3339d (PR #343) with review fixes in 6673a65 (PR #360), released in 2.15.0 (#365). T048 (needs-human) closed: pyflexicon 4.12.0 ships the peer-schema-guard capability and PR #364 regenerated the indexes against it with the floor raised. Writes specs/exclusive-access-gate/AS-BUILT.md, git mvs the full docs (spec, plan, tasks, research, data-model, contracts, checklists, quickstart, reviews, evidence, issues/filing-ledger) to specs/_archive/exclusive-access-gate/, and repoints the live references (docs/SHARED-MODE.md, CHANGELOG.md, CLAUDE.md SPECKIT section, code comments, test docstrings) at the archive or the live one-pager.
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.
Summary
Shared mode lets the MCP write while FieldWorks has a project open. Two kinds of change are still unsafe from a peer:
This PR adds the refusal that shared mode was missing, plus the follow-ups around it. Spec:
specs/exclusive-access-gate/(successor to #93).requires_exclusive_access. It refuses a write-enabledrun_modulewhose script makes such a change when the probe saysopen_sharedorunknown. The refusal comes before confirmation, backup and subprocess.open_exclusive/held_by_otherstill giveproject_locked, and read-only runs and ordinary edits are never gated.XOperations(project)) and adds an AST pass for raw LCM names (server/exclusive_access.py)._facade_generic_matches) catchesCreate/Deleteon an untyped<x>.WritingSystems/<x>.CustomFieldsreceiver, including a local alias, a chained rebind, a module-level alias, or aforloop over a literal list. The certifier leaves these asunresolved_receiverrows. Values derived from the facade (p.WritingSystems.GetAll()) are not gated. Not followed yet: tuple unpacking andwith ... asbindings.unknown_method, [WM] Nonexistent method (ParserOperations.TryWord) classified as mutating -> unprotected_writes loop; return unknown_method + did-you-mean #306, took it to 47 first). The change is additive undertool-responses/1.x, with a golden fixture.WritingSystems.Ensure()(FR-002b). AnEnsure()is checked first instead of refused outright.CurVernWss/CurAnalysisWss) are streamed from the.fwdata: no project open, about 150 ms, then cached on (size, mtime).stage: runtime. The guard is also on for every write-enabled run on a gated verdict, as a backstop.Ensure()is refused as before.validate_onlygainsproject_lock.exclusive_access={required, operations, blocking, ...}and makes the same decision as the real run.open_sharedproject-open failure no longer says "Close FieldWorks and retry";open_sharedadvisory points at the refusal;workflow-summary.md, the style guide,flextools_start's description);SHARED-MODE.mddocuments the refusal and the conditionalEnsure(), how to see an MCP change in FLEx (navigate away and back; F5 alone is not enough), why FLEx's Undo cannot reverse an MCP write, and that the save-conflict hazard is fixed in flexicon 4.6.0.Merge with main
origin/mainis merged in (merge commit, no rebase): #306/#338/#339/#342unknown_method, #315/#336project_lockednext_steps, #341 archive move ofshared-mode-access, and dependency bumps. Conflicts were resolved by keeping both sides:docs/TOOL-CONTRACT.md: main'sproject_lockedrow with next_steps, next torequires_exclusive_access.execution.py_diagnose_project_open_error: main's own-worker holder check (project_locked by a python PID the model cannot end; verify whether holder is the idle parse worker #315) runs first, then the FR-011open_sharedhint.response_models.pyand the two count tests: both codes listed, and the count is 48 everywhere (TOOL-CONTRACT, response_models, tests, CHANGELOG).docs/SHARED-MODE.md: the branch's expanded WS/CF table,Ensure()section and corrected LCM citations. Evidence paths now point tospecs/_archive/shared-mode-access/, and so doesexclusive_access._WS_LIVE_EVIDENCE."Close FieldWorks" message class:
reviews/pattern-audit-close-fieldworks.md. Three sites fixed; three kept, with the reason for each.Detector shape (receiver/name matching, role confusion):
reviews/sweep-detector.md. No sibling found in the detector. Pre-existing write-gate siblings invalidators.pyare logged as filing candidates.Untyped-facade receivers: nothing else in
src/matches certifier rows by name with a generic-name exclusion.unresolved_receiverrows come only fromvalidators.py:6993/7013, and_raw_matchesalready uses the whole receiver chain.After the merge: the stale error-code counts and
specs/shared-mode-access/paths were swept. Remaining hits are intentional (they point to the predecessor's AS-BUILT, or are historical task text).Tests
tests/test_exclusive_access_detect.py(78, including 10 untyped-facade cases),tests/test_exclusive_access_gate.py(43) andtests/test_docs_no_undo_claims.py.validate_integrity.py serverclean; pre-commit clean.Live verification (Sena 3, FieldWorks PID 27984, sharing on):
specs/exclusive-access-gate/evidence/live-gate.mdEnsurerefused;.ldmlunchanged; no backup; FLEx responsivevalidate_onlygivesblocking: trueEnsureallowed / refused up front / refused at run time / refused without the guard; WS lists unchangedzzExclTestentry deleted;qaa-x-zzexclresidue removedBefore merge
Ensure()fails closed. This PR stays a draft until T048 is done.ProjectSettingsOperations.SetDefaultVernacular/Analysisbelong in the gated list.🤖 Generated with Claude Code