fix(validators): write-gate fail-opens -- loop containers, compound guards, receiver boundaries (#350, #352, #351) - #354
Merged
Conversation
_collect_local_container_names treated a for-loop target as a local container when its iterable was a list literal or local list, so an unguarded `for coll in [e.SensesOS]: coll.Add(s)` certified read-only. A name now counts as local only when every store of it binds a local container constructor (or another local name); loop/comprehension targets, tuple unpacking, with-as and any non-container rebinding disqualify it (flow-insensitive, fail closed). The suppression in find_liblcm_mutations is now keyed by call node (line + column past the method name) instead of by line, so `tmp.Add(x); entry.SensesOS.Add(s)` no longer hides the real Add. closes #350 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…compares _is_write_enabled_check now treats an `and` as a guard when any operand is one and an `or` only when every operand is; `not` flips to the disabled check. The early-return disabled check mirrors it, so `if not modifyAllowed or <cond>: return` protects the tail (#139 forms unchanged). For a compound test only the body is protected, since an operand before the guard can itself mutate. Pattern-audit sibling: the Compare branch accepted ANY comparison that mentioned modifyAllowed / project.writeEnabled, so `if modifyAllowed == False:` certified its body as protected. Compares now count only against a literal True/False in the enabling direction. The scaffold FTM_ModifiesDB regex also recognises compound guards. closes #352 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The project / facade receiver patterns (_PATTERN_CREATE/DELETE/ UPDATE_PROJECT, _PATTERN_PROJECT_ACCESSOR_CALL, the facade accessor templates) now require `\b` before the receiver, so `myproject.X.Delete(` and `prefx.Foo.SetBar(` no longer resolve as facade writes, while `self.project.X.Delete(` still does. The type-prefix receivers (entry/sense/.../pos) in the property= and generic-Add patterns use _RECEIVER_PREFIX_BOUNDARY: a word boundary, a snake_case `_`, or a camelCase hump. `nonsense.Form =` and `compose.Comment =` stop matching; `new_entry.LexemeFormOA =` and `newEntry.LexemeFormOA =` stay gated. A bare `\b` would have dropped those, turning an over-match into a missed write. closes #351 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- #351: drop the left boundary on `project` receivers (self._project, srcProject stay gated) and on entry/sense receiver prefixes (subsense, subentry, lexentry stay gated); only a short list of exact English words (nonsense, compose, position, ...) is excluded. Resolved facade names keep `\b`, since each is one exact identifier. - #352: when a guard's test contains a call, protect only the lines after the test, so `if not (x.Add(s) or not modifyAllowed):` and one-line `if x.Add(s) and modifyAllowed: pass` still report the call. - #350: parameters, lambda args, import aliases, except/match captures and def/class names now disqualify a name from local-container status. Refs #350, #351, #352. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MattGyverLee
added a commit
that referenced
this pull request
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.
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.
Fixes three write-gate bugs in
src/flextoolsmcp/server/validators.py. Two are fail-opens, where an unsafe script could be certified read-only. Do not merge until reviewed.#350 (P1, safety): loop element taken for a local container
_collect_local_container_namesnow counts a name as a local list/set only if every binding of it builds a local container. It checks Assign, AnnAssign, NamedExpr and AugAssign, plus aliases, using a fixpoint.except ... as,matchcaptures, and def/class names (new_non_name_store_bindings).x = e.SensesOS; x.Add(s); x = []was certified read-only._local_collection_mutation_sites). Sotmp.Add(x); entry.SensesOS.Add(s)is now flagged._lines_with_local_collection_mutationsis removed.results = []; results.append(...)andseen = set(); seen.Add(...)are still not flagged.Closes #350
#352 (P2): compound
modifyAllowedguards_is_write_enabled_checkaccepts anandwhen any operand is a guard, and anoronly when every operand is.notflips the polarity._is_write_disabled_checkmirrors this, soif not modifyAllowed or <cond>: returnprotects the code after it. The bug: unprotected_writes preflight gate rejects the early-return guard idiom, only accepts if/else #139 early-return forms are unchanged.iftest contains a call, protection starts on the line after the test. Soif x.Add(s) and modifyAllowed:across lines still reports the mutation in the test.if modifyAllowed == False:,!= True,is not Trueandproject.writeEnabled == Falsewere treated as protecting their body. The new_write_flag_compare_polarityonly counts a comparison in the enabling direction._MODIFY_GUARD_RE, which sets the scaffold's FTM_ModifiesDB, now matches compound guards.Closes #352
#351 (P3): receiver regexes with no left boundary
fx) now need a word boundary, soprefxno longer matches._RECEIVER_PREFIX_BOUNDARY. The keyword can still appear anywhere in the identifier, sosubsense,subentry,lexentry,mainentry,self._senseandnonsenseEntryare all still flagged. Only a short list of exact English words is skipped: nonsense, compose, position, purpose, suppose and similar.project, includingmyproject,old_project,subproject,srcProjectandself._project. Any of these could be a real FLExProject handle, so flagging them fails closed. Review found that adding a\bprojectboundary missed real writes, so that part of the issue is not done.Closes #351
Pattern audit
I audited the whole write gate (
find_liblcm_mutations,find_protected_ranges/ProtectionFinder,detect_cud_operations,certify_script_readonly) and the guard and receiver detectors elsewhere insrc/._iter_assign_pairsFor handling over-types, which is the safe direction. The otherseen_linessets only remove duplicate report rows and do not suppress the gate._MODIFY_GUARD_RE.local_recipes.py:544fails closed.extend_protected_ranges_for_guarded_helper_callsis fixed through the ranges it uses.property=patterns and_PATTERN_CREATE_GENERIC._cache\s*\.patterns, since over-matching there fails closed._PATTERN_REPORT_*, which the gate does not use.kernel.py:631, which only feeds pattern-learning stats.position.Note =is now skipped through the English-word list. Otherpos\w*names still match, which fails closed. See the open question below.Tests
.venvpython,-m "not requires_flex"): 4979 passed, 4 skipped, 119 deselected, 45 subtests passed.python scripts/validate_integrity.py server: exit 0. 31 tools registered, USAGE.md documents all 31, golden and minimal success payloads OK.tests/test_issue350_loop_container_write_gate.py,tests/test_issue352_compound_guard.py,tests/test_issue351_regex_word_boundary.py. Each was written first and failed before its fix.Live verification: none run. These are offline-only changes to static analysis.
Known merge interactions
These branches are open in parallel: feat/exclusive-access-gate (#343), fix/preflight-recovery-dx, feat/335-surface-recipes.
validators.py: fix/preflight-recovery-dx probably also touches the preflight and validator paths. Expect conflicts there.CHANGELOG.mdUnreleased: every branch adds entries. These are trivial conflicts.execution.py: this branch changes none of them. The conflicts belong to the other branches, but rerunvalidate_integrity.py serverand the offline suite after rebasing.Open questions
posreceiver be narrowed further, for example toposfollowed by a non-letter or an uppercase letter? The alternative is to leave it as a fail-closed over-match.🤖 Generated with Claude Code