You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: detach analyze_target's per-child recon loop for large firmware (#278)
* fix: detach analyze_target's per-child recon loop for large firmware
Found while designing directory-import: promote_file and reveal_dir got
detached earlier tonight (#274, #275), but the loop they both actually
detach — analyze_target's sequential per-child sandbox recon — was never
fixed at its OWN primary call site. `ingest_and_analyze` (hexgraph ingest /
target_ingest / the web UI's upload endpoint) calls analyze_target
synchronously, inline, with no aggregate bound — a large firmware still
blocks the initial ingest call for however long sequential per-child recon
takes, the exact same bug class, just one call site earlier in the chain.
Above CHILD_RECON_DETACH_THRESHOLD (25 — comfortably above every existing
test fixture's few-file unpacks, comfortably below what a real large
firmware produces) unpacked children, recon now runs as ONE detached
process working through them sequentially, reusing the exact
create_task/spawn_detached_task infrastructure built for promote_file and
reveal_dir tonight. Child target rows still exist immediately (unpack's own
registration is a cheap host-side copy+hash, not the bottleneck) and are
returned in the summary; only their recon facts land later. A small
firmware (every existing fixture) is completely unaffected — same
synchronous behavior, same summary shape, recon_status="done".
Threaded through every entry point that calls analyze_target/
ingest_and_analyze directly: CLI (`hexgraph ingest`), MCP (`target_ingest`),
and the web UI's upload endpoint (`api_add_target`) all now surface
`recon_status` and handle the "queued" case without breaking their existing
synchronous-case output.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: mention recon_status in target_ingest's MCP catalog description
Flagged in #278's review: docs/mcp.md, vr_skill.py, and ingest()'s own
docstring were updated to explain the new detached-recon behavior, but the
advertised MCP catalog description (the one an agent actually sees under
deferred/name-only tool loading) was missed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: src/hexgraph/agent/mcp_catalog.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -109,7 +109,7 @@
109
109
{"type": "object", "properties": {"project_id": {"type": "string"}, "include_hidden": {"type": "boolean", "description": "also list hidden firmware children (default false)"}}, "required": ["project_id"]}),
110
110
("read", "target_facts", _t.target_facts, "The cached recon SUMMARY for a target — its detected architecture and file format, the key imported/exported symbols, and the security mitigations recon already computed at ingest. The cheapest first orientation (it reads stored recon, no sandbox run). Note its `dangerous_imports` to find the risky libc calls fast. For the AUTHORITATIVE, uncapped symbol table + relocations + full mitigations, run re_binutils_facts instead.",
("run", "target_ingest", _t.ingest, "INGEST a binary/firmware from a local path as a new target (the bytes are processed in the sandbox; firmware unpacks into child targets). Use this to bring a binary/firmware artifact into HexGraph. Creates a project if none is given. Returns a BOUNDED summary — {root_target_id, children_count, a preview of the first ~20 children, format}; call target_list(project_id) for the FULL child set (firmware can unpack into hundreds).",
112
+
("run", "target_ingest", _t.ingest, "INGEST a binary/firmware from a local path as a new target (the bytes are processed in the sandbox; firmware unpacks into child targets). Use this to bring a binary/firmware artifact into HexGraph. Creates a project if none is given. Above a couple dozen unpacked children, per-child recon runs DETACHED in the background instead of blocking this call — `recon_status` is \"done\" or \"queued\" (child targets exist either way; their recon facts land later for a large firmware). Returns a BOUNDED summary — {root_target_id, children_count, a preview of the first ~20 children, format, recon_status}; call target_list(project_id) for the FULL child set (firmware can unpack into hundreds).",
113
113
{"type": "object", "properties": {"path": {"type": "string", "description": "local filesystem path to the binary/firmware"}, "name": {"type": "string", "description": "optional target name (defaults from the path)"}, "project_id": {"type": "string", "description": "optional; a NEW project is created if omitted"}}, "required": ["path"]}),
114
114
("run", "target_promote_file", _t.promote_file, "PROMOTE one file from a firmware target's unpacked filesystem into its OWN child target — the bridge from browsing the rootfs (fs_list/fs_read_file) to ANALYZING a binary in it (re_decompile_function/re_list_functions/task_run/fuzz_start). `path` is relative to the extracted root; an entry's `is_elf` flags a binary worth promoting, `added` means it's already a target. Idempotent per path. Returns as soon as the child target exists (seconds) — analysis (recon; for a CONTAINER, unpack + recon of every nested file) runs DETACHED in the sandbox when Docker is up, since a large deeply-nested firmware package can be thousands of sequential sandbox runs, minutes to hours. `analysis_status` reports queued/running/succeeded/failed; CALL AGAIN with the same target_id/path to poll — returns the same child, no duplicate work. Promoting a CONTAINER (.pkg/squashfs/cpio — e.g. a large vendor firmware package) registers its inner binaries as HIDDEN child targets once analysis succeeds; the result then reports `registered_children` (+ a note: target_list(include_hidden=true) to see them, target_set_visible to analyze one) and `packed_containers` for any still-nested containers. Returns {id, name, kind, parent_id, arch, analysis_status?, registered_children?, packed_containers?, note?}.",
115
115
{"type": "object", "properties": {"target_id": {"type": "string", "description": "the firmware target whose filesystem holds the file"}, "path": {"type": "string", "description": "file path relative to the extracted root (see fs_list)"}}, "required": ["target_id", "path"]}),
0 commit comments