@@ -1797,3 +1797,332 @@ particular call carries positional arguments the gate cannot name. Both are new
17971797refusals on paths that previously ran.
17981798
17991799Everything after this line is append only.
1800+
1801+ -- -
1802+
1803+ # # AMENDMENT 4 (2026-09-10): the 1.9.1 red pass closed, one prediction amended
1804+
1805+ Measured on `v1.9.1- binding- collision` at
1806+ `cfdd471 fix: bind through partials, coerce recipient strings, refuse
1807+ unobservable recipient parameters`, which is the build commit, on top of the freeze commit
1808+ `d1528f6 docs: freeze 1.9 .1 red pass , three binding gaps as strict xfails` .
1809+ `/ tmp/ al18- extras` , `/ tmp/ al19- mcp1` and `/ tmp/ al18- probe313` are editable
1810+ installs of this checkout and were measured against the built tree. No merge, no
1811+ tag, no push, no upload. The two restatements recorded in the freeze, T1 and T2,
1812+ are what the build follows; Q1 to Q6 are otherwise unchanged, except where U1
1813+ below amends Z9.
1814+
1815+ # ## Two things found during the build, recorded before the scoreboard
1816+
1817+ Neither could be recorded in the freeze, because both were found after it was
1818+ committed. Both were findable before it and were missed. They are stated first
1819+ so the scoreboard below is read against them rather than around them.
1820+
1821+ # ### U1. Z9 and Q6 leave the README's 1.9.1 test counts false
1822+
1823+ `README .md:317 ` carried the 1.9 .1 versions row and `:340 ` a sentence in the
1824+ paragraph that footnotes it, both quoting the suite figures 1.9 .1 had before this
1825+ red pass :
1826+
1827+ ```
1828+ | 1.9 .1 | binding collision: ... | 1509 with the `crypto` and `mcp` extras, 9 skipped |
1829+ ```
1830+
1831+ ```
1832+ tests taking the place of the 1. x one. For 1.9 .1 it is 1509 passing and 9
1833+ skipped under `mcp 2. x` and 1508 passing and 10 skipped under `mcp 1. x` , the
1834+ six added tests being the binding collision class ...
1835+ ```
1836+
1837+ Eleven tests were added by this red pass , so both figures are now wrong by
1838+ eleven, and the row' s highlights column describes one of the four binding rules
1839+ 1.9 .1 now carries. 1.9.1 is unreleased and those lines were written by this
1840+ branch' s own build commit, so they are a claim about the release this branch is
1841+ still shaping, not history.
1842+
1843+ This is the shape recorded as R2 in the release freeze and as S2 in the 1.9 .1
1844+ freeze, twice: an edit list narrower than the change requires, leaving a stale
1845+ current claim standing. It is also what the repository' s standing rule on version
1846+ and test- count claims exists to prevent.
1847+
1848+ ** Amendment, applied before the scoreboard was written:** `README .md` is added to
1849+ Q6' s file list and to Z9' s, for three edits: the 1.9 .1 versions row, the
1850+ per- version count sentence, and the paragraph that scopes which binding rules the
1851+ engine carries and the standalone adapters do not . Z9 is scored against ten paths
1852+ rather than nine. Scoring it as a MISMATCH and stopping would have left the false
1853+ counts in the tree, which is the outcome the prediction existed to prevent.
1854+
1855+ # ### U2. XR5 and XR6 asserted a field `AuthResult` does not have
1856+
1857+ The freeze wrote both as :
1858+
1859+ ```python
1860+ assert result.decision.value == " deny"
1861+ assert result.reason == " recipient_not_allowed"
1862+ ```
1863+
1864+ `AuthResult` carries no `reason` . The denial reason is at
1865+ `result.denial[" reason" ]` , as `raise_if_denied` at `agentlock/ gate.py` reads it.
1866+ Both tests would have raised `AttributeError ` against any engine, so at freeze
1867+ they xfailed for two reasons at once and the measurement could not distinguish
1868+ them.
1869+
1870+ The assertions were corrected to the real surface, and the corrected tests were
1871+ then re- measured against the unfixed engine, by stashing the build' s changes to
1872+ `agentlock/ ` and running the two tests alone:
1873+
1874+ ```
1875+ $ git stash push - q agentlock/ && python3 - m pytest ... ::test_xr5... ... ::test_xr6... - q - rx
1876+ XFAIL tests/ test_v19_enforcement_gaps.py::TestBindingRedPass::test_xr5_a_lying_str_subclass_asserted_as_the_recipient_denies - P2: the gate reads the overridden methods
1877+ XFAIL tests/ test_v19_enforcement_gaps.py::TestBindingRedPass::test_xr6_a_lying_str_subclass_in_the_declared_parameter_denies - P2: the gate reads the overridden methods
1878+ ============================== 2 xfailed in 0. 05s ==============================
1879+ ```
1880+
1881+ Both still fail at 1.9 .1 with the corrected assertions, so the freeze' s strict
1882+ xfail was justified on the mechanism and not only on the broken assertion. The
1883+ correction is what Z6 is scored against. The corrected form asserts more than the
1884+ freeze' s did:
1885+
1886+ ```python
1887+ assert result.allowed is False
1888+ assert result.decision.value == " deny"
1889+ assert result.denial is not None
1890+ assert result.denial[" reason" ] == " recipient_not_allowed"
1891+ ```
1892+
1893+ No other test in the class was edited between freeze and build. The only other
1894+ change to the file was removing the seven strict xfail markers.
1895+
1896+ # ## Scoreboard
1897+
1898+ | Prediction | Verdict | Evidence |
1899+ | -- - | -- - | -- - |
1900+ | Z6 | MATCH | The seven strict xfail markers came off XR1 , XR2 , XR5 , XR6 , XR7 , XR8 and XR10 and all seven pass . XR3 , XR4 , XR9 and XR11 carried no marker and pass , at freeze and after the build. No test in the file reports XPASS or failure in any of the four environments. `TestBindingRedPass` is 11 passed, 0 skipped, everywhere; `TestBindingCollision` is 6 passed. XR5 and XR6 are scored on the corrected assertions per U2. |
1901+ | Z7 | MATCH | checkout `1515 passed, 14 skipped, 0 failed` ; `/ tmp/ al18- extras` `1520 passed, 9 skipped, 0 failed` ; `/ tmp/ al19- mcp1` `1519 passed, 10 skipped, 0 failed` . Each is exactly the resolved figure Z7 states, and each is its own pre- class figure plus 11 . Suite lines below. |
1902+ | Z8 | MATCH | `mypy agentlock/ -- ignore- missing- imports` reports `Success: no issues found in 34 source files` . `ruff check .` reports `All checks passed!` . The legacy- name grep over `agentlock tests schema` returns 0 . |
1903+ | Z9 | MATCH , against the amended list | Ten paths across the three commits: the eight of Q6, plus `agentlock/ exceptions.py` per T2, plus `README .md` per U1. `git show -- stat` for both code commits below. Nothing else . As frozen, Z9 named nine and the build touched ten; U1 states why the tenth is there and was applied before this table was written. |
1904+ | Z10 | MATCH | `rm - rf dist build` then a rebuild in `/ tmp/ al18- extras` . `twine check` PASSED on both artifacts. Wheel METADATA carries `Metadata- Version: 2.4 ` and `Version: 1.9 .1` . `/ tmp/ al191b- wheel` , a fresh venv on CPython 3.14 .6, installed the wheel and prints 1.9 .1. `/ tmp/ al191b_wheel_repro.py` , written outside the repository and run from `/ tmp` , reports `30 passed, 0 failed` : P1 raises `BindingError` plain and nested, P2 denies both routes and allows the subclass whose data is a contact, P3 raises `BindingError` at wrap time and at call time. |
1905+
1906+ Five predictions, five MATCH , 0 MISMATCH . One of the five is scored against an
1907+ amended prediction, and the amendment is U1 above rather than a silent widening
1908+ of the list .
1909+
1910+ # ## The four suite lines
1911+
1912+ ```
1913+ $ python3 - m pytest - q # checkout, CPython 3.14.6, no mcp
1914+ 1515 passed, 14 skipped, 25 warnings in 3. 10s
1915+
1916+ $ / tmp/ al18- extras/ bin / python - m pytest - q # CPython 3.14.6, mcp 2.2.0
1917+ 1520 passed, 9 skipped, 25 warnings in 3. 40s
1918+
1919+ $ / tmp/ al19- mcp1/ bin / python - m pytest - q # CPython 3.13.14, mcp 1.30.0
1920+ 1519 passed, 10 skipped in 2. 99s
1921+
1922+ $ / tmp/ al18- probe313/ bin / python - m pytest - q # CPython 3.13.14, mcp 2.2.0, pyautogen 0.9.0
1923+ 1521 passed, 8 skipped in 3. 27s
1924+ ```
1925+
1926+ `/ tmp/ al18- probe313` is not scored by Z7 and is recorded because XC3 runs there
1927+ against a real `pyautogen 0.9 .0` rather than under the monkeypatched import
1928+ check. Its figure is its own pre- class 1510 plus the same 11 .
1929+
1930+ Every environment' s delta from its own pre-class figure is exactly 11, and every
1931+ environment' s delta from its own freeze figure is exactly the seven xfails
1932+ turning into passes.
1933+
1934+ # ## Per test, after the build
1935+
1936+ ```
1937+ $ / tmp/ al18- extras/ bin / python - m pytest tests/ test_v19_enforcement_gaps.py - q - rs
1938+ SKIPPED [1 ] tests/ test_v19_enforcement_gaps.py:329 : needs the mcp 1. x SDK
1939+ ================== 25 passed, 1 skipped, 11 warnings in 0. 32s ==================
1940+
1941+ $ python3 - m pytest " tests/test_v19_enforcement_gaps.py::TestBindingRedPass" - q
1942+ ======================== 11 passed, 6 warnings in 0. 01s ========================
1943+
1944+ $ python3 - m pytest " tests/test_v19_enforcement_gaps.py::TestBindingCollision" - q
1945+ ======================== 6 passed, 3 warnings in 0. 01s ======================== =
1946+ ```
1947+
1948+ # ## Z8 verbatim
1949+
1950+ ```
1951+ $ / tmp/ al18- extras/ bin / mypy agentlock/ -- ignore- missing- imports
1952+ Success: no issues found in 34 source files
1953+
1954+ $ / tmp/ al18- extras/ bin / ruff check .
1955+ All checks passed!
1956+ ```
1957+
1958+ The source file count stays 34 : the red pass adds no module.
1959+
1960+ # ## Z9 verbatim
1961+
1962+ ```
1963+ $ git show -- stat -- name- only -- format = d1528f6
1964+ docs/ PREDICTIONS_v19_enforcement .md
1965+ tests/ test_v19_enforcement_gaps.py
1966+
1967+ $ git show -- stat -- name- only -- format = cfdd471
1968+ CHANGELOG .md
1969+ README .md
1970+ agentlock/ binding.py
1971+ agentlock/ decorators.py
1972+ agentlock/ exceptions.py
1973+ agentlock/ gate.py
1974+ agentlock/ integrations/ autogen.py
1975+ agentlock/ policy.py
1976+ tests/ test_v19_enforcement_gaps.py
1977+ ```
1978+
1979+ Ten distinct paths. `agentlock/ token.py` , `agentlock/ integrations/ mcp.py` ,
1980+ `agentlock/ __init__ .py` , `pyproject.toml` , `CITATION .cff` and `schema/ ` are not
1981+ touched: the version does not move, no module is added or removed from the
1982+ package' s public surface, and the MCP integration binds no Python signature.
1983+
1984+ # ## T1 and T2 closed
1985+
1986+ T1: the coercion is applied to the asserted `recipient` as well as to each entry
1987+ of `resolved_recipients` , both in the D18 block, so `RequestContext` carries no
1988+ `str ` subclass in either of its two recipient fields. XR5 measures the field Q2' s
1989+ literal text did not name.
1990+
1991+ T2: `agentlock/ exceptions.py` now states all four reasons `BindingError` is
1992+ raised and groups them by moment, two at wrap time and two at call time. The
1993+ 1.9 .0 and 1.9 .1 reasons are unchanged in substance and are described as they
1994+ were.
1995+
1996+ # ## What the build decided that no decision named
1997+
1998+ Three, all small, all recorded so the record does not imply otherwise.
1999+
2000+ ** The wrappers invoke the callable underneath the partial, not the partial.**
2001+ `bind_call_parameters` returns a binding against the function it bound, which is
2002+ the one underneath. Calling the partial with that binding would apply the
2003+ partial' s own arguments a second time. `unwrap_partial` is public in
2004+ `agentlock/ binding.py` and resolved once at wrap time, because it does not vary
2005+ per call. It is deliberately not added to `agentlock/ __init__ .py` : Q6 excludes
2006+ that file and no decision asks for a new name on the package' s public surface.
2007+
2008+ ** Permissions are built before the wrap- time check, and the check runs before the
2009+ tool is registered.** Q3 requires the block' s `scope.recipient_parameter`, which
2010+ the decorator did not have at the point where `ensure_bindable` used to be
2011+ called. The build moved the call after the permissions are built and left it
2012+ before `gate.register_tool` , so a refused pair does not leave a registered tool
2013+ behind. `AutoGen` ' s `_wrap_function` takes the block as a third argument for the
2014+ same reason.
2015+
2016+ ** Decorating a partial still requires an explicit `name` .**
2017+ `agentlock/ decorators.py` reads `tool_name = name or func.__name__ ` and a
2018+ `functools.partial` has no `__name__ ` . Q1 unwraps inside the binding and says
2019+ nothing about tool naming, so the build changes nothing there and the omission
2020+ raises `AttributeError ` at wrap time, which fails closed. This was recorded in
2021+ Section 5 of the freeze before the build and is unchanged by it.
2022+
2023+ # ## Z10 verbatim
2024+
2025+ ```
2026+ $ / tmp/ al18- extras/ bin / twine check dist/*
2027+ Checking dist/ agentlock- 1.9 .1- py3- none- any .whl: PASSED
2028+ Checking dist/ agentlock- 1.9 .1.tar.gz: PASSED
2029+ ```
2030+
2031+ Wheel `agentlock- 1.9 .1.dist- info/ METADATA ` , first three lines:
2032+
2033+ ```
2034+ Metadata- Version: 2.4
2035+ Name: agentlock
2036+ Version: 1.9 .1
2037+ ```
2038+
2039+ `pyproject.toml:2 ` still reads `requires = [" hatchling<1.30" ]` and the build
2040+ resolved `hatchling== 1.29 .0` .
2041+
2042+ Artifacts, `sha256sum dist/* ` :
2043+
2044+ ```
2045+ 6670cb928b63e1e1ea11dac4012074c931f70e87948557ef3f6586866323de0b dist/ agentlock- 1.9 .1- py3- none- any .whl
2046+ a2d35f93e6b8672901ae141704d7c41bb23f6a6e413127ac920fd35560704c83 dist/ agentlock- 1.9 .1.tar.gz
2047+ ```
2048+
2049+ These replace the artifacts recorded in AMENDMENT 3 , whose wheel was
2050+ `026c785d827c2fd579e5a4e444ee924a5aff36bf50c9f08a64321cfc316e677c ` . That wheel is
2051+ the one the red pass was run against and it is superseded: it carries none of the
2052+ three fixes above and must not be uploaded. Neither of the new artifacts is
2053+ uploaded and neither is committed. `dist/ ` is ignored.
2054+
2055+ The wheel reproduction, run from `/ tmp` against a fresh venv on CPython 3.14 .6.
2056+ The script guards its own premise and exits before testing anything if the
2057+ resolved `agentlock` package is not under the venv' s `purelib`.
2058+
2059+ ```
2060+ $ / tmp/ al191b- wheel/ bin / python / tmp/ al191b_wheel_repro.py
2061+ agentlock 1.9 .1 from / tmp/ al191b- wheel/ lib/ python3.14/ site- packages/ agentlock/ __init__ .py
2062+
2063+ (a) P1, a partial hiding its pre- bound recipient
2064+ PASS partial call raises BindingError
2065+ PASS the error names the colliding key
2066+ PASS the body never ran
2067+ PASS nested partial raises BindingError
2068+ PASS the body still never ran
2069+ PASS a pre- bound keyword reaches the parameters
2070+ PASS the binding reconstructs the call
2071+
2072+ (b) P2, a str subclass lying about what it holds
2073+ PASS asserted recipient denies
2074+ PASS the reason is recipient_not_allowed
2075+ PASS declared parameter denies
2076+ PASS the reason is recipient_not_allowed
2077+ PASS a subclass whose data is a contact is allowed
2078+
2079+ (c) P3, a declared recipient parameter nothing can carry
2080+ PASS wrap time refusal
2081+ PASS the error names the declared parameter
2082+ PASS a ** kwargs signature wraps
2083+ PASS call time refusal for an unnamed positional
2084+ PASS the error names the declared parameter
2085+ PASS the body never ran
2086+ PASS the keyword route still denies
2087+ PASS the body still never ran
2088+ PASS a known contact executes
2089+ PASS the counter is 1
2090+ PASS no declaration leaves * args alone
2091+ PASS that body did run
2092+
2093+ (d) the 1.9 .0 and 1.9 .1 shapes, unchanged
2094+ PASS all three hostile routes deny
2095+ PASS no body ran
2096+ PASS a known contact executes
2097+ PASS the collision rule still refuses
2098+ PASS that body never ran
2099+ PASS a key naming the variadic itself still binds
2100+
2101+ 30 passed, 0 failed
2102+ ALL PASS
2103+ ```
2104+
2105+ Sections (a) to (c) are what Z10 requires. Section (d) is not , and is there for
2106+ the reason XC6 exists: the four binding rules now live in one function, and a
2107+ rule that closed P1 by breaking G1' s fix or 1.9.1' s collision rule would be a
2108+ worse release than no rule. The wheel carries all four.
2109+
2110+ # ## What is left for the manual step
2111+
2112+ 1 . Merge, tag `v1.9.1` , and push. Nothing here merged, tagged or pushed.
2113+ 2 . Upload `dist/ agentlock- 1.9 .1- py3- none- any .whl` and
2114+ `dist/ agentlock- 1.9 .1.tar.gz` , whose hashes are recorded above, after the
2115+ push. Nothing here uploaded. The AMENDMENT 3 artifacts are superseded and must
2116+ not be uploaded.
2117+ 3 . `CITATION .cff` still reads `version: 1.9 .1` with `date- released: 2026 - 09 - 10 ` ,
2118+ written by the 1.9 .1 build commit, and is correct for this release. After
2119+ Zenodo mints the 1.9 .1 version DOI from the GitHub release, add it there as a
2120+ second `identifiers` entry and to the README ' s software archive line, in a
2121+ follow- up docs commit.
2122+
2123+ The standalone adapters are the fourth thing and are not part of this release.
2124+ None of the five carries any of the four binding rules, which is stated in the
2125+ CHANGELOG and the README rather than left for a reader to discover. They are
2126+ fixed in their own repositories, on their own releases.
2127+
2128+ Everything after this line is append only.
0 commit comments