<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/idatui/worker.py, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/idatui/worker.py?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/idatui/worker.py?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-07-24T12:53:56Z</updated>
<entry>
<title>mcp: delete the ida-pro-mcp transport, supervisor, and mcp-only tests</title>
<updated>2026-07-24T12:53:56Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-24T12:53:56Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=22ffea91210610d1def042e230ffe8d223022b30'/>
<id>urn:sha1:22ffea91210610d1def042e230ffe8d223022b30</id>
<content type='text'>
The idalib worker is the only backend now, so remove the dead HTTP/supervisor
surface entirely (~2200 lines):

* deleted idatui/client.py (the IDAClient HTTP/JSON-RPC transport + session
  manager), idatui/tui.py (the old mcp TUI entry, superseded by launch.py),
  spawn.sh, and systemd/ (the supervisor unit).
* deleted the mcp-only tests (stress_client, smoke_client, test_keepalive,
  stress_paging, rpc_smoke, serverctl.sh, pane_smoke, test_domain) -- the worker
  pilot (tests/test_scenarios.py) supersedes them.
* migrated the tmux RPC harness (idatui/pane.py) to the worker: it spawns
  `idatui.launch &lt;binary&gt; --rpc &lt;sock&gt;` instead of the mcp `idatui.tui`, drops
  the supervisor auto-start/ensure machinery, and reaps our own worker
  (idatui/worker.py) instead of ida_pro_mcp.idalib_server. --db/--url/--no-
  ensure-server are gone; --open is required.
* __init__ / __main__ / domain no longer import client (exceptions come from
  errors.py, the domain client hint is WorkerClient); pyproject points both
  console scripts at idatui.launch; README + ida-tui header describe the
  worker-only flow.

What stays (by design): the ida_pro_mcp *package* (the worker reuses its @tool
functions in-process) and server/patch_server.py (the worker injects its custom
tools on startup). Verified: whole package imports + IdaTui constructs + pilot
lists 31 scenarios. The worker pilot (134 pass / 2 known flakes) is the E2E gate.
</content>
</entry>
<entry>
<title>worker: inject custom tools on startup (self-sufficient, no spawn.sh needed)</title>
<updated>2026-07-24T12:23:18Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-24T12:23:18Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=73d36d05123c6c396d78ef710b87788dc879da63'/>
<id>urn:sha1:73d36d05123c6c396d78ef710b87788dc879da63</id>
<content type='text'>
The worker reuses ida-pro-mcp's @tool functions, but our custom tools
(heads/read_raw/resolve_names/func_types/del_type/set_lvar_type) only exist
because server/patch_server.py injected them into the installed api_types.py --
which historically only ran via spawn.sh. On a fresh box (or worker-only setup
with spawn.sh gone) those tools would be missing and heads/listing would break.

_ensure_tools_injected() now runs patch_server.main() (idempotent, IDA-free) at
the top of _open_and_register, before ida_pro_mcp.ida_mcp is imported, so the
worker guarantees its own tool surface. Verified under /usr/bin/python: injection
runs and heads/read_raw/resolve_names/func_types land in api_types.py. This is
the prerequisite for deleting spawn.sh from the worker path.
</content>
</entry>
<entry>
<title>worker: wrap non-dict tool returns as {"result": ...} to match MCP shapes</title>
<updated>2026-07-23T23:49:57Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T23:49:57Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=26e99af2eca88343746eb67bfb4af26c7d252091'/>
<id>urn:sha1:26e99af2eca88343746eb67bfb4af26c7d252091</id>
<content type='text'>
The MCP server sets structuredContent = result if isinstance(result, dict) else
{"result": result} (zeromcp mcp.py:832), and domain.py parses that exact shape —
e.g. function_of() reads payload["result"] from lookup_funcs, which returns a
bare list. Our worker returned the raw list, so function_of got a non-dict and
returned None: hence "F5 — cursor is not inside a defined function" on Tab.

Replicate the rule in the worker's dispatch: dict passes through, anything else
(list/scalar) is wrapped as {"result": ...}. Fixes function_of and every other
list-returning tool (resolve_names, xref_query, list_funcs) in one shot.
</content>
</entry>
<entry>
<title>worker: surface the real startup failure (not just "code 1")</title>
<updated>2026-07-23T23:42:43Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T23:42:43Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=784df98d19b70eca5906ce0ac7fcf2bfcd0f3515'/>
<id>urn:sha1:784df98d19b70eca5906ce0ac7fcf2bfcd0f3515</id>
<content type='text'>
The worker's stderr was swallowed by the TUI, so an open failure showed only
"worker exited during startup (code 1)". Now:

* WorkerClient captures the worker's stdout+stderr to /tmp/idatui-worker-*.log
  and, on a startup exit, surfaces the last meaningful line in the error (the
  worker prints a clean 'WORKER-FATAL: ...' marker; _log_tail prefers it).
* worker.py wraps main() to print that marker + traceback before exiting 1, and
  gives an ACTIONABLE open error: "failed to open &lt;bin&gt;: the .i64 is likely held
  by a running ida-mcp worker (pkill -f idalib) or wedged (delete .id0/.id1/
  .id2/.nam/.til)". Also calls ida_auto.auto_wait() after open to fully match
  ida-mcp's session manager (open_database + auto_wait).

Root cause of the reported failure is almost certainly a leftover ida-mcp worker
still holding bash's .i64 from earlier --backend mcp runs: idalib can't open a
database another process has locked. Fix: pkill -f idalib, then retry
--backend worker; the error message now says so instead of "code 1".
</content>
</entry>
<entry>
<title>worker: idalib worker + WorkerClient (drop-in for IDAClient) — migration step 1</title>
<updated>2026-07-23T23:29:06Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-23T23:29:06Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b14f26f0bb79baf9e1476f2d67e2cddd8b6957d1'/>
<id>urn:sha1:b14f26f0bb79baf9e1476f2d67e2cddd8b6957d1</id>
<content type='text'>
First concrete step off the mcp HTTP transport. Instead of reimplementing ~25
tools, reuse ida-pro-mcp's tool *functions* verbatim and replace only the
transport + process management:

* idatui/worker.py — opens ONE database in-process on the main thread (as idalib
  requires), imports ida_pro_mcp (which registers every stock + our patched-in
  custom tool against MCP_SERVER), then serves MCP_SERVER.tools.methods[name]
  (**args) over a unix socket with length-prefixed pickle. Serial on the main
  thread (idalib is single-threaded; tools run inline through execute_sync).
  Session-management tools (idb_open/idb_save/server_health/idb_list) are shimmed
  since the worker *is* the single session.
* idatui/worker_client.py — WorkerClient exposes the exact surface the app/domain
  use on the client (call/call_envelope/connect/set_db/resolve_db/list_sessions/
  health/keepalive/close) and returns byte-identical payloads (the worker calls
  the same functions IDAClient.call ultimately hits). So domain.py and the app
  are UNCHANGED — you just construct a WorkerClient instead of an IDAClient.
  Calls are serialized under a lock over one socket; keepalive is a no-op (the
  worker is ours and never idles out).

Not wired into the app yet — the mcp path is fully intact.

Verified without idalib: pickle framing round-trips arbitrary payloads incl raw
bytes; WorkerClient has full IDAClient surface; call_envelope produces the
result.structuredContent shape domain.decompile() reads. The idalib E2E
(experiments/worker_smoke.py drives the real domain.Program read path through the
worker) is written but couldn't run here — this sandbox has degraded to reaping
any idalib spawn; the underlying unix-socket protocol already ran clean in the
inproc_spike bench (~50us/call), and the worker dispatches the same tool
functions the HTTP path does, so shapes match by construction.

Next: stand up progress reporting during analysis, then flip _connect/_reconnect
to build a WorkerClient behind a flag and run the pilot suite against it.
</content>
</entry>
</feed>
