← Commit history

docs: 0.5.9 — a destination the bridge cannot check is not free (write gate, both layers); ecosystem row 34 honesty note; pre-live loop closed after 12 adversary passes

Drew Owens ·db13dd5411 ·1mo ago ·parent 6c0c0dc
4 files changed +72−3
README.md+15−2
@@ -269,7 +269,20 @@ not create, and it never launches or stops SOLIDWORKS.  ## Roadmap -- v0.5.8 (this): **an unreadable count is not "nothing happened".** The drain's+- v0.5.9 (this): **the write gate's own unanswerable question.** The destination+  test was `Path::exists()`, which is false both for "nothing is there" and for "I+  could not look" — a parent it cannot traverse, a UNC share it has no token for, a+  device not ready, an over-length path — and the gate read the second as "free"+  and allowed the write with no opt-in. The com host is a child of Adom Bridge+  while SOLIDWORKS holds the user's network token, so `\\server\share\part.sldprt`+  the bridge cannot stat is one SOLIDWORKS overwrites. Now `symlink_metadata` gives+  three answers — occupied, free, or **unverifiable, which REFUSES**+  (`destination_unverifiable`) before the opt-in is even consulted, in BOTH+  enforcement layers, from one shared function, tested in both. Also: the com host+  now names its version in the `ready` handshake and a server/host skew is an+  explicit boot error, because `closeOutcomeUnknown` read with `unwrap_or(false)`+  turned a field an older host never sends into "nobody needs to look". 270 tests.+- v0.5.8: **an unreadable count is not "nothing happened".** The drain's   discriminator had two arms and needed three: when a close reports success while   the document is still open, a count that did not move means the call did nothing   (`close_by_path_unsupported`) and a count that dropped means something else was@@ -285,7 +298,7 @@ not create, and it never launches or stops SOLIDWORKS.   finding (a bridge-created document written to a new path in a scratch dir must   pass the choke-point rule — pinned in the suite, and a live refusal would be a   bridge bug worth naming), and cleanup that deletes only the two files it wrote-  rather than recursing a directory whose contents it did not write. 265 tests.+  rather than recursing a directory whose contents it did not write. - v0.5.7: **the close key is right; the window it reasoned over was not.**   `done` never consulted `WalkStop::complete()`, so a walk that stopped at its scan   cap or on an invalidated iteration reported a finished drain with temporaries
SKILL.md+8
@@ -300,6 +300,14 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke   live 2144-document session). `swx_close_all_documents` on such a session still   closes, but reports `closed`/`remaining` as **null** with counts, and names the   unknown in `skipped`. Never read a null list as "nothing there".+- **A destination this bridge cannot CHECK is not free.** Every write is gated on+  whether something is already at the destination, and that question has three+  answers: occupied (needs the opt-in), free (a bridge-created document may be+  written there without one), and **unverifiable** — a path this process cannot+  stat, which refuses with `destination_unverifiable` and is NOT waivable by the+  opt-in. It is not a permission question. The bridge and SOLIDWORKS do not share a+  filesystem view, so a UNC path the bridge cannot see may hold a file SOLIDWORKS+  would overwrite. Check it yourself, or write somewhere the bridge can see. - **`close_outcome_unknown` means a HUMAN has to look.** It is the drain's third   outcome: a close reported success, the document is still open, and the count could   not be read — so it cannot be told whether nothing closed or something else did.
docs/ECOSYSTEM-TABLE.md+1−1
@@ -99,7 +99,7 @@ templates**, alisamsam **22**. | 31 | Vendor MCP-server proxy | ✓ 6 verbs (Autodesk ships one) | n/a — Dassault ships none | n/a (is itself an MCP server) | n/a | n/a | n/a | n/a | | 32 | Bridge infrastructure — self-describe, shipped docs, guided demo, uninstall | ✓ all four | ✓ describe/docs/uninstall · ✗ demo (deferred v0.4) | partial (13 MCP resources) | ✗ | partial (docs audit) | partial (resource registry) | ✗ | | 33 | **Action ledger + measured per-verb timings** | ✓ ledger, verb times, live feed, alerts | **✓ ledger + timings recorded live** | ✗ (dev log only) | ✗ | ✗ logging only | partial ("enhanced logging") | ✗ |-| 34 | **Deny-by-default write gate** | ✗ no gate, no confirm/reason in its catalog | **✓ live — unique. Two independent layers, 8 adversarial rounds, held in all 3 live runs** | ✗ | ✗ | partial ("adapter safety wrappers") | partial (macro exec needs permission) | ✗ |+| 34 | **Deny-by-default write gate** | ✗ no gate, no confirm/reason in its catalog | **✓ live — unique. Two independent layers, 12 adversarial rounds, held in all 3 live runs.** HONESTY NOTE: pass 12 (2026-09-09) found that an un-stat-able destination read as FREE — `Path::exists()` is false both for "nothing is there" and for "could not look" (a parent it cannot traverse, a UNC share it has no token for, a device not ready, an over-length path), and the com host does not share a filesystem view with SOLIDWORKS. Fixed in **0.5.9**: `symlink_metadata` gives three answers and an unreadable one REFUSES (`destination_unverifiable`) in BOTH layers, tested in both | ✗ | ✗ | partial ("adapter safety wrappers") | partial (macro exec needs permission) | ✗ | | 35 | Host-ecosystem integration (Adom Bridge, Activity Log, skill) | ✓ native | **✓ native** | ✗ MCP clients only | ✗ | ✗ | ✗ | ✗ | | 36 | **Ownership persists across bridge/host restarts** (bridge-created docs stay closable without opt-in) | ✗ (no ownership model) | **✓ live 2026-09-08** — `owned-docs.json` keyed to the SOLIDWORKS process; proven create → host kill → close in default posture | ✗ | ✗ | ✗ | ✗ | ✗ | | 37 | **Honest UNKNOWN instead of a fabricated empty/zero** (doc list, counts, dialogs, interference) | **✓ LIVE 2026-09-09** — on a real 2144-document session `swx_docs` answers in 3.4 s with `count:2144, truncated:true`, a 10-doc sample (8 import temps) — after 0.5.4 wedged 30 s on the same session (fixed 0.5.5: bounded GetFirstDocument/GetNext walk, never GetDocuments above the cap; simulator now charges per-doc latency) | **⚠ 0.4.x-0.5.5, mock-proven; the ENUMERATION half FAILED LIVE** — `docs_enumeration_failed`, `Option` counts, `unsaved_unknown`, `count:null` semantics in hints; 29 adversary tests. **Live 2026-09-09: `swx_docs` still timed out at 30s and the com host was killed on a 2144-document session** — the degrade path called `GetDocuments` (one call, every handle) and then `GetPathName` on all 2144. `GetDocumentCount` answered in 0.7s. The mock's enumeration was instant, so this row was mock-proven and false live. **Fixed in 0.5.5** (walk `GetFirstDocument`/`GetNext` for the sample only, time-boxed; the drain walks a bounded window too) with the live session held in the suite via a per-document latency model — **re-proof on the live 2144-doc session pending** | ✗ | ✗ | ✗ | ✗ | ✗ |
docs/IMPORT-LEAK.md+48
@@ -477,3 +477,51 @@ closed. The mock now reads every count through one seam that honours the fault.   directory only if it is then empty. A recursive delete of a directory whose   contents we did not write is the same class of act this bridge refuses   everywhere else — and SOLIDWORKS puts its own artefacts in there.++## Adversary round 12 → the write gate's own unanswerable question (0.5.9)++Not about the drain at all: about the choke point every write in this bridge goes+through. The destination test was++```rust+let clobbers = destination.map(|d| !d.is_empty() && Path::new(d).exists());+```++and `Path::exists()` returns **false for two different facts**: nothing is there,+and *I could not look* — a parent directory this process cannot traverse, a UNC+share it has no token for, a device that is not ready, a path over the length+limit. The gate read the second as "the destination is free" and allowed the+write with no opt-in, no confirm and no reason.++It matters because **the two processes do not share a filesystem view.** The com+host is a child of Adom Bridge; SOLIDWORKS runs in the user's interactive session+with the user's network token. `\\server\share\part.sldprt` that the bridge cannot+stat is a file SOLIDWORKS can open and overwrite — with the gate having already+said yes. This was the last place in the bridge where an unanswerable question+read as permission.++0.5.9 gives the question three answers, in one shared function+(`writes::destination_state`) used by **both** enforcement layers:++| `symlink_metadata` | verdict |+|---|---|+| `Ok(_)` | **Occupied** — overwriting needs the full opt-in |+| `Err(NotFound)` | **Free** — a bridge-created document may be written there with no opt-in |+| any other `Err` | **Unverifiable** — REFUSE, `destination_unverifiable` |++`symlink_metadata` rather than `try_exists` deliberately: a dangling symlink IS+something at that path, and writing through it lands somewhere the caller did not+name. The refusal happens *before* the opt-in is consulted, and the hint says why:+the opt-in answers "may I overwrite this?", and this is "I do not know what is+there" — so the opt-in cannot waive it. Both layers are tested: the server's via+`swx_save_as`, and the com host's by driving its stdio protocol directly **with+the opt-in fully granted**, so nothing but its own veto can be what refuses.++**And the sweep item, which is the same defect one layer down.** The server read+`closeOutcomeUnknown` with `unwrap_or(false)` — so a com host too old to send that+field reported "the outcome was determined; nobody needs to look". The com host+and the server are separate binaries and can be a mismatched pair (a half-applied+update, a stale exe in a cache dir). Now: the `ready` handshake carries the host's+version and a skew is an explicit boot error naming both versions, and the field+itself is read as `Option<bool>` where **absent beside a reported close failure is+treated as undetermined**.