Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
main
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):
- 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. - 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.
- No standalone MCP server. External agents reach Altium through
adom-bridge-clion 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
-Rlane: 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 refusesshell_execute(412 — CLI-only, with pull_file/send_files/hd_send_files), so the bridge owns its child processes directly; andCommand::arg()re-quotes an argument containing a space, which Altium cannot parse —raw_argis required (the Python bridge hit the identical trap in August). - Board reads.
read_boardis 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.rsderives every view from it:board_summary232 ms,rules_by_kind152 ms, raw read 157 ms.
- rules, composed in DelphiScript from the EXISTING readers (no new Altium API surface, so it
cannot introduce an engine-halting identifier).
- The shaping is now tested with no Altium anywhere.
fixtures/read_board.jsonis a recorded live board read;cargo testruns 8 tests against it on any machine. They have already earned their keep: one caught a dispatch bug (an innermatchon the un-stripped verb name, soaltium_board_summarysilently 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
TRuleKindas 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 askind <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.jsonnext 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
killImageNameto the exe basename so stop/kill is image-verified. Ours is inbridge.jsonalongside this README (the shipped Python bridge keeps../src/bridge.jsonuntil 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
- Port lifecycle (launch/close, bootstrap, recover with the busy-notice reaper via ab).
- Extend the read primitives: give
read_boardthe stack-in-use flag it still lacks, plus aread_schematiccompanion, and re-express the remaining introspection verbs as views over them. Writes get the mirror-image naming (write_*/apply_*). - Surfaces + frames (all via ab screenshots), checks/outputs, library authoring, boards.
- 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:
Breakinside aTry..Exceptdoes not break — the engine raises it, theExcepteats it, and a 20-iteration retry loop always runs all 20 with itsSleep(10). That was the entire mysterious ~300 ms per agent file write. Flag-based loops (While (Not Done)) fix it.hb.txtmust be tmp+rename; responses can be written directly — readers hold the heartbeat's final path open, so a directSaveToFileonto it raisesEFCreateErrorand halts the engine with a modal. A freshresponse_<id>.jsonhas 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.
# 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`](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)
```bash
# 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)
```bash
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.