SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
477e64f
1mo ago
three-musketeers build — solidworks-bridge Rust port — 2026-09-01
Task: Port the Adom SOLIDWORKS bridge from Python + PowerShell to a single native Rust binary, at top-tier quality (kicad/pup/native-browser bar). Stage: Compiled + mock-tested on Linux; Windows COM backend compile-verified, awaiting a real SOLIDWORKS install for live shakedown (trial serial pending from Hawk Ridge). Rounds: 2 (build → adversary → fix → verify + regression-adversary → final tightening → converge). Models: Builder Opus · Adversary Opus (fresh context each round) · Verifier Sonnet. Plus a parity-diff pass (Sonnet) against the deleted Python implementation.
What was built
One binary, swx-bridge (spawn.kind:"exe", ~10ms cold start, no interpreter to provision), replacing server.py + com_host.py + the PowerShell COM sidecar. Same 23 swx_* verbs, same response shapes, same error codes, same mock data as v0.1.
- Two modes in one exe. HTTP server (tiny_http + serde) that ab drives; and
--com-host, a child process the server spawns from its owncurrent_exe(), speaking JSON-lines over stdio. One file to ship, onekillImageNamefor both. - COM in a killable child, not a thread. A wedged COM call (SOLIDWORKS on a modal, a runaway rebuild) can't be killed as a thread but can as a process. The server owns the child's lifecycle: lazy spawn, one request in flight, per-request timeout, kill + respawn on crash/hang, stderr tail surfaced in the failing verb's
detail. Real COM is late-bound IDispatch via thewindowscrate (GetActiveObjectbeforeCoCreateInstance, STA), behind aBackendtrait with aMockBackendtwin so the whole surface tests with no SOLIDWORKS and no Windows. - Verb families: file management (find/open/docs/save/close/export STEP·IGES·STL·PDF·DXF·DWG·PNG·JPG·Parasolid/pack-and-go), custom properties (get/set), BOM (per-file+configuration quantities with custom-property columns, JSON or CSV to disk), analysis (mass properties, interference, rebuild-health census), plus status/readiness/describe/state/progress/screenshot/com_eval/uninstall.
- SDK etiquette throughout:
_hinton every response,_reportIssueson failures,windowEtiquette:"background", terminalprogressframes + a liveswx_progress, one log line per verb with caller identity, 48h/300-file scratch prune, answx_uninstallthat removes only the bridge's own exports dir.
How to run / exercise it
cd /home/adom/project/solidworks-bridge
cargo build # linux debug (mock mode)
cargo test # 40 unit + 4 e2e + 5 protocol
cargo build --release --target x86_64-pc-windows-gnu # -> target/.../release/swx-bridge.exe (~851K)
SWX_MOCK=1 SWX_SCRATCH_DIR=/tmp/swx ./target/debug/swx-bridge --port 24695 &
curl -s localhost:24695/status
curl -s localhost:24695/command -d '{"command":"swx_bom","args":{"format":"csv"}}'
Plan amendments (builder was right where the plan was wrong)
Recorded in full in docs/RUST-PORT-BRIEF.md → "Build notes — 2026-09-01". Highlights:
| Plan assumed | Builder did instead | Why |
|---|---|---|
Adopt hello-rust's release profile wholesale (incl. panic="abort") |
Dropped panic="abort" |
The com host catches panics and answers them as errors — that needs unwinding. A bad late-bound call should cost one verb, not the user's open documents. |
| "next call respawns the child" is enough crash recovery | Reap-before-write + a single retry limited to pre-execution failures | The naive reading still costs one failed verb; a mid-request death is reported, never silently re-run (re-running an export behind the caller's back is worse than an error). |
swx_com_eval runs raw PowerShell/script |
A late-bound call-chain evaluator ($sw/$doc dotted chains, literal args), parser outside cfg(windows) so it unit-tests here |
No script host on the live machine; auditable; testable on Linux. |
platform: "win32" (Python's sys.platform) |
platform: "windows" |
Cleaner; documented as an intentional value change, not silent drift. |
Adversary findings resolved
Round 1: 1 Critical, 6 High, 8 Med, 7 Low — all fixed or rebutted with reason. Round 2 (regression): 0 substantive, 5 Low residuals + 1 doc nit — all fixed. Selected:
| Finding | Location | Sev | Resolution |
|---|---|---|---|
VARIANT double-free: arg VARIANTs bitwise-copied into a Vec<VARIANT> — the windows 0.62.2 VARIANT DOES have drop glue, so every BSTR/IDispatch arg was freed twice (heap corruption on first real swx_open) |
variant.rs | Critical | Fixed: DISPPARAMS args are Vec<ManuallyDrop<VARIANT>>; whole Var ownership model re-audited to one-AddRef/one-Release. Round-2 adversary confirmed no double-free and no under-free/leak, compile-verified for the Windows target. |
find_files returned OLDEST files when limit tripped (truncated in walk order, then sorted) |
findfiles.rs | High | Fixed: bounded size-limit min-heap by mtime → correct newest-first AND O(limit) memory. |
swx_uninstall remove_dir_all'd ab's own cache root (the running exe) |
verbs.rs, scratch.rs | High | Fixed: scoped to <cache>/exports, best-effort per item; e2e asserts the cache root survives. |
swx_status (the statusVerb ab polls after a timeout) blocked behind the com-host mutex |
verbs.rs, comhost/mod.rs | High | Fixed: try_lock snapshot (busy/hostPid/inFlight); poison distinguished from contention so it can't lie busy after a panic. |
| No panic isolation in HTTP workers → 500 + permanent worker loss + process death | server.rs | High | Fixed: two-layer catch_unwind → JSON success:false + _reportIssues; worker survives. |
swx_bom labelled risk:"read" but wrote a caller-chosen file (no relay-caller approval prompt) |
verbs.rs | High | Fixed: relabelled write; safe_out refuses relative/../drive-relative out on bom-csv, export, pack-and-go. |
Interference Done() skipped on panic; scalar SAFEARRAYs read as []; non-silent OpenDoc fallback; silent-zero rebuild errors; mock/COM eval drift; swx_stop reported failure after quitting |
com.rs, variant.rs, mock.rs | Med | All fixed (Drop guard; scalar→1-elem coercion + VT_UNKNOWN; fallback removed; unreadableFeatures; mock runs the real parser; ExitApp RPC-disconnect = success). |
Round-2 residuals: drive-relative C:foo admitted; status busy-forever after poison; find_files unbounded buffer; watchdog STILL_ACTIVE/259 misread + pid-reuse race; doc nit |
verbs.rs, findfiles.rs, host.rs | Low | All fixed (separator-required absolute check; poison-aware snapshot; bounded heap; WaitForSingleObject; pid-reuse documented as benign). |
Rebutted/kept with reason: coverage gate stays call-based (the three-way bridge.json↔describe↔calls check already enforces it); topLevelOnly default false kept (it's the documented default, echoed back); free_port TOCTOU left (single-instance guard makes any collision a clean failure).
Behavioral verification (Verifier, Sonnet — drove the real binary over HTTP)
| Flow exercised | What the verifier did | Result | Notes |
|---|---|---|---|
| Health + describe | GET /status; swx_describe | PASS | led/summary/tooltip, mock:true, all 23 verbs listed |
| Full CAD flow | start→open asm→bom(json+csv)→mass_props→interference→rebuild→export STEP→pack-and-go→props get/set/save→close→stop | PASS | CSV parsed with Material/PartNo columns; STEP had real bytes; props round-tripped (dirty:true→save→dirty:false) |
| find_files ordering | real .sldprt/.sldasm/.slddrw + ~$lock + .txt fixtures, distinct mtimes | PASS | limit:2 returned the two NEWEST; lock + .txt excluded; subdir descended; query/types correct |
| Error paths | unknown verb, malformed JSON, 9MB body, com_eval w/o reason, bom/interference on a part, .. csv out |
PASS | 400 / 413 / errorCode:"reason_required" / clean assembly-required failure / errorCode:"unsafe_path" |
| Panic recovery | SWX_TEST_PANIC=swx_docs then call, then swx_status |
PASS | clean JSON error, server survived, next request succeeded |
| Slow status | SWX_MOCK_SLOW_MS=6000, swx_start in background, poll swx_status |
PASS | status answered in 0.013s with busy snapshot while start took the full ~6s |
| Uninstall scoping | populate exports/ + sibling dirs, swx_uninstall | PASS | removed only exports/, siblings + root untouched |
| Full suite + cross-compile | cargo test; windows-gnu release build | PASS | 49 passed / 0 failed / 1 ignored; swx-bridge.exe produced, valid MZ PE header |
Open items / human calls
- The Windows COM backend has never executed. It compiles clean for
x86_64-pc-windows-gnuand the double-free class is fixed, but marshalling correctness (late-bound byref[out]params, SAFEARRAY-vs-scalar return shapes,GetErrorCode2's signature) can only be flushed by a live run. Round-2 adversary confidence: ~75% on memory-safety, residual risk is functional/marshalling. First live-run order to exercise the risky marshalling:swx_open(byref errors/warnings) →swx_props_get(Get4 double-byref) →swx_interference(single vs multiple) →swx_pack_and_go. Removing the non-silent OpenDoc6 fallback is the single biggest first-contact risk — watch thatswx_openstill succeeds on a doc with missing references. - Watchdog wedged-child test is
#[ignore]d (PID-namespace-sensitive sandbox); the mechanism is code-verified and the graceful-orphan path is covered. Confirm on a real Windows host that the parent-liveness poll +taskkillpath fires when the server is hard-killed mid-COM-call. - Not done (deliberate, later steps): publish (release zip + manifest + skills pkg to wiki
adom/solidworks-bridgeper the adom-bridge-sdk-publish flow), hero image, and the v0.3 roadmap (drawing-table BOM extraction,swx_show_*family, measured cold/warm start history). - Two cosmetic verifier notes (not defects): the generic COM-error
_hintalso shows for caller-side validation failures (e.g. bom-on-a-part);swx_com_evalgrammar requires a$sw./$doc.prefix, enforced and explained but worth a doc example.
# three-musketeers build — solidworks-bridge Rust port — 2026-09-01
**Task:** Port the Adom SOLIDWORKS bridge from Python + PowerShell to a single native Rust binary, at top-tier quality (kicad/pup/native-browser bar).
**Stage:** Compiled + mock-tested on Linux; Windows COM backend compile-verified, awaiting a real SOLIDWORKS install for live shakedown (trial serial pending from Hawk Ridge).
**Rounds:** 2 (build → adversary → fix → verify + regression-adversary → final tightening → converge).
**Models:** Builder Opus · Adversary Opus (fresh context each round) · Verifier Sonnet. Plus a parity-diff pass (Sonnet) against the deleted Python implementation.
## What was built
One binary, `swx-bridge` (`spawn.kind:"exe"`, ~10ms cold start, no interpreter to provision), replacing `server.py` + `com_host.py` + the PowerShell COM sidecar. Same 23 `swx_*` verbs, same response shapes, same error codes, same mock data as v0.1.
- **Two modes in one exe.** HTTP server (tiny_http + serde) that ab drives; and `--com-host`, a child process the server spawns from its own `current_exe()`, speaking JSON-lines over stdio. One file to ship, one `killImageName` for both.
- **COM in a killable child, not a thread.** A wedged COM call (SOLIDWORKS on a modal, a runaway rebuild) can't be killed as a thread but can as a process. The server owns the child's lifecycle: lazy spawn, one request in flight, per-request timeout, kill + respawn on crash/hang, stderr tail surfaced in the failing verb's `detail`. Real COM is late-bound IDispatch via the `windows` crate (`GetActiveObject` before `CoCreateInstance`, STA), behind a `Backend` trait with a `MockBackend` twin so the whole surface tests with no SOLIDWORKS and no Windows.
- **Verb families:** file management (find/open/docs/save/close/export STEP·IGES·STL·PDF·DXF·DWG·PNG·JPG·Parasolid/pack-and-go), custom properties (get/set), BOM (per-file+configuration quantities with custom-property columns, JSON or CSV to disk), analysis (mass properties, interference, rebuild-health census), plus status/readiness/describe/state/progress/screenshot/com_eval/uninstall.
- **SDK etiquette throughout:** `_hint` on every response, `_reportIssues` on failures, `windowEtiquette:"background"`, terminal `progress` frames + a live `swx_progress`, one log line per verb with caller identity, 48h/300-file scratch prune, an `swx_uninstall` that removes only the bridge's own exports dir.
**How to run / exercise it**
```bash
cd /home/adom/project/solidworks-bridge
cargo build # linux debug (mock mode)
cargo test # 40 unit + 4 e2e + 5 protocol
cargo build --release --target x86_64-pc-windows-gnu # -> target/.../release/swx-bridge.exe (~851K)
SWX_MOCK=1 SWX_SCRATCH_DIR=/tmp/swx ./target/debug/swx-bridge --port 24695 &
curl -s localhost:24695/status
curl -s localhost:24695/command -d '{"command":"swx_bom","args":{"format":"csv"}}'
```
## Plan amendments (builder was right where the plan was wrong)
Recorded in full in `docs/RUST-PORT-BRIEF.md` → "Build notes — 2026-09-01". Highlights:
| Plan assumed | Builder did instead | Why |
|---|---|---|
| Adopt hello-rust's release profile wholesale (incl. `panic="abort"`) | Dropped `panic="abort"` | The com host catches panics and answers them as errors — that needs unwinding. A bad late-bound call should cost one verb, not the user's open documents. |
| "next call respawns the child" is enough crash recovery | Reap-before-write + a single retry limited to pre-execution failures | The naive reading still costs one failed verb; a mid-request death is reported, never silently re-run (re-running an export behind the caller's back is worse than an error). |
| `swx_com_eval` runs raw PowerShell/script | A late-bound call-chain evaluator (`$sw`/`$doc` dotted chains, literal args), parser outside `cfg(windows)` so it unit-tests here | No script host on the live machine; auditable; testable on Linux. |
| `platform: "win32"` (Python's `sys.platform`) | `platform: "windows"` | Cleaner; documented as an intentional value change, not silent drift. |
## Adversary findings resolved
Round 1: **1 Critical, 6 High, 8 Med, 7 Low** — all fixed or rebutted with reason. Round 2 (regression): **0 substantive, 5 Low residuals + 1 doc nit** — all fixed. Selected:
| Finding | Location | Sev | Resolution |
|---|---|---|---|
| VARIANT double-free: arg VARIANTs bitwise-copied into a `Vec<VARIANT>` — the `windows 0.62.2` VARIANT DOES have drop glue, so every BSTR/IDispatch arg was freed twice (heap corruption on first real `swx_open`) | variant.rs | **Critical** | Fixed: DISPPARAMS args are `Vec<ManuallyDrop<VARIANT>>`; whole `Var` ownership model re-audited to one-AddRef/one-Release. Round-2 adversary confirmed no double-free and no under-free/leak, compile-verified for the Windows target. |
| `find_files` returned OLDEST files when `limit` tripped (truncated in walk order, then sorted) | findfiles.rs | High | Fixed: bounded size-`limit` min-heap by mtime → correct newest-first AND O(limit) memory. |
| `swx_uninstall` `remove_dir_all`'d ab's own cache root (the running exe) | verbs.rs, scratch.rs | High | Fixed: scoped to `<cache>/exports`, best-effort per item; e2e asserts the cache root survives. |
| `swx_status` (the statusVerb ab polls after a timeout) blocked behind the com-host mutex | verbs.rs, comhost/mod.rs | High | Fixed: try_lock snapshot (`busy`/`hostPid`/`inFlight`); poison distinguished from contention so it can't lie busy after a panic. |
| No panic isolation in HTTP workers → 500 + permanent worker loss + process death | server.rs | High | Fixed: two-layer `catch_unwind` → JSON `success:false` + `_reportIssues`; worker survives. |
| `swx_bom` labelled `risk:"read"` but wrote a caller-chosen file (no relay-caller approval prompt) | verbs.rs | High | Fixed: relabelled `write`; `safe_out` refuses relative/`..`/drive-relative `out` on bom-csv, export, pack-and-go. |
| Interference `Done()` skipped on panic; scalar SAFEARRAYs read as `[]`; non-silent OpenDoc fallback; silent-zero rebuild errors; mock/COM `eval` drift; `swx_stop` reported failure after quitting | com.rs, variant.rs, mock.rs | Med | All fixed (Drop guard; scalar→1-elem coercion + VT_UNKNOWN; fallback removed; `unreadableFeatures`; mock runs the real parser; ExitApp RPC-disconnect = success). |
| Round-2 residuals: drive-relative `C:foo` admitted; status busy-forever after poison; find_files unbounded buffer; watchdog `STILL_ACTIVE`/259 misread + pid-reuse race; doc nit | verbs.rs, findfiles.rs, host.rs | Low | All fixed (separator-required absolute check; poison-aware snapshot; bounded heap; `WaitForSingleObject`; pid-reuse documented as benign). |
Rebutted/kept with reason: coverage gate stays call-based (the three-way bridge.json↔describe↔calls check already enforces it); `topLevelOnly` default `false` kept (it's the documented default, echoed back); free_port TOCTOU left (single-instance guard makes any collision a clean failure).
## Behavioral verification (Verifier, Sonnet — drove the real binary over HTTP)
| Flow exercised | What the verifier did | Result | Notes |
|---|---|---|---|
| Health + describe | GET /status; swx_describe | PASS | led/summary/tooltip, mock:true, all 23 verbs listed |
| Full CAD flow | start→open asm→bom(json+csv)→mass_props→interference→rebuild→export STEP→pack-and-go→props get/set/save→close→stop | PASS | CSV parsed with Material/PartNo columns; STEP had real bytes; props round-tripped (dirty:true→save→dirty:false) |
| find_files ordering | real .sldprt/.sldasm/.slddrw + ~$lock + .txt fixtures, distinct mtimes | PASS | `limit:2` returned the two NEWEST; lock + .txt excluded; subdir descended; query/types correct |
| Error paths | unknown verb, malformed JSON, 9MB body, com_eval w/o reason, bom/interference on a part, `..` csv out | PASS | 400 / 413 / errorCode:"reason_required" / clean assembly-required failure / errorCode:"unsafe_path" |
| Panic recovery | `SWX_TEST_PANIC=swx_docs` then call, then swx_status | PASS | clean JSON error, server survived, next request succeeded |
| Slow status | `SWX_MOCK_SLOW_MS=6000`, swx_start in background, poll swx_status | PASS | status answered in 0.013s with busy snapshot while start took the full ~6s |
| Uninstall scoping | populate exports/ + sibling dirs, swx_uninstall | PASS | removed only exports/, siblings + root untouched |
| Full suite + cross-compile | cargo test; windows-gnu release build | PASS | 49 passed / 0 failed / 1 ignored; swx-bridge.exe produced, valid MZ PE header |
## Open items / human calls
- **The Windows COM backend has never executed.** It compiles clean for `x86_64-pc-windows-gnu` and the double-free class is fixed, but marshalling correctness (late-bound byref `[out]` params, SAFEARRAY-vs-scalar return shapes, `GetErrorCode2`'s signature) can only be flushed by a live run. Round-2 adversary confidence: ~75% on memory-safety, residual risk is functional/marshalling. **First live-run order to exercise the risky marshalling:** `swx_open` (byref errors/warnings) → `swx_props_get` (Get4 double-byref) → `swx_interference` (single vs multiple) → `swx_pack_and_go`. Removing the non-silent OpenDoc6 fallback is the single biggest first-contact risk — watch that `swx_open` still succeeds on a doc with missing references.
- **Watchdog wedged-child test is `#[ignore]`d** (PID-namespace-sensitive sandbox); the mechanism is code-verified and the graceful-orphan path is covered. Confirm on a real Windows host that the parent-liveness poll + `taskkill` path fires when the server is hard-killed mid-COM-call.
- **Not done (deliberate, later steps):** publish (release zip + manifest + skills pkg to wiki `adom/solidworks-bridge` per the adom-bridge-sdk-publish flow), hero image, and the v0.3 roadmap (drawing-table BOM extraction, `swx_show_*` family, measured cold/warm start history).
- **Two cosmetic verifier notes (not defects):** the generic COM-error `_hint` also shows for caller-side validation failures (e.g. bom-on-a-part); `swx_com_eval` grammar requires a `$sw.`/`$doc.` prefix, enforced and explained but worth a doc example.