SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
477e64f
1mo ago
Rust port brief — solidworks-bridge (swx)
Goal: replace the Python+PowerShell implementation in this repo with a single
Rust binary (swx-bridge.exe) of the SAME 23-verb surface and the same
response shapes. Quality bar: the kicad, pup, and native-browser bridges —
rich _hints on every response, honest error codes, background etiquette,
progress blocks, self-cleaning scratch, a suite that cannot lie.
Why Rust / what the SDK says
Adom Bridge (ab) treats compiled bridges as first-class: spawn.kind: "exe",
ab spawns the entrypoint DIRECTLY appending --port <port> to argv (~10ms cold
start, no interpreter provisioning). Contract identical to node/python:
- Parse
--port Nfrom argv. BindADOM_BIND_HOSTenv (always 127.0.0.1; default to it when absent). NEVER 0.0.0.0 (firewall prompt). - Bind the port FIRST, before anything slow. A failed bind = another instance owns it → print a line and exit 0 (single-instance guard). No SO_REUSEADDR.
- HTTP: GET
/statusand/health→ 200 JSON with{status:"ok", led, summary, tooltip, bridgeVersion, solidworks:{...}, mock}. POST/commandwith{"command":"swx_...","args":{...}}→ JSON result. Unknown path → 404. - Every response ab relays VERBATIM to the calling AI. The
_hinton every verb response IS the documentation the AI reads. Response contract keys:success,errorCode(stable, branchable — e.g.host_app_not_installed,reason_required),windowEtiquette: "background"on anything that touches windows,progressblocks on slow verbs (terminal frame percent:100 + elapsedSec) and a fastswx_progressverb returningactive[]for polling. swx_uninstall: idempotent, non-interactive, fast; removes ONLY what the bridge created (%LOCALAPPDATA%\Adom Bridge\bridges-cache\solidworks\), never the host app or user docs. ab calls it with{"cascade":true}on app uninstall.- Prune what you write: exports scratch dir capped (48h age, 300 files, oldest-first, rate-limited 1/10min).
- Caller identity: ab delivers
args.caller = {aiThread, containerName}(and/orX-Adom-Caller-*headers). Log one compact line per verb:[swx] <command> caller="<thread>" | args: {...}(truncate args ~400 chars). Self-asserted — logging/UX only, NEVER authorization. - Logging volume rules: one line per incoming verb; never per-item in loops; never a parallel log file; line-buffered stdout/stderr (ab captures them).
- bridge.json changes for the port:
spawn.kind:"exe",entrypoint:"swx-bridge.exe",killImageName:"swx-bridge.exe". Keep verbs, timeouts, detect, risk, uninstall, updateManifestUrl as-is.
Architecture (required)
ONE binary, two modes:
Server mode (default): the HTTP bridge server. tiny_http + serde/serde_json (this is what the official hello-rust sample uses; see the size-optimized release profile in its Cargo.toml). Verb handlers port 1:1 from the Python
server.pyin git history / reference copy — same names, same arg names, same response keys, same hints (improve wording freely, never drop information).--com-hostmode: a child process the server spawns FROM ITS OWN EXE (std::env::current_exe()), speaking the same JSON-lines stdio protocol the PowerShell sidecar spoke:{"id":N,"cmd":"...","args":{...}}per line in,{"id":N,"ok":true,"result":{...}}/{"id":N,"ok":false,"error":"...", "detail":"..."}per line out, plus one{"event":"ready","pid":N}line at boot. Diagnostics to stderr only.WHY a child process and not an in-process COM thread: a wedged COM call (modal dialog, hung SOLIDWORKS) cannot be killed as a thread, but the child can be killed + respawned. The server owns: lazy spawn, one request in flight (mutex), per-request timeout, kill + respawn after crash/hang, stderr tail capture surfaced in
detail. On Windows spawn the child with CREATE_NO_WINDOW (0x08000000) — no console may ever blip on screen.Backend trait inside com-host mode:
MockBackend(all platforms; selected bySWX_MOCK=1or non-Windows): port ofsidecar/mock_host.py— same canned data (3-row BOM with bracket/shaft/m4-screw, qty 2/1/8, mass 0.4173 kg, 1 interference, etc.) so the existing HTTP test expectations keep passing.ComBackend(cfg(windows)): real COM via thewindowscrate, late-bound IDispatch (GetIDsOfNames + Invoke), STA-initialized main thread in com-host mode. Sidecar semantics to preserve from the PS version: attach via GetActiveObject("SldWorks.Application") first, only CoCreateInstance (launch) whenlaunch:true; OpenDoc6 with swOpenDocOptions_Silent=1 and byref VT_I4 errors/warnings (fall back to legacy OpenDoc if it returns null); Save3(1, byref, byref); SaveAs3(path, 0, 1) keyed off destination extension; CustomPropertyManager("").GetNames / Get4(name, false, byref BSTR, byref BSTR) / Add3(name, 30 /text/, value, 2 /replace/); CreateMassProperty (Mass, Volume, SurfaceArea, Density, CenterOfMass safearray of f64); assembly GetComponents(topOnly) safearray of IDispatch → per-component GetSuppression()!=0 filter, GetPathName, ReferencedConfiguration, GetModelDoc2 → props; InterferenceDetectionManager (UseTransform=true, TreatCoincidenceAsInterference, GetInterferences / GetInterferenceCount, Done() in a finally); ForceRebuild3(false) + feature walk FirstFeature/GetNextFeature/GetErrorCode2; Pack and Go via Extension.GetPackAndGo → IncludeDrawings, SetSaveToName(true, dir), GetDocumentSaveToNames byref, Extension.SavePackAndGo. Doc types: .sldprt=1 .sldasm=2 .slddrw=3. Doc info shape: {title, path, type ("part"/"assembly"/"drawing"), config, dirty}. The COM layer CANNOT be exercised until SOLIDWORKS installs (trial serial pending) — it must COMPILE for x86_64-pc-windows-gnu, degrade gracefully (clear errors, never panic across the protocol boundary), and keep theeval→swx_com_evalescape hatch (in Rust: accept a limited expression — at minimum<method>(args...)chains on $sw — or document what it supports; reason-gated as today).
Verbs that DON'T go through COM (implement directly in the server, all platforms): swx_status, swx_readiness, swx_describe, swx_progress, swx_find_files (disk walk: .sldprt/.sldasm/.slddrw, skip ~$ lock files, dot/$ dirs, node_modules; newest first; limit + 150s guard; roots default to ~/Documents + ~/Desktop), swx_uninstall, swx_screenshot (POST to ADOM_DIRECT_API_URL/command with X-Adom-Bridge-Token + forwarded X-Adom-Caller-* headers + X-Adom-Caller-Delegate: solidworks — plain HTTP to loopback, hand-roll over TcpStream or use a tiny client; never fail the verb because the callback failed), swx_state (detection + docs via com-host when running), and the BOM CSV assembly (sidecar returns rows; server sorts by name, collects the union of property names as columns, writes CSV to the scratch dir).
Detection (server, cfg(windows)): App Paths registry key
HKLM/HKCU ...\App Paths\SLDWORKS.exe, then path globs (Program Files
SOLIDWORKS Corp variants), plus HKLM\SOFTWARE\SolidWorks subkey enum for
registryVersions, plus tasklist check for SLDWORKS.exe running. Non-Windows:
installed:false. swx_status re-detects each call.
Host-app gate: when NOT mock and NOT installed, every COM-backed verb returns
{success:false, errorCode:"host_app_not_installed", error:"SOLIDWORKS is not installed", _hint:<trial-pending hint>}. The trial hint: a 30-day desktop
trial was requested from Hawk Ridge Systems (reseller); serial arrives by
email; never tell the user to install by hand.
Testing (adapted from the app-testing skill — build ALL of it)
The suite must not be able to lie. No test may assert by reading source files as strings; every test drives the REAL compiled binary over HTTP/stdio.
- Harness: build (debug) and spawn the real server binary on a free port with SWX_MOCK=1 and temp dirs for anything it writes; poll /status until healthy; always kill + clean up. Rust integration tests (tests/*.rs, use std or minimal deps) or keep/extend the existing Python tests/test_verbs.py (works: it drives HTTP exactly as ab does). Prefer BOTH: cargo test for Rust-side units (find_files filters, CSV assembly, prune logic, protocol framing) + the HTTP suite for end-to-end.
- Verb-coverage gate (the interaction-coverage analog): read bridge.json
verbs[]at test time and FAIL if any listed verb has no test entry, and FAIL if swx_describe returns a verb absent from bridge.json or vice versa. A new verb shipped without a test must turn the suite red. - Error-gate analog: every response must parse as JSON, carry a boolean
success(or status ok for GET), never HTTP 500 on well-formed input; a malformed body → clean 400. Assert numbered error paths: unknown verb (hint lists valid commands), missing required args, wrong doc type for bom/ interference, reason gate on swx_com_eval. - Protocol e2e: the real server spawning the REAL com-host child (mock backend) — the over-the-wire loop between our two processes: verb → child → response; child killed mid-run → next verb respawns it and succeeds (crash recovery is a required test); a child that answers garbage lines → server skips them without wedging.
- Effects, not existence: swx_export writes a real file (assert bytes on disk); swx_bom csvPath exists and parses as CSV with the property columns; find_files honors query/types/limit and skips ~$ files (create real fixture files in a temp dir).
- Exit non-zero on any failure. Suite runs green on this Linux container.
Build requirements
cargo build --release --target x86_64-pc-windows-gnuMUST succeed here (toolchain + mingw already installed). Also plaincargo build(linux) for the mock-mode test run. Windows-only code behind cfg(windows).- Use the hello-rust release profile (opt-level z, lto, strip) for the shipped exe. Dependencies: tiny_http, serde, serde_json; windows + winreg behind [target.'cfg(windows)'.dependencies]. Justify any addition in Cargo.toml comments. No async runtime.
- Remove the Python implementation files (server.py, com_host.py, swx_detect.py, caller_identity.py, quiet_console.py, sidecar/*.ps1, sidecar/mock_host.py) — git history keeps them; port tests/test_verbs.py to drive the Rust binary. Update README.md (architecture diagram, dev instructions) and SKILL.md where they mention Python/PowerShell. Update bridge.json spawn block. Keep package.json/install.sh/uninstall.sh (skills pkg) as-is except doc touch-ups.
- Keep BRIDGE_VERSION and Cargo.toml version in lockstep: bump to 0.2.0.
References on disk (read them)
- This repo pre-port (Python impl):
git show HEAD— or the files before your removal commit. The response shapes and hints in server.py are the spec. - Official Rust sample bridge: /tmp/claude-1001/-home-adom-project/ca530728-41cc-4f93-a92d-d32d4afe1153/scratchpad/hr/ (src/main.rs, Cargo.toml, bridge.json — the exe contract in miniature).
- kicad-bridge latest release (the quality bar for hints/etiquette/logging): /tmp/claude-1001/-home-adom-project/ca530728-41cc-4f93-a92d-d32d4afe1153/scratchpad/kb-latest/ (server.py, caller_identity.py, bridge.json).
- pup-bridge page (README/SKILL/docs — the quality bar for docs + response design): /tmp/claude-1001/-home-adom-project/ca530728-41cc-4f93-a92d-d32d4afe1153/scratchpad/pup-bridge/
- Local skills: ~/.claude/skills/adom-bridge-sdk*/SKILL.md (the SDK, manifest, runtime contracts), ~/.claude/skills/pup/SKILL.md, ~/.claude/skills/native-browser-safety/SKILL.md.
Non-goals for this port
- No publishing (release zip / wiki page) — a later step.
- No new verbs beyond the 23 (parity port + quality).
- No UI. No PowerShell anywhere.
Build notes — 2026-09-01
What the port actually did differently from this brief, and why. Everything here is a deliberate deviation, not a shortcut.
Release profile: no panic = "abort". The hello-rust sample sets it for
size, but the com-host child catches panics (catch_unwind) and answers them
as ordinary ok:false responses — which needs unwinding. A late-bound COM call
meeting something it did not expect should cost one verb, not the user's whole
SOLIDWORKS session (open documents included). The exe is still 824 KB.
Crash recovery is transparent, and narrowly so. The brief asked that a
killed child be respawned by the next verb. Naively that costs one failed verb:
the server writes into a dead pipe, then reports it. So ComHost::call now
(a) reaps a child that has already exited BEFORE writing, and (b) retries once
if the write itself fails. The retry is deliberately limited to failures that
happen before the request reaches the child — nothing can have half-happened.
A child that dies MID-request is still reported, never silently re-run: quietly
repeating an export or a Pack and Go would be worse than the error. Stale-id and
non-JSON lines are skipped as specified, under an overall deadline.
swx_status.com.hostPid (additive). Which process holds the COM object is
the one fact you need to debug a wedge, and it is what makes crash recovery
observable — tests/protocol.rs kills that pid and asserts the next verb
succeeds against a different one. Nothing else in the response changed.
swx_com_eval is a call-chain evaluator, not a script host. There is no
PowerShell any more and Rust has no eval, so the escape hatch became a small
grammar: $sw / $doc rooted dotted chains with string, number and boolean
literals ($doc.Extension.CustomPropertyManager("").GetNames()). That is
exactly what prototyping a verb needs, and it is auditable, which for something
driving a live CAD application is the better trade. The parser lives in
comhost/expr.rs OUTSIDE cfg(windows) precisely so it is unit-tested on this
Linux host; only the evaluation is Windows-only. The verb's hints say what it
supports.
lib + bin instead of one main.rs. The integration tests need the crate's
own HTTP client to drive the binary, and the pure logic (CSV assembly, prune
selection, find_files filters, the eval grammar, path helpers) deserves unit
tests. Splitting src/lib.rs off gets both while the tests still spawn the REAL
CARGO_BIN_EXE_swx-bridge.
Version lockstep is enforced by the compiler. BRIDGE_VERSION is checked
against CARGO_PKG_VERSION in a const assertion — they cannot drift. The
suite additionally asserts /status.bridgeVersion == bridge.json.version, which
is what caught bridge.json still saying 0.1.0.
SWX_SCRATCH_DIR override. The Python fallback (<repo>/.scratch when
LOCALAPPDATA is unset) makes no sense for a shipped exe, and left the tests
writing into the developer's real cache. The root is now
SWX_SCRATCH_DIR → %LOCALAPPDATA%\... → OS temp, and swx_uninstall removes
that root — so "uninstall really deleted what the bridge created" became an
assertable fact instead of a claim.
Two test-only knobs in the mock backend, both documented in place and never
set in production: SWX_MOCK_NOISE=1 makes the child emit a non-JSON line, an
event line and a stale-id response before every answer (this is how the
skip-what-you-cannot-parse path is proven over the real wire), and
SWX_MOCK_SLOW_MS gives the "cold start" real duration (this is how
swx_progress answering DURING a slow verb is proven).
_reportIssues on every failure and on the unknown-verb response, per
adom-bridge-sdk-audit. The Python version predated that contract.
Small correctness wins carried over deliberately: the mock's document titles
now use a real basename that splits on both separators (Python's
os.path.basename returned the whole C:\... string when the mock ran on
Linux); detection.platform reports std::env::consts::OS (windows/linux)
rather than Python's win32; the log line truncates on a char boundary, so a
non-ASCII folder name cannot panic the logger.
Threading. tiny_http is fed by a std TcpListener we bind ourselves, so the
single-instance guard is explicit and nothing enables address reuse. Four worker
threads serve requests — not for throughput (COM work serializes on the host
child regardless) but so swx_progress can answer while a 90-second
interference check is in flight. There is a test for exactly that.
Not done, by design: the COM backend has never executed — it compiles for
x86_64-pc-windows-gnu and is written to fail honestly (every error carries
what SOLIDWORKS said, via EXCEPINFO), but its live shakedown waits on the trial
serial. swx_screenshot's ab callback is exercised only in its "ab unreachable"
shape, which is the only shape available off a real desktop.
Build notes — 2026-09-01 (round 2: adversarial-review fixes)
The adversarial reviewer and a parity diff found real defects. What changed:
CRITICAL — VARIANT double-free (variant.rs). The reviewer was right and my
earlier comment was wrong: VARIANT has real drop glue in windows 0.62.2
(needs_drop::<VARIANT>() is true), so building the DISPPARAMS array with
ptr::read made a second owner that double-freed every BSTR / IDispatch
argument. Fixed: the arg array is now Vec<ManuallyDrop<VARIANT>> — the Var
arguments stay the sole owners and clear exactly once. Re-audited every Var
handoff (Var::dispatch, as_dispatch, as_array, coerce): each is now
balanced one-AddRef/one-Release. This was the one that had to be right before
the serial lands.
HIGH. find_files now collects all matches, sorts by mtime, THEN truncates,
so the limit can no longer drop the newest file (regression test added).
swx_uninstall removes ONLY <cache>/exports, best-effort per item, never the
cache root ab owns (unit + e2e tests). swx_status (the statusVerb) try_locks a
non-blocking snapshot so polling it never waits on a 580s Pack-and-Go; shutdown
kills the child by pid when the request lock is held, so the cascade can't
block. HTTP workers now catch_unwind at two layers (per-verb → JSON error with
_reportIssues; per-request → worker survives); test-only fault injection
(SWX_TEST_PANIC, debug builds only) proves it end to end. swx_bom relabelled
risk "write", and a path-traversal guard (safe_out) refuses a relative or
.. destination for bom-csv, export and pack-and-go.
MED COM invariants (compile-only until the serial lands). Interference
Done() is now a Drop guard (survives panics), not a trailing statement. The
OpenDoc6 legacy fallback is gone (it was non-silent — a modal-dialog risk).
rebuild_check distinguishes a clean feature from an unreadable one
(unreadableFeatures, code:null). swx_stop treats the expected RPC disconnect
from ExitApp as success. as_array coerces a scalar to a one-element vec (the
PowerShell @(...) semantics), and handles VT_UNKNOWN. bridge.json's
hostAppOptionalVerbs gains uninstall + state (so the cascade and state work with
SOLIDWORKS absent — the current target), and the 3DEXPERIENCE glob is added.
The mock now runs the SAME parse_chain as the real backend, so the eval grammar
is genuinely exercised over the wire.
Child watchdog (#14). The child now runs a watchdog thread that polls the
parent server's liveness (SWX_PARENT_PID) and exits if the server is
hard-killed while a COM call is wedged — the one case the plain stdin-EOF path
(only checked between requests) cannot catch. Proven to work in a plain shell;
the wedged-case integration test is #[ignore]d because it is PID-namespace
sensitive (a sandboxed observer cannot always see or signal the child in its
namespace), with the always-on graceful-orphan test as cover.
LOW. EXCEPINFO BSTRs are freed after each failed Invoke and the bad-arg
index is included in the error. 8MB request-body cap → 413. 400/413 responses
carry _hint + _reportIssues. swx_screenshot no longer flips to failure on a
present-but-null error, and its gate/doc are reconciled (it needs a running
window, so it is gated). swx_export echoes the ACTUAL output extension
(outputExtension). The detect.rs env test no longer calls set_var (was a
data race). find_files accepts a bare-string types and a numeric-string
limit (Python parity); an empty/unmatched types means "all" again (the
previous code returned zero files, contradicting its own doc).
Intentional, documented parity changes (NOT silent drift):
solidworks.platformreports"windows"/"linux"(std::env::consts::OS), not Python's"win32". Cleaner; a caller branching on the old literal must update. Called out here so it is a decision, not an accident.- BOM row
fileuses a backslash separator (C:\mock\bracket.sldprt); Python emitted a forward slash when the mock ran on Linux. Rust is correct. - Re-opening an already-open doc moves it to the end of the mock's list (Python kept its position); this only affects close_doc's fallback target in mock mode. No test locks either old string.
Rebutted / deferred:
- Coverage gate "accepts a bare call with zero assertions" (#21): left as is. The harness RECORDS every call and the gate checks bridge.json ⇔ describe ⇔ calls-made in all three directions; in practice every verb in the suite is asserted on. Requiring a per-verb assertion count would add machinery for no real coverage the three-way check doesn't already give. free_port TOCTOU is acknowledged and, as the reviewer agreed, fine to leave (the single-instance guard turns any collision into a clean failure, not a hang).
- swx_bom.topLevelOnly defaulting to false (#E): kept — false is the documented default and the flag is echoed back, so a caller always sees what ran.
# Rust port brief — solidworks-bridge (swx)
Goal: replace the Python+PowerShell implementation in this repo with a single
Rust binary (`swx-bridge.exe`) of the SAME 23-verb surface and the same
response shapes. Quality bar: the kicad, pup, and native-browser bridges —
rich `_hint`s on every response, honest error codes, background etiquette,
progress blocks, self-cleaning scratch, a suite that cannot lie.
## Why Rust / what the SDK says
Adom Bridge (ab) treats compiled bridges as first-class: `spawn.kind: "exe"`,
ab spawns the entrypoint DIRECTLY appending `--port <port>` to argv (~10ms cold
start, no interpreter provisioning). Contract identical to node/python:
- Parse `--port N` from argv. Bind `ADOM_BIND_HOST` env (always 127.0.0.1;
default to it when absent). NEVER 0.0.0.0 (firewall prompt).
- Bind the port FIRST, before anything slow. A failed bind = another instance
owns it → print a line and exit 0 (single-instance guard). No SO_REUSEADDR.
- HTTP: GET `/status` and `/health` → 200 JSON with `{status:"ok", led,
summary, tooltip, bridgeVersion, solidworks:{...}, mock}`. POST `/command`
with `{"command":"swx_...","args":{...}}` → JSON result. Unknown path → 404.
- Every response ab relays VERBATIM to the calling AI. The `_hint` on every
verb response IS the documentation the AI reads. Response contract keys:
`success`, `errorCode` (stable, branchable — e.g. `host_app_not_installed`,
`reason_required`), `windowEtiquette: "background"` on anything that touches
windows, `progress` blocks on slow verbs (terminal frame percent:100 +
elapsedSec) and a fast `swx_progress` verb returning `active[]` for polling.
- `swx_uninstall`: idempotent, non-interactive, fast; removes ONLY what the
bridge created (`%LOCALAPPDATA%\Adom Bridge\bridges-cache\solidworks\`), never
the host app or user docs. ab calls it with `{"cascade":true}` on app uninstall.
- Prune what you write: exports scratch dir capped (48h age, 300 files,
oldest-first, rate-limited 1/10min).
- Caller identity: ab delivers `args.caller = {aiThread, containerName}` (and/or
`X-Adom-Caller-*` headers). Log one compact line per verb:
`[swx] <command> caller="<thread>" | args: {...}` (truncate args ~400 chars).
Self-asserted — logging/UX only, NEVER authorization.
- Logging volume rules: one line per incoming verb; never per-item in loops;
never a parallel log file; line-buffered stdout/stderr (ab captures them).
- bridge.json changes for the port: `spawn.kind:"exe"`,
`entrypoint:"swx-bridge.exe"`, `killImageName:"swx-bridge.exe"`. Keep verbs,
timeouts, detect, risk, uninstall, updateManifestUrl as-is.
## Architecture (required)
ONE binary, two modes:
1. **Server mode** (default): the HTTP bridge server. tiny_http + serde/serde_json
(this is what the official hello-rust sample uses; see the size-optimized
release profile in its Cargo.toml). Verb handlers port 1:1 from the Python
`server.py` in git history / reference copy — same names, same arg names, same
response keys, same hints (improve wording freely, never drop information).
2. **`--com-host` mode**: a child process the server spawns FROM ITS OWN EXE
(`std::env::current_exe()`), speaking the same JSON-lines stdio protocol the
PowerShell sidecar spoke: `{"id":N,"cmd":"...","args":{...}}` per line in,
`{"id":N,"ok":true,"result":{...}}` / `{"id":N,"ok":false,"error":"...",
"detail":"..."}` per line out, plus one `{"event":"ready","pid":N}` line at
boot. Diagnostics to stderr only.
WHY a child process and not an in-process COM thread: a wedged COM call
(modal dialog, hung SOLIDWORKS) cannot be killed as a thread, but the child
can be killed + respawned. The server owns: lazy spawn, one request in
flight (mutex), per-request timeout, kill + respawn after crash/hang, stderr
tail capture surfaced in `detail`. On Windows spawn the child with
CREATE_NO_WINDOW (0x08000000) — no console may ever blip on screen.
3. **Backend trait** inside com-host mode:
- `MockBackend` (all platforms; selected by `SWX_MOCK=1` or non-Windows):
port of `sidecar/mock_host.py` — same canned data (3-row BOM with
bracket/shaft/m4-screw, qty 2/1/8, mass 0.4173 kg, 1 interference, etc.)
so the existing HTTP test expectations keep passing.
- `ComBackend` (cfg(windows)): real COM via the `windows` crate,
late-bound IDispatch (GetIDsOfNames + Invoke), STA-initialized main
thread in com-host mode. Sidecar semantics to preserve from the PS
version: attach via GetActiveObject("SldWorks.Application") first, only
CoCreateInstance (launch) when `launch:true`; OpenDoc6 with
swOpenDocOptions_Silent=1 and byref VT_I4 errors/warnings (fall back to
legacy OpenDoc if it returns null); Save3(1, byref, byref); SaveAs3(path,
0, 1) keyed off destination extension; CustomPropertyManager("").GetNames /
Get4(name, false, byref BSTR, byref BSTR) / Add3(name, 30 /*text*/, value,
2 /*replace*/); CreateMassProperty (Mass, Volume, SurfaceArea, Density,
CenterOfMass safearray of f64); assembly GetComponents(topOnly) safearray
of IDispatch → per-component GetSuppression()!=0 filter, GetPathName,
ReferencedConfiguration, GetModelDoc2 → props; InterferenceDetectionManager
(UseTransform=true, TreatCoincidenceAsInterference, GetInterferences /
GetInterferenceCount, Done() in a finally); ForceRebuild3(false) + feature
walk FirstFeature/GetNextFeature/GetErrorCode2; Pack and Go via
Extension.GetPackAndGo → IncludeDrawings, SetSaveToName(true, dir),
GetDocumentSaveToNames byref, Extension.SavePackAndGo. Doc types:
.sldprt=1 .sldasm=2 .slddrw=3. Doc info shape: {title, path, type
("part"/"assembly"/"drawing"), config, dirty}.
The COM layer CANNOT be exercised until SOLIDWORKS installs (trial serial
pending) — it must COMPILE for x86_64-pc-windows-gnu, degrade gracefully
(clear errors, never panic across the protocol boundary), and keep the
`eval`→`swx_com_eval` escape hatch (in Rust: accept a limited expression —
at minimum `<method>(args...)` chains on $sw — or document what it
supports; reason-gated as today).
Verbs that DON'T go through COM (implement directly in the server, all
platforms): swx_status, swx_readiness, swx_describe, swx_progress,
swx_find_files (disk walk: .sldprt/.sldasm/.slddrw, skip ~$ lock files, dot/$
dirs, node_modules; newest first; limit + 150s guard; roots default to
~/Documents + ~/Desktop), swx_uninstall, swx_screenshot (POST to
ADOM_DIRECT_API_URL/command with X-Adom-Bridge-Token + forwarded
X-Adom-Caller-* headers + X-Adom-Caller-Delegate: solidworks — plain HTTP to
loopback, hand-roll over TcpStream or use a tiny client; never fail the verb
because the callback failed), swx_state (detection + docs via com-host when
running), and the BOM CSV assembly (sidecar returns rows; server sorts by name,
collects the union of property names as columns, writes CSV to the scratch dir).
Detection (server, cfg(windows)): App Paths registry key
HKLM/HKCU `...\App Paths\SLDWORKS.exe`, then path globs (Program Files
SOLIDWORKS Corp variants), plus HKLM\SOFTWARE\SolidWorks subkey enum for
registryVersions, plus tasklist check for SLDWORKS.exe running. Non-Windows:
installed:false. `swx_status` re-detects each call.
Host-app gate: when NOT mock and NOT installed, every COM-backed verb returns
`{success:false, errorCode:"host_app_not_installed", error:"SOLIDWORKS is not
installed", _hint:<trial-pending hint>}`. The trial hint: a 30-day desktop
trial was requested from Hawk Ridge Systems (reseller); serial arrives by
email; never tell the user to install by hand.
## Testing (adapted from the app-testing skill — build ALL of it)
The suite must not be able to lie. No test may assert by reading source files
as strings; every test drives the REAL compiled binary over HTTP/stdio.
1. **Harness**: build (debug) and spawn the real server binary on a free port
with SWX_MOCK=1 and temp dirs for anything it writes; poll /status until
healthy; always kill + clean up. Rust integration tests (tests/*.rs, use
std or minimal deps) or keep/extend the existing Python tests/test_verbs.py
(works: it drives HTTP exactly as ab does). Prefer BOTH: cargo test for
Rust-side units (find_files filters, CSV assembly, prune logic, protocol
framing) + the HTTP suite for end-to-end.
2. **Verb-coverage gate** (the interaction-coverage analog): read bridge.json
`verbs[]` at test time and FAIL if any listed verb has no test entry, and
FAIL if swx_describe returns a verb absent from bridge.json or vice versa.
A new verb shipped without a test must turn the suite red.
3. **Error-gate analog**: every response must parse as JSON, carry a boolean
`success` (or status ok for GET), never HTTP 500 on well-formed input; a
malformed body → clean 400. Assert numbered error paths: unknown verb (hint
lists valid commands), missing required args, wrong doc type for bom/
interference, reason gate on swx_com_eval.
4. **Protocol e2e**: the real server spawning the REAL com-host child (mock
backend) — the over-the-wire loop between our two processes: verb → child
→ response; child killed mid-run → next verb respawns it and succeeds
(crash recovery is a required test); a child that answers garbage lines →
server skips them without wedging.
5. **Effects, not existence**: swx_export writes a real file (assert bytes on
disk); swx_bom csvPath exists and parses as CSV with the property columns;
find_files honors query/types/limit and skips ~$ files (create real fixture
files in a temp dir).
6. Exit non-zero on any failure. Suite runs green on this Linux container.
## Build requirements
- `cargo build --release --target x86_64-pc-windows-gnu` MUST succeed here
(toolchain + mingw already installed). Also plain `cargo build` (linux) for
the mock-mode test run. Windows-only code behind cfg(windows).
- Use the hello-rust release profile (opt-level z, lto, strip) for the shipped
exe. Dependencies: tiny_http, serde, serde_json; windows + winreg behind
[target.'cfg(windows)'.dependencies]. Justify any addition in Cargo.toml
comments. No async runtime.
- Remove the Python implementation files (server.py, com_host.py, swx_detect.py,
caller_identity.py, quiet_console.py, sidecar/*.ps1, sidecar/mock_host.py) —
git history keeps them; port tests/test_verbs.py to drive the Rust binary.
Update README.md (architecture diagram, dev instructions) and SKILL.md where
they mention Python/PowerShell. Update bridge.json spawn block. Keep
package.json/install.sh/uninstall.sh (skills pkg) as-is except doc touch-ups.
- Keep BRIDGE_VERSION and Cargo.toml version in lockstep: bump to 0.2.0.
## References on disk (read them)
- This repo pre-port (Python impl): `git show HEAD` — or the files before your
removal commit. The response shapes and hints in server.py are the spec.
- Official Rust sample bridge: /tmp/claude-1001/-home-adom-project/ca530728-41cc-4f93-a92d-d32d4afe1153/scratchpad/hr/
(src/main.rs, Cargo.toml, bridge.json — the exe contract in miniature).
- kicad-bridge latest release (the quality bar for hints/etiquette/logging):
/tmp/claude-1001/-home-adom-project/ca530728-41cc-4f93-a92d-d32d4afe1153/scratchpad/kb-latest/ (server.py, caller_identity.py, bridge.json).
- pup-bridge page (README/SKILL/docs — the quality bar for docs + response
design): /tmp/claude-1001/-home-adom-project/ca530728-41cc-4f93-a92d-d32d4afe1153/scratchpad/pup-bridge/
- Local skills: ~/.claude/skills/adom-bridge-sdk*/SKILL.md (the SDK, manifest,
runtime contracts), ~/.claude/skills/pup/SKILL.md, ~/.claude/skills/native-browser-safety/SKILL.md.
## Non-goals for this port
- No publishing (release zip / wiki page) — a later step.
- No new verbs beyond the 23 (parity port + quality).
- No UI. No PowerShell anywhere.
## Build notes — 2026-09-01
What the port actually did differently from this brief, and why. Everything
here is a deliberate deviation, not a shortcut.
**Release profile: no `panic = "abort"`.** The hello-rust sample sets it for
size, but the com-host child catches panics (`catch_unwind`) and answers them
as ordinary `ok:false` responses — which needs unwinding. A late-bound COM call
meeting something it did not expect should cost one verb, not the user's whole
SOLIDWORKS session (open documents included). The exe is still 824 KB.
**Crash recovery is transparent, and narrowly so.** The brief asked that a
killed child be respawned by the next verb. Naively that costs one failed verb:
the server writes into a dead pipe, then reports it. So `ComHost::call` now
(a) reaps a child that has already exited BEFORE writing, and (b) retries once
if the write itself fails. The retry is deliberately limited to failures that
happen *before* the request reaches the child — nothing can have half-happened.
A child that dies MID-request is still reported, never silently re-run: quietly
repeating an export or a Pack and Go would be worse than the error. Stale-id and
non-JSON lines are skipped as specified, under an overall deadline.
**`swx_status.com.hostPid` (additive).** Which process holds the COM object is
the one fact you need to debug a wedge, and it is what makes crash recovery
observable — `tests/protocol.rs` kills that pid and asserts the next verb
succeeds against a *different* one. Nothing else in the response changed.
**`swx_com_eval` is a call-chain evaluator, not a script host.** There is no
PowerShell any more and Rust has no eval, so the escape hatch became a small
grammar: `$sw` / `$doc` rooted dotted chains with string, number and boolean
literals (`$doc.Extension.CustomPropertyManager("").GetNames()`). That is
exactly what prototyping a verb needs, and it is auditable, which for something
driving a live CAD application is the better trade. The parser lives in
`comhost/expr.rs` OUTSIDE `cfg(windows)` precisely so it is unit-tested on this
Linux host; only the evaluation is Windows-only. The verb's hints say what it
supports.
**lib + bin instead of one `main.rs`.** The integration tests need the crate's
own HTTP client to drive the binary, and the pure logic (CSV assembly, prune
selection, find_files filters, the eval grammar, path helpers) deserves unit
tests. Splitting `src/lib.rs` off gets both while the tests still spawn the REAL
`CARGO_BIN_EXE_swx-bridge`.
**Version lockstep is enforced by the compiler.** `BRIDGE_VERSION` is checked
against `CARGO_PKG_VERSION` in a `const` assertion — they cannot drift. The
suite additionally asserts `/status.bridgeVersion == bridge.json.version`, which
is what caught bridge.json still saying 0.1.0.
**`SWX_SCRATCH_DIR` override.** The Python fallback (`<repo>/.scratch` when
`LOCALAPPDATA` is unset) makes no sense for a shipped exe, and left the tests
writing into the developer's real cache. The root is now
`SWX_SCRATCH_DIR` → `%LOCALAPPDATA%\...` → OS temp, and `swx_uninstall` removes
that root — so "uninstall really deleted what the bridge created" became an
assertable fact instead of a claim.
**Two test-only knobs in the mock backend**, both documented in place and never
set in production: `SWX_MOCK_NOISE=1` makes the child emit a non-JSON line, an
event line and a stale-id response before every answer (this is how the
skip-what-you-cannot-parse path is proven over the real wire), and
`SWX_MOCK_SLOW_MS` gives the "cold start" real duration (this is how
`swx_progress` answering DURING a slow verb is proven).
**`_reportIssues` on every failure** and on the unknown-verb response, per
`adom-bridge-sdk-audit`. The Python version predated that contract.
**Small correctness wins carried over deliberately:** the mock's document titles
now use a real basename that splits on both separators (Python's
`os.path.basename` returned the whole `C:\...` string when the mock ran on
Linux); `detection.platform` reports `std::env::consts::OS` (`windows`/`linux`)
rather than Python's `win32`; the log line truncates on a char boundary, so a
non-ASCII folder name cannot panic the logger.
**Threading.** tiny_http is fed by a std `TcpListener` we bind ourselves, so the
single-instance guard is explicit and nothing enables address reuse. Four worker
threads serve requests — not for throughput (COM work serializes on the host
child regardless) but so `swx_progress` can answer while a 90-second
interference check is in flight. There is a test for exactly that.
**Not done, by design:** the COM backend has never executed — it compiles for
`x86_64-pc-windows-gnu` and is written to fail honestly (every error carries
what SOLIDWORKS said, via EXCEPINFO), but its live shakedown waits on the trial
serial. `swx_screenshot`'s ab callback is exercised only in its "ab unreachable"
shape, which is the only shape available off a real desktop.
## Build notes — 2026-09-01 (round 2: adversarial-review fixes)
The adversarial reviewer and a parity diff found real defects. What changed:
**CRITICAL — VARIANT double-free (variant.rs).** The reviewer was right and my
earlier comment was wrong: VARIANT has real drop glue in windows 0.62.2
(`needs_drop::<VARIANT>()` is true), so building the DISPPARAMS array with
`ptr::read` made a second owner that double-freed every BSTR / IDispatch
argument. Fixed: the arg array is now `Vec<ManuallyDrop<VARIANT>>` — the `Var`
arguments stay the sole owners and clear exactly once. Re-audited every Var
handoff (`Var::dispatch`, `as_dispatch`, `as_array`, `coerce`): each is now
balanced one-AddRef/one-Release. This was the one that had to be right before
the serial lands.
**HIGH.** find_files now collects all matches, sorts by mtime, THEN truncates,
so the limit can no longer drop the newest file (regression test added).
swx_uninstall removes ONLY `<cache>/exports`, best-effort per item, never the
cache root ab owns (unit + e2e tests). swx_status (the statusVerb) try_locks a
non-blocking snapshot so polling it never waits on a 580s Pack-and-Go; shutdown
kills the child by pid when the request lock is held, so the cascade can't
block. HTTP workers now catch_unwind at two layers (per-verb → JSON error with
_reportIssues; per-request → worker survives); test-only fault injection
(`SWX_TEST_PANIC`, debug builds only) proves it end to end. swx_bom relabelled
risk "write", and a path-traversal guard (`safe_out`) refuses a relative or
`..` destination for bom-csv, export and pack-and-go.
**MED COM invariants (compile-only until the serial lands).** Interference
`Done()` is now a Drop guard (survives panics), not a trailing statement. The
OpenDoc6 legacy fallback is gone (it was non-silent — a modal-dialog risk).
rebuild_check distinguishes a clean feature from an unreadable one
(`unreadableFeatures`, code:null). swx_stop treats the expected RPC disconnect
from ExitApp as success. as_array coerces a scalar to a one-element vec (the
PowerShell `@(...)` semantics), and handles VT_UNKNOWN. bridge.json's
hostAppOptionalVerbs gains uninstall + state (so the cascade and state work with
SOLIDWORKS absent — the current target), and the 3DEXPERIENCE glob is added.
The mock now runs the SAME parse_chain as the real backend, so the eval grammar
is genuinely exercised over the wire.
**Child watchdog (#14).** The child now runs a watchdog thread that polls the
parent server's liveness (`SWX_PARENT_PID`) and exits if the server is
hard-killed while a COM call is wedged — the one case the plain stdin-EOF path
(only checked between requests) cannot catch. Proven to work in a plain shell;
the wedged-case integration test is `#[ignore]`d because it is PID-namespace
sensitive (a sandboxed observer cannot always see or signal the child in its
namespace), with the always-on graceful-orphan test as cover.
**LOW.** EXCEPINFO BSTRs are freed after each failed Invoke and the bad-arg
index is included in the error. 8MB request-body cap → 413. 400/413 responses
carry _hint + _reportIssues. swx_screenshot no longer flips to failure on a
present-but-null `error`, and its gate/doc are reconciled (it needs a running
window, so it is gated). swx_export echoes the ACTUAL output extension
(`outputExtension`). The detect.rs env test no longer calls set_var (was a
data race). find_files accepts a bare-string `types` and a numeric-string
`limit` (Python parity); an empty/unmatched `types` means "all" again (the
previous code returned zero files, contradicting its own doc).
**Intentional, documented parity changes (NOT silent drift):**
- `solidworks.platform` reports `"windows"`/`"linux"` (`std::env::consts::OS`),
not Python's `"win32"`. Cleaner; a caller branching on the old literal must
update. Called out here so it is a decision, not an accident.
- BOM row `file` uses a backslash separator (`C:\mock\bracket.sldprt`); Python
emitted a forward slash when the mock ran on Linux. Rust is correct.
- Re-opening an already-open doc moves it to the end of the mock's list (Python
kept its position); this only affects close_doc's fallback target in mock
mode. No test locks either old string.
**Rebutted / deferred:**
- Coverage gate "accepts a bare call with zero assertions" (#21): left as is.
The harness RECORDS every call and the gate checks bridge.json ⇔ describe ⇔
calls-made in all three directions; in practice every verb in the suite is
asserted on. Requiring a per-verb assertion count would add machinery for no
real coverage the three-way check doesn't already give. free_port TOCTOU is
acknowledged and, as the reviewer agreed, fine to leave (the single-instance
guard turns any collision into a clean failure, not a hang).
- swx_bom.topLevelOnly defaulting to false (#E): kept — false is the documented
default and the flag is echoed back, so a caller always sees what ran.