main
John Lauer v0.1.16: rename dump_board -> read_board; read_*/write_* naming convention recorded 47c2b31 1mo ago

altium-bridge (Rust) — the bridge process, rewritten

The next major version of the bridge process (the part ab spawns on the Windows box). Decisions on record (John, 2026-08-31):

  1. The bridge calls ab's verbs for everything desktop-shaped — window enumeration, dialogs, screenshots, UIA, shell, taskbar — instead of re-implementing Win32. No windows-rs, no ctypes.
  2. The DelphiScript agent shrinks to generic primitives; the bridge becomes the brain. Only ~213 of the agent's 3,675 lines touch Altium's API; the rest (JSON assembly, parsing, tallies, filters) moves here where it is typed, testable against recorded board reads, and never needs an Altium restart to change.
  3. No standalone MCP server. External agents reach Altium through adom-bridge-cli on PATH (the Adom Anywhere lane), same as this container does.

Status — 2026-09-01: cold bootstrap proven, board-read primitive landed

26 of the Python bridge's 95 verbs (27%). The Python bridge (../src, 0.1.14) stays the shipped runtime until Rust covers the paths users actually hit.

Proven live on the box this session:

  • Cold bootstrap through the -R lane: 1.59 s, new generation installed, heartbeat up, zero dialogs. This was the last unproven path. Two bugs it flushed out: ab's direct API refuses shell_execute (412 — CLI-only, with pull_file/send_files/hd_send_files), so the bridge owns its child processes directly; and Command::arg() re-quotes an argument containing a space, which Altium cannot parse — raw_arg is required (the Python bridge hit the identical trap in August).
  • Board reads. read_board is ONE agent round trip for components + nets + layers + copper
    • rules, composed in DelphiScript from the EXISTING readers (no new Altium API surface, so it cannot introduce an engine-halting identifier). src/board.rs derives every view from it: board_summary 232 ms, rules_by_kind 152 ms, raw read 157 ms.
  • The shaping is now tested with no Altium anywhere. fixtures/read_board.json is a recorded live board read; cargo test runs 8 tests against it on any machine. They have already earned their keep: one caught a dispatch bug (an inner match on the un-stripped verb name, so altium_board_summary silently returned the raw read), and one caught a wrong assumption of mine about the Fanout rule group.
  • A real view the agent could not give cheaply: Altium emits TRuleKind as a bare ordinal. rule_kind_name() maps 31 ordinals to names, read off a live default rule set (each default rule is named after its own kind — observed, not guessed); unknown ordinals render as kind <n> rather than being invented, and a test fails loudly if a board carries an ordinal we do not know.

Original v0 vertical slice

Speaks the existing agent's file-IPC protocol unchanged (request.txt / request.payload / response_<id>.json / hb.txt), so it serves these verbs with zero template changes:

status, readiness, agent_status, eval, run_command, open_document, describe (ab's Verbs tab renders it verbatim — one entry per verb with input/output schemas, hints and pitfalls), uninstall (the cascade verb: drops the agent's stop file and clears the agent workspace, never a user design file), and the Phase 1 introspection surface — list_designators, component_data, component_pins, list_nets, net_connections, layers, copper_layers, read_rules, set_layer_visibility, footprint_primitives, schematic_data.

/health detects Altium through ab's desktop_find_window and parses the title the same way the Python detector does (edition word without version parens — issue #2 — included).

Not yet ported: launch/close, agent bootstrap/recover (X2 -R lanes), dialogs, surfaces with frames, checks/outputs, library authoring, boards, licence driving, web-control bundle install. Until parity, the Python bridge (../src) stays the shipped runtime.

The gate is OPEN — the runtime kind is "exe" (ab, on #987)

ab shipped the compiled-binary runtime kind; it is spelled spawn.kind: "exe" (there is no "binary" alias). ab spawns spawn.entrypoint directly with --port <n> appended, same env contract as node/python bridges (ADOM_BIND_HOST, ADOM_DIRECT_API_URL + bridge token, forwarded X-Adom-Caller-*), same health probing, PID tracking, log capture and reap/respawn supervision. bridge_readiness reports runtime: "none", runtimeReady: true, so no spurious needs-runtime state.

Three things to know:

  • Integrity is gated at the zip, not the exe. ab sha256-verifies the release zip against the manifest before extracting. A spawn-time re-hash was deliberately not added (the reference hash would live in bridge.json next to the exe — same tamper domain, no trust boundary).
  • Manifests are single-zip today, so a compiled bridge is per-OS. Fine here: Altium is Windows-only.
  • Set killImageName to the exe basename so stop/kill is image-verified. Ours is in bridge.json alongside this README (the shipped Python bridge keeps ../src/bridge.json until parity).

Reference implementation: scripts/sample-bridges/hello-rust in adom-inc/adom-bridge — page adom/hello-rust-bridge, v1.3.1. ~290 lines covering --port, ADOM_BIND_HOST, the /status led/summary/tooltip self-report, caller-header logging, describe, and the uninstall cascade.

Build (cross-compile from the container; the box has no MSVC linker)

# one-time: rustup + the windows-gnu target + mingw cross linker
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
rustup target add x86_64-pc-windows-gnu && sudo apt-get install -y mingw-w64
# build
cd bridge-rs && cargo build --release --target x86_64-pc-windows-gnu
# -> target/x86_64-pc-windows-gnu/release/altium-bridge.exe

Test on the box (manual, until ab spawns it)

adom-bridge --ai-thread "<t>" send_files '{"filePaths":["<repo>/bridge-rs/target/x86_64-pc-windows-gnu/release/altium-bridge.exe"]}'
# on the box (PowerShell), with the Python bridge's agent already resident:
#   $env:ADOM_DIRECT_API_URL='http://127.0.0.1:24680'; & "$HOME\Downloads\altium-bridge.exe" --port 8991
curl -s http://127.0.0.1:8991/health
curl -s -X POST http://127.0.0.1:8991/command -H 'content-type: application/json' -d '{"command":"altium_list_designators","args":{}}'

Both bridges can read the same resident agent (the inbox is serialized by the agent, one request at a time) — but only ONE session should drive it at a time; see the dev skill's one-driver rule.

Naming

Verbs say what the caller wants, in the caller's words: read_* to get data out, write_* / apply_* to put it back. The board-wide read is read_board, not dump_board — "dump" describes the implementation (a blob out of memory) rather than the intent, and it has no counterpart, so it breaks the symmetry the moment writes arrive. (John, 2026-09-01, on the first draft: "that's not a great verb name ... we should probably pick a better verb thats more like a read or write*"*.)

Layout

File Role
src/main.rs --port, ADOM_BIND_HOST, /health /status /verbs /command, envelope timing
src/ab.rs loopback client to ab's direct API (token badge, forwarded X-Adom-Caller-*, delegate)
src/agent.rs the file-drop protocol to the resident DelphiScript agent
src/health.rs Altium detection via ab + the title parser
src/verbs.rs dispatch; v0 views over existing agent commands

Next

  1. Port lifecycle (launch/close, bootstrap, recover with the busy-notice reaper via ab).
  2. Extend the read primitives: give read_board the stack-in-use flag it still lacks, plus a read_schematic companion, and re-express the remaining introspection verbs as views over them. Writes get the mirror-image naming (write_* / apply_*).
  3. Surfaces + frames (all via ab screenshots), checks/outputs, library authoring, boards.
  4. Swap: publish the zip (altium-bridge.exe + bridge.json + BRIDGE_VERSION + README) as a release asset, repoint the manifest, and retire ../src.

Two script-engine laws this design depends on

Measured on the box 2026-08-31, and the reason the agent shrinks to dumb primitives:

  1. Break inside a Try..Except does not break — the engine raises it, the Except eats it, and a 20-iteration retry loop always runs all 20 with its Sleep(10). That was the entire mysterious ~300 ms per agent file write. Flag-based loops (While (Not Done)) fix it.
  2. hb.txt must be tmp+rename; responses can be written directly — readers hold the heartbeat's final path open, so a direct SaveToFile onto it raises EFCreateError and halts the engine with a modal. A fresh response_<id>.json has no readers and writes in 0 ms.

With both applied, in-engine file writes measure 0 ms and the round-trip floor is the poll interval, not I/O — which is exactly the budget this bridge's read-primitive design assumes.