openadapt-verify 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. openadapt_verify-0.2.0/.gitignore +7 -0
  2. openadapt_verify-0.2.0/LICENSE +21 -0
  3. openadapt_verify-0.2.0/PKG-INFO +382 -0
  4. openadapt_verify-0.2.0/README.md +357 -0
  5. openadapt_verify-0.2.0/pyproject.toml +88 -0
  6. openadapt_verify-0.2.0/scripts/check_clean_install.py +319 -0
  7. openadapt_verify-0.2.0/scripts/check_distribution.py +144 -0
  8. openadapt_verify-0.2.0/src/openadapt_verify/__init__.py +65 -0
  9. openadapt_verify-0.2.0/src/openadapt_verify/__main__.py +3 -0
  10. openadapt_verify-0.2.0/src/openadapt_verify/_ledger_config.py +21 -0
  11. openadapt_verify-0.2.0/src/openadapt_verify/adapters/__init__.py +1 -0
  12. openadapt_verify-0.2.0/src/openadapt_verify/adapters/browser_use.py +39 -0
  13. openadapt_verify-0.2.0/src/openadapt_verify/adapters/claude_sdk.py +5 -0
  14. openadapt_verify-0.2.0/src/openadapt_verify/adapters/openai_agents.py +5 -0
  15. openadapt_verify-0.2.0/src/openadapt_verify/adapters/stagehand.py +32 -0
  16. openadapt_verify-0.2.0/src/openadapt_verify/audit.py +742 -0
  17. openadapt_verify-0.2.0/src/openadapt_verify/audit_demo.py +106 -0
  18. openadapt_verify-0.2.0/src/openadapt_verify/cli.py +463 -0
  19. openadapt_verify-0.2.0/src/openadapt_verify/guard.py +130 -0
  20. openadapt_verify-0.2.0/src/openadapt_verify/mcp.py +24 -0
  21. openadapt_verify-0.2.0/src/openadapt_verify/quickstart.py +567 -0
  22. openadapt_verify-0.2.0/src/openadapt_verify/shadow.py +5 -0
  23. openadapt_verify-0.2.0/tests/conftest.py +190 -0
  24. openadapt_verify-0.2.0/tests/test_adapters.py +111 -0
  25. openadapt_verify-0.2.0/tests/test_audit.py +430 -0
  26. openadapt_verify-0.2.0/tests/test_cli_mcp.py +183 -0
  27. openadapt_verify-0.2.0/tests/test_cli_protected_ledger.py +306 -0
  28. openadapt_verify-0.2.0/tests/test_cli_recheck_sticky.py +101 -0
  29. openadapt_verify-0.2.0/tests/test_cli_verdicts.py +299 -0
  30. openadapt_verify-0.2.0/tests/test_guard.py +83 -0
  31. openadapt_verify-0.2.0/tests/test_import_boundary.py +139 -0
  32. openadapt_verify-0.2.0/tests/test_ledger_integrity_hold.py +345 -0
  33. openadapt_verify-0.2.0/tests/test_mcp_protected_ledger.py +223 -0
  34. openadapt_verify-0.2.0/tests/test_mcp_recheck_sticky.py +107 -0
  35. openadapt_verify-0.2.0/tests/test_protected_ledger_capability.py +56 -0
  36. openadapt_verify-0.2.0/tests/test_quickstart.py +80 -0
  37. openadapt_verify-0.2.0/tests/test_verification.py +173 -0
  38. openadapt_verify-0.2.0/uv.lock +1678 -0
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 MLDSAI Inc., Richard Abrich, and contributors.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,382 @@
1
+ Metadata-Version: 2.4
2
+ Name: openadapt-verify
3
+ Version: 0.2.0
4
+ Summary: Verify an agent's writes through an independent system-of-record read path
5
+ Project-URL: Repository, https://github.com/OpenAdaptAI/OpenAdapt/tree/main/packages/verify
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: <3.13,>=3.10
9
+ Requires-Dist: cryptography>=50.0.0
10
+ Requires-Dist: openadapt-flow[receipts]<4,>=3.0.0
11
+ Provides-Extra: claude
12
+ Requires-Dist: openadapt-flow[claude-agent-sdk]<4,>=3.0.0; extra == 'claude'
13
+ Provides-Extra: dev
14
+ Requires-Dist: build>=1.2; extra == 'dev'
15
+ Requires-Dist: mypy>=1.10; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: ruff>=0.4; extra == 'dev'
18
+ Provides-Extra: mcp
19
+ Requires-Dist: openadapt-flow[agent]<4,>=3.0.0; extra == 'mcp'
20
+ Provides-Extra: mysql
21
+ Requires-Dist: pymysql<2,>=1.1; extra == 'mysql'
22
+ Provides-Extra: openai
23
+ Requires-Dist: openadapt-flow[openai-agents]<4,>=3.0.0; extra == 'openai'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # openadapt-verify
27
+
28
+ Check whether an agent's write landed in the system of record, and that
29
+ nothing else changed. openadapt-verify reads the store before and after the
30
+ write through a read path the agent doesn't control (SQL, REST or FHIR), and
31
+ returns `CONFIRMED`, `REFUTED` or `UNCERTAIN` with a signed receipt. The
32
+ writer's own response is never evidence that the write persisted.
33
+
34
+ ```sh
35
+ pip install openadapt-verify
36
+ ```
37
+
38
+ Requires Python 3.10, 3.11 or 3.12. The install is 21 distributions and about
39
+ 41 MB of site-packages, with no vision, OCR, browser, GUI, NumPy or ML stack. It uses the verifier, write ledger, protected journal, SQL guards and
40
+ receipt signing from `openadapt-flow`: the same code, not a copy. Flow's
41
+ vision stack is its `vision` extra, which this package never installs.
42
+
43
+ ## Five-minute quickstart
44
+
45
+ ```sh
46
+ openadapt-verify quickstart
47
+ ```
48
+
49
+ This starts disposable synthetic stores on 127.0.0.1, has a scripted agent
50
+ make three writes against each, and checks each write through an independent
51
+ read path:
52
+
53
+ | Agent write | Outcome |
54
+ |---|---|
55
+ | Saves the values it declared | `CONFIRMED` |
56
+ | Saves a different value than it declared | `REFUTED` |
57
+ | Reports success, but nothing reached the store | `UNCERTAIN` |
58
+
59
+ It runs the REST, SQLite and FHIR R4 read paths. Every result gets a portable
60
+ signed receipt (COSE_Sign1 with a local transparency log), and the quickstart
61
+ checks each receipt's signature. All state goes to a temporary directory that
62
+ is deleted afterwards. The keys are generated for the run; in a deployment,
63
+ pin the issuer and log keys you trust.
64
+
65
+ Add PostgreSQL or MySQL by pointing the quickstart at a **disposable**
66
+ database. It creates and drops its own table. The password comes from
67
+ `OPENADAPT_QUICKSTART_DB_PASSWORD`:
68
+
69
+ ```sh
70
+ pip install 'openadapt-verify[mysql]' # PyMySQL, MIT
71
+ pip install 'psycopg[binary]' # LGPL-3.0; not a dependency of this package
72
+ openadapt-verify quickstart \
73
+ --postgres '{"host": "127.0.0.1", "port": 5432, "user": "qs", "dbname": "qs"}' \
74
+ --mysql '{"host": "127.0.0.1", "port": 3306, "user": "qs", "database": "qs"}'
75
+ ```
76
+
77
+ A MySQL read must return utf8mb4 results. With any other result charset the
78
+ verifier refuses to read, so the quickstart reports `NOT SENT` and the agent
79
+ writes nothing.
80
+
81
+ The contracts the quickstart uses are in
82
+ [`quickstart.py`](src/openadapt_verify/quickstart.py). Each is an
83
+ [`openadapt.verify/v1` contract](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFIER_API.md);
84
+ copy one and point it at your store.
85
+
86
+ ## Audit your automation's success check
87
+
88
+ `openadapt-verify audit` tells you how many wrong saves your own automation
89
+ reports as success. It puts an HTTP proxy in front of your app, runs your
90
+ command through it several times, and on each run changes one write. It then reads the store
91
+ through your contract's read path, never through the proxy, and counts the
92
+ runs where your command said it succeeded but the store disagrees.
93
+
94
+ ```sh
95
+ openadapt-verify audit --demo
96
+ ```
97
+
98
+ The demo audits a script that trusts the save's 2xx reply, against the
99
+ quickstart's synthetic REST store (16 runs, about 20 seconds):
100
+
101
+ ```text
102
+ fault runs bad saves check said OK caught false alarms
103
+ clean 2 0 0 0 0
104
+ no_persist 2 2 2 0 0
105
+ rejected 2 2 0 2 0
106
+ partial 2 2 2 0 0
107
+ wrong_value 2 2 2 0 0
108
+ duplicate 2 2 2 0 0
109
+ extra_write 2 2 2 0 0
110
+ lost_response 2 0 0 0 2
111
+
112
+ Your check (exit code 0) reported success on 10 of 12 bad saves.
113
+ False alarms: 0 of 2 clean runs, 2 on faulted runs whose write still landed correctly.
114
+ ```
115
+
116
+ `--demo readback` audits a script that also lists the items through the app
117
+ and looks for its own. It catches everything except `duplicate` and
118
+ `extra_write`: it finds its record and never asks what else was saved.
119
+
120
+ Point it at your own app:
121
+
122
+ ```sh
123
+ openadapt-verify audit \
124
+ --upstream http://staging-app:8080 --listen 127.0.0.1:9000 \
125
+ --contract verify.yaml --params values.json --runs 16 \
126
+ --report audit.json -- npx playwright test save-order.spec.ts
127
+ ```
128
+
129
+ - Run your command against `--listen` (the proxy) instead of the app. The
130
+ command gets `OPENADAPT_AUDIT_URL` (the proxy), `OPENADAPT_AUDIT_VALUES`
131
+ (the values this run should save, as JSON) and `OPENADAPT_AUDIT_RUN`. It is
132
+ never told which fault a run carries.
133
+ - `values.json` holds the values the command saves; `{run}` in a value becomes
134
+ the run number, so each run's record is distinct.
135
+ - The contract's read path must reach the store directly (the app's own API
136
+ at `--upstream`, or SQL). A contract that reads through the proxy is
137
+ refused.
138
+ - A run succeeds when the command exits 0, or with `--success-regex` when its
139
+ output matches.
140
+ - The fault goes to the run's first save: the first `POST`, `PUT`, `PATCH`
141
+ or `DELETE` whose JSON or form body carries one of the declared values. A
142
+ sign-in or other write before it passes through unchanged. With
143
+ `--write-path REGEX`, the first write whose path matches is the save
144
+ instead, body or not. `--write-method` narrows the methods. Later requests
145
+ in the run, retries included, pass through unchanged.
146
+
147
+ | Fault | What the proxy does to the run's first write |
148
+ |---|---|
149
+ | `no_persist` | Doesn't forward it; answers with the app's last real success reply |
150
+ | `rejected` | Doesn't forward it; answers 409 |
151
+ | `partial` | Blanks one declared value in the JSON or form body |
152
+ | `wrong_value` | Changes one declared value |
153
+ | `duplicate` | Forwards it twice; your command sees the first reply |
154
+ | `extra_write` | Forwards it, then a copy with one value changed |
155
+ | `lost_response` | Forwards it, then closes the connection without a reply |
156
+ | `wrong_record` | Rewrites a field naming the record (`--wrong-record FIELD=VALUE`) |
157
+
158
+ A bad save is any run whose store result isn't `CONFIRMED`. A false alarm is
159
+ a run whose write landed correctly but your command reported failure, such as
160
+ `lost_response` above. A run whose fault couldn't be applied (no write went
161
+ through the proxy, or no declared value was in the body) or whose store
162
+ couldn't be read is reported as not measured. The audit exits 0 when every bad
163
+ save was caught with no false alarms, 1 otherwise, and 2 when nothing was
164
+ measured.
165
+
166
+ Every fault is applied at the HTTP layer, so the audit needs no access to the
167
+ app's code. Two kinds from the verifier's fault set are not run, and the
168
+ report says so: `stale` (overwriting another user's concurrent change) and
169
+ `collateral_unaudited` (a change to a table the read path doesn't cover).
170
+ The faults are a fixed, hand-written set: the counts say which of these kinds
171
+ your check misses, not how often they happen in production.
172
+
173
+ Tested with two scripted HTTP clients against the quickstart's synthetic REST
174
+ store; it has not been run with a browser. The proxy's limits:
175
+
176
+ - Plain HTTP/1.x between your command and the proxy, one request per
177
+ connection. Each response is read in full before it is sent on, so
178
+ streamed responses arrive at once.
179
+ - No `Upgrade`: WebSocket and server-sent events don't pass through.
180
+ - Only `Location` headers that point at `--upstream` are rewritten to the
181
+ proxy. Absolute URLs in page bodies and cookie domains are left as they are.
182
+ - With an `https` upstream, the proxy itself serves plain HTTP, so a browser
183
+ drops the app's `Secure` cookies.
184
+ - `--listen 0.0.0.0` lets anyone who can reach the proxy write to the app;
185
+ the command warns. `--listen [::1]:9000` listens on IPv6 loopback.
186
+
187
+ ## One line per write
188
+
189
+ Plain Python, as a decorator. The function's arguments are the contract's
190
+ values. The call returns the verifier's reply instead of raising, and a write
191
+ the verifier can't read first is never sent:
192
+
193
+ ```python
194
+ from openadapt_verify import verified
195
+
196
+ @verified("verify.yaml", pinned={"account": "account-17"})
197
+ def save_item(code, quantity, account):
198
+ api.post("/items", json={"code": code, "quantity": quantity})
199
+
200
+ reply = save_item(code="item-17", quantity="3", account="account-17")
201
+ reply["outcome"], reply["seal_path"]
202
+ ```
203
+
204
+ Or as a context manager, with a portable signed receipt:
205
+
206
+ ```python
207
+ from openadapt_verify import checked_write
208
+
209
+ with checked_write("verify.yaml", values) as write:
210
+ api.post("/items", json=values)
211
+ write.result.outcome, write.result.write_receipt_path
212
+ ```
213
+
214
+ Agent frameworks, each SDK behind its own extra:
215
+
216
+ ```python
217
+ from openadapt_verify.adapters.claude_sdk import verified # [claude]
218
+ tool = verified(save_order, contract="verify.yaml")
219
+
220
+ from openadapt_verify.adapters.openai_agents import verified # [openai]
221
+ tool = verified(save_order, contract="verify.yaml")
222
+ ```
223
+
224
+ ```sh
225
+ openadapt-verify serve --contracts ./contracts --state ./state # [mcp]
226
+ ```
227
+
228
+ | Extra | Adds |
229
+ |---|---|
230
+ | `mysql` | PyMySQL, for a MySQL read path |
231
+ | `claude` | Claude Agent SDK tool adapter |
232
+ | `openai` | OpenAI Agents SDK tool adapter |
233
+ | `mcp` | The MCP server behind `openadapt-verify serve` |
234
+
235
+ SQLite is in the standard library. For PostgreSQL, install `psycopg`
236
+ yourself: the PostgreSQL drivers whose session the verifier can make
237
+ read-only are LGPL-3.0, so this package declares none.
238
+
239
+ ## Wrap a write
240
+
241
+ Use an existing [`openadapt.verify/v1` contract](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFIER_API.md)
242
+ that reads your store through SQL, HTTP or another supported independent path.
243
+ Pin record and tenant identifiers that the agent must not choose.
244
+
245
+ ```python
246
+ from openadapt_verify import WriteGuard
247
+
248
+ guard = WriteGuard("verify.yaml", pinned={"account": "account-17"})
249
+ reply = guard.run(values, lambda: existing_agent_write(values))
250
+ print(reply["outcome"], reply["seal"], reply["seal_path"])
251
+ ```
252
+
253
+ The canonical guard reads before the write, refuses an unreadable baseline,
254
+ and reads again after the write, including an error or cancellation. It keeps
255
+ the existing uncertain-write ledger and retry protections. `arun` accepts an
256
+ async write. Only calls through the guard are checked; other tools and direct
257
+ calls remain outside it.
258
+
259
+ `CONFIRMED`, `REFUTED` and `UNCERTAIN` describe the store result. A confirmed
260
+ write can still be provisional when its contract requires a later recheck.
261
+ The contract's scope states what was read and what was not checked. It does
262
+ not establish that the agent chose an authorized or appropriate write.
263
+
264
+ ## Two signed formats
265
+
266
+ `WriteGuard` and the SDK/MCP adapters keep their existing result format:
267
+ a confirmed effect receipt can have a **local Seal** at `seal_path`.
268
+ `seal: "sealed"` reports issuance; a store result can remain confirmed when
269
+ signing is off or fails. `verify_local_seal` checks that format against an
270
+ independently pinned key registry and the receipt file's SHA256.
271
+
272
+ For a **portable COSE/SCITT write receipt**, use the canonical verifier API:
273
+
274
+ ```python
275
+ from openadapt_verify import Actor, Contract, begin_write, verify_write
276
+
277
+ pending = begin_write(Contract.load("verify.yaml"), values,
278
+ actor=Actor("agent", "existing-agent"), workdir="./state")
279
+ try:
280
+ existing_agent_write(values)
281
+ finally:
282
+ result = verify_write(pending, write_receipt=True)
283
+ print(result.outcome.value, result.write_receipt, result.write_receipt_path)
284
+ ```
285
+
286
+ Check `write_receipt == "issued"` before using its path. `verify_receipt`
287
+ checks portable receipts with pinned issuer and log public keys and returns
288
+ `PortableReceiptVerification`; `verify_local_seal` returns
289
+ `LocalSealVerification`. A valid signature reports integrity and the pinned
290
+ issuer, separately from the payload's verdict. Never trust a key merely because
291
+ the receipt carries it. No receipt format or cryptography is defined here.
292
+
293
+ ## Existing agents
294
+
295
+ The existing `WriteGuard` compatibility adapter emits Flow's
296
+ `DeprecationWarning` when first requested. Plain `import openadapt_verify`
297
+ does not load that adapter.
298
+
299
+ OpenAI and Claude use the existing tool adapters:
300
+
301
+ ```sh
302
+ pip install 'openadapt-verify[openai]' # or [claude]
303
+ ```
304
+
305
+ Import `verified` from `openadapt_verify.adapters.openai_agents` or
306
+ `openadapt_verify.adapters.claude_sdk`, and pass an existing tool and
307
+ `contract="verify.yaml"`. The Claude module also exports the existing `only`
308
+ helper for restricting the agent's built-in tools.
309
+
310
+ For browser-use, wrap a typed async action **before** registering it with
311
+ `Tools.action`:
312
+
313
+ ```python
314
+ from openadapt_verify.adapters.browser_use import verified
315
+
316
+ checked_save = verified(save_action, contract="verify.yaml")
317
+ tools.action("Save the item")(checked_save)
318
+ ```
319
+
320
+ Parameter names and annotations, including `browser_session`, are preserved.
321
+ The action returns the verifier reply as tool text. Other browser actions are
322
+ unchanged. See the [official custom-action API](https://docs.browser-use.com/open-source/customize/tools/add).
323
+
324
+ For a caller-owned Stagehand Python session:
325
+
326
+ ```python
327
+ from openadapt_verify.adapters.stagehand import verified
328
+
329
+ checked = verified(session, contract="verify.yaml")
330
+ reply = await checked.act(values=values, input=observed_action)
331
+ ```
332
+
333
+ This checks that single `act` call. It does not replace the original session
334
+ or wrap its other methods. See the [official Python session API](https://docs.stagehand.dev/v3/sdk/python).
335
+ Tests use scripted SDK callbacks and local stores; they do not establish live
336
+ model/provider behavior.
337
+
338
+ ## Commands and MCP
339
+
340
+ ```sh
341
+ openadapt-verify run --contract verify.yaml --params values.json --state ./state -- python write.py
342
+ openadapt-verify verify write.receipt.cbor --issuer-key issuer.pub.pem --log-key log.pub.pem
343
+ openadapt-verify recheck --contract verify.yaml --params values.json --recheck-state ./state/WRITE_ID/recheck.json
344
+ pip install 'openadapt-verify[mcp]'
345
+ openadapt-verify serve --contracts ./contracts --state ./state
346
+ ```
347
+
348
+ `run` executes an explicit argv without a shell. It reads back and requests a
349
+ portable receipt even when the command fails. Exit zero requires a final
350
+ confirmed result, issued portable receipt and command exit zero. A provisional
351
+ result exits 5 and reports `recheck_state_path`. Command diagnostics go to stderr.
352
+
353
+ `verify` checks the portable signature and log inclusion against the pinned
354
+ keys. Its JSON `valid` field reports those integrity checks; the exit code
355
+ also requires a final `CONFIRMED` payload. Both commands use 0 for final
356
+ `CONFIRMED`, 3 for `UNCERTAIN`, 4 for `REFUTED` and 5 for provisional confirmation.
357
+ Code 1 reports a signature, signing or command failure; 2 reports a usage,
358
+ configuration or read error. Inspect the JSON outcome and signing fields
359
+ separately before deciding what happened.
360
+
361
+ At or after `recheck_due_at`, `recheck` reads the store using the canonical
362
+ verifier and the private state saved by `run`. Supply the original contract,
363
+ params and `--agent-id`, and retain the same verifier authority and ledger
364
+ directories. The ledger directory must not be writable by the checked agent.
365
+ Before the due time it exits 5 without ending the window.
366
+ A confirmed recheck exits 0, an uncertain one 3 and a refuted one 4. It reports
367
+ the current result and read window as unsigned JSON; the earlier signed
368
+ receipt remains provisional. A write after the recheck read is outside its
369
+ window. A refusal requires a person to check the record before any retry.
370
+
371
+ `serve` exposes the existing `verify_contracts`, `verify_begin`, `verify_check`
372
+ and `verify_recheck` MCP tools. Their contracts remain operator-owned, and a
373
+ refusal never authorizes retrying the write. The `openadapt_verify.shadow`
374
+ module also exposes the canonical shadow verifier without changing its scope.
375
+
376
+ To configure the same protected journal for `run`, `recheck`, and `serve`,
377
+ see [Use the protected journal from installed tools](../flow/docs/VERIFIER_API.md#use-the-protected-journal-from-installed-tools).
378
+ An enrolled journal refuses if a process opens it without its authority.
379
+ The legacy default provides no tamper evidence when the checked agent can
380
+ write to its journal directory.
381
+
382
+ A recheck exits zero only after the exact ledger attempt records confirmation for the same receipt. A prior refutation remains held for person review even if the unexplained change is later removed. A transient unreadable-store recheck can recover after a fresh read of the same originally confirmed write.