Analysis pulled every export, import and symbol across the wire
and then kept the handful that name code -- 87k rows for a
kernel, each carrying a type and an address as text.
Ask for what it uses instead: the offset, name and source of each
function the module knows about, chosen where the rows already
are.
A comment is a last resort, and these were not one: section banners,
doc headers naming what the declaration below already names, and
notes saying in prose what the next line says in code.
What is left is what the code cannot show -- an ordering that matters,
a platform quirk, why the obvious way fails.
A notebook cell holds a Pharo snippet and what it produced, updated
field by field and synced back when it is re-run. Trace blobs move
to a store of their own, read through one loader.
The method mark's editor keeps its own keys and clicks, so editing
inside an opened body works like editing anywhere else.
Serve symbol pages by offset and step through them with prev/next
controls showing the current range. Reset to the first page when
the filter or category changes. Clamp the page index at the
mutation site and floor the offset in the agent so hammering
prev/next can't run past the ends into a negative range.
Bind Cmd+F (Ctrl+F on GTK) to focus the filter input.
Modules like YouTube Kids export 100k+ symbols. Enumerating and
marshalling the whole set per open stalled the UI, and both
frontends re-filtered the full arrays on the main thread on every
keystroke.
Build a cached, pre-sorted, pre-lowercased index in the agent and
serve filtered, capped pages. Add queryModuleSymbols returning a
window plus match and category counts. Wire both frontends to it
with a debounced filter; drop the client-side filtering.
Hoist the system-session drain script out of ProcessNode into a
per-device SystemDrainService owned by Engine. Multiple targets on
a device now drain their ringbuffers through a single PID 0 script
instead of one attach and script each. Retire the service when its
last node goes away.
Attach to the system session and load the drain script on the
first trace, not at every process attach. Most sessions never
use itrace and need not touch PID 0.
Index members under their enclosing namespace/interface too, so
names that are ambiguous on their own (Process.id, Thread.id)
resolve once the base expression is known.
Have the agent classify each member as callable via its property
descriptor (no getter side effects), then order properties ahead
of functions, alphabetically within each group.
Index the embedded frida-gum typings into a member-to-signature
map and attach the matching type or doc summary to each reflected
completion. Drop names that resolve ambiguously so a hint is
shown only when it is certain.
Carry a display/insert split through completion so command
suggestions show r2's help text via the native secondary title,
and complete command arguments against eval vars and flagspaces
in addition to flags. Recall the language alongside the code when
cycling history, and keep r2 output's leading whitespace intact.
Persist the REPL draft and active language per session in
SessionUIState so they survive reopening the project. Defer the
input field's first-responder change out of the view update.
Complete the command token via r2's own help tree ("<prefix>?")
and argument tokens against flag and symbol names ("fq~"). Route
completion by language and replace the whole token in r2 mode
rather than splitting on dots as the JavaScript path does.
Make the REPL prompt a clickable mode toggle between JavaScript
and r2; ":", ":r2" and ":js" switch from the keyboard. Carry the
language on REPLCell and REPLResult, route r2 input through the
session disassembler, and render output as a JSInspectValue tree
for JSON commands or ANSI-parsed styled text otherwise.
An imported symbol anchors to the linked library path (e.g.
libSystem.B.dylib) while a search anchors to the defining module
(libsystem_c.dylib), so anchor-string equality never matched and
the search offered Add instead of Go to Hook. Match by the
agent-resolved address instead, in both the macOS and GTK config
views. Relax resolve() to accept a module identified by path, not
just leaf name; without it path-form anchors fail to resolve at
all.
Drop the shared-buffer rendezvous. Each trace now owns its
buffer, created before Stalker is armed and announced to Luma
via a token-keyed start/ack handshake. The ack carries the
drain verdict: Luma remaps the buffer out-of-process while the
agent blocks, so an early crash still yields data; on
task_for_pid denial (cached per target) the agent drains and
pushes chunks itself.
Fold all itrace into the tracer instrument. Function-arm traces
and thread traces share one Trace registry; thread traces become
threadTraces config entries on a find-or-created tracer instance.
Core no longer exports itrace, and the drain agent keys its
readers by session so concurrent traces stay separate.
The itrace ring buffer may live in shared memory mapped into the drain
script's address space, so it survives the target's death. Drain a final
chunk before finalizing pending traces so panic events written near the
crash actually surface, instead of being silently dropped when the
periodic drainer is cancelled.
Console widgets get an appendImage / respond.image API on the
agent. read_widget_state surfaces image entries as attachments
scaled to at most max_dimension pixels (default 1568) on the
longest edge. Anthropic nests image blocks inside tool_result;
OpenAI gets a synthetic follow-up user message. The host scales
via CoreImage on macOS and GdkPixbuf on GTK.
Engine.symbolDisplay returns a richer SymbolDisplay (overlay,
source, raw, primary) and persists every live symbolicate
result into module_symbol (sessionID, modulePath, function-entry
offset → name). Offline rendering falls through to that table
first, then to enclosingFunction from the persisted block
ranges, then to module+offset. ProcessNode delegates trace
block annotation back through this path via
onResolveBlockNames so the same names show up on detached
traces.
Identify a process instance by a deterministic identity returned
from the agent (per-OS, with a Process.mainModule + pid fallback)
and stash it on ProcessSession.processInfo. Rekey memory_page
into memory_page_module (module UUID + offset) so cached bytes
survive ASLR, and memory_page_anon (process identity + address)
for JIT / heap regions that only survive within one process
instance. Purge stale anon pages on every fresh attach.
Add updateProcessInfo, updateModuleAnalysis and updateInsight
SessionOps plus a side-channel publish-memory-page op and a
+fetch-memory-page request. The driver write-throughs every
page it fetches; non-drivers pull on miss and persist locally.
Module-analysis rows flow the same way so non-drivers can key
their cache by module UUID. The lab-join snapshot now carries
processInfo and module_analyses too.
Refresh actions on the insight detail view: Reread Bytes drops
the pages overlapping the visible range; Reanalyze Module drops
the module's analysis and pages, re-running r2 on next access.
The macOS detail view now reads the insight live from
insightsBySession via observation; GTK forwards .insightUpdated
into the detail and reschedules a refresh when the resolved
address changes.
Persist per-session module analysis (executable ranges + known
functions, keyed by module name + LC_UUID) so on-demand
analysis runs once per binary and rebases on reattach. The
agent gains enumerateModuleRanges and getModuleIdentity; the
store gains a module_analysis table.
Disassembler queries $FB/$FE for the request address; when it
hits a function start, fetch via pDJ bounded to the function's
byte range. Otherwise fall back to pdJ N. Filter pdJ
decoration entries (xrefs, fcn headers, var summaries) that
share the addr of an instruction.
DisassemblyPage carries a scope (.function/.span) so the
SwiftUI and GTK insight views can disable loadMore when the
page is function-bounded. The disassemble AI tool surfaces the
scope too, so the model knows whether it has the whole
function or just an N-instruction span.
Go-to-Function-Start now opens (or selects) the insight rooted
at the function start, which then renders via the bounded
path.
Hook packs and custom instruments can declare a console widget for
REPL-style input. Adds a WidgetConsoleEntry update kind, per-session
persistence, an agent-side appendConsole/onConsoleInput API, and a
ConsoleView-backed renderer card.
Regroups all widget kinds across the codebase as
counter, histogram, graph, list, table, hex, console
(aggregates, collections, raw, interactive).
evalInREPL returns the produced REPLResult; the tool awaits
it and returns {cell_id, code, kind, value|text} inline.
REPL results never reached the event log, so the prior
fire-and-forget shape forced a wait_for_event that always
timed out.
Add an .engine RuntimeEvent source so host-tier issues
(devices, packages, attach, spawn-gating, instruments,
hookpacks, repl, itrace, editor) flow into the existing
event stream instead of stdout. HookPackLibrary takes a
callback for decode errors, and ITraceDecoder returns
collected panic strings so ProcessNode can yield them as
engine events.
compileTracerConfig collects per-hook outcomes and throws
TracerHookCompileFailures with a map of hook ID to error.
Engine catches that on the load and config-apply paths and
distributes the failures via replaceComponentStatuses on
the node's InstrumentRef, leaving the instrument-level
status clear so the icon shows on the broken hook row.
Promote the runtime per-instrument flag from a bare reason
string to an InstrumentStatus enum so later kinds (load,
reload, compile failures) can share the surface. Behaviour
is unchanged: skipIfIncompatible sets .incompatible and the
sidebar still renders a single warning icon.
summarize_recent_events and wait_for_event embed each
event's payload as JSON with $type sentinels for
NativePointer, BigInt, Date, typed arrays, Map, Set, and
friends. Tracer events decode into named fields with
caller and backtrace pre-symbolicated.
Add read_event(event_id) for the full uncapped payload.
SymbolicateResult collapses to one struct with optional
offset and source. Agent symbolicate returns objects with
a numeric offset field.
- Store source files as named CustomInstrumentFile rows per def
- Render sidebar with disclosure groups
- Drive editor via Monaco project models with activePath
- Validate Add/Rename/Delete dialogs and submit on Enter
- Fork installed hook packs into editable custom instruments
- Reload instances on def change; clear stale compat reasons live
- Focus editor on selection (native + Monaco)
Each instrument carries an InstrumentCompatibility (platforms,
osIDs, archs; nil set means any). Hook pack manifests and custom
defs declare it; tracer and codeshare default to universal.
System parameters are queried lazily via SystemParametersCache,
keyed by device id and invalidated when devices disappear.
Restoring an incompatible instrument now marks it with a reason
on its InstrumentRef instead of attaching.
Add write_memory, list_threads, list_session_instruments,
arm_session, disarm_session, list_address_insights, unpin_insight,
detach_session, read_widget_state, and invoke_widget_action.
read_memory grows an explicit format hint (hex/utf8/cstring);
eval_repl's description clarifies that the intent string lands
in the audit log.
Failure paths now emit a machine-readable code alongside the
human message (invalid_input, not_found, unavailable, rejected,
failed) so LLM-driven missions can react without parsing prose.
Table mirrors list with multi-column rows + per-row actions and
configurable column alignment. Counter is a single big value
with optional unit and signed delta. Histogram supports both
setHistogram and incrementBucket and renders as a bar chart.
Hex shows a live binary buffer with a base address, capped to a
trailing window.
Agent SDK gains upsertRow/removeRow/setCounter/setHistogram/
incrementBucket/setHex on ctx.widget(id). Per-def TS narrows
table column ids and action ids the same way list actions are
narrowed; restored snapshots get matching shapes.
Engine owns the widget state cache, applies inbound updates from
the agent, persists session-scoped widgets, and broadcasts to
collaborating peers (action invocations route to the host).
Renderers in both UIs read the cache and the unified update
stream, with a Clear button that propagates to peers.
Agent SDK adds ctx.widget(id) with push/upsertItem/removeItem/
clear, plus a typed onAction handler. create() now takes a
restored snapshot seeded from persisted state and the join
snapshot.
Per-def .d.ts narrows the widget map, restored shape, and action
union to the exact ids declared in the editor.
Drop the dual ITraceCaptureRecord/CapturedITrace types in favour of
a single ITrace covering both function-call and thread origins.
Promote itrace-capture from an instrument into a core service so
both origins share the same start/stop/drain machinery.
ProcessNode now keys pending traces by an opaque session id, hands
finalized data to a TraceStore, throttles running emits, drains
in-process when no system-drain is available, and skips the
Interceptor-trampoline trim for thread origin. ModuleDelta carries
incremental module updates.
LumaDocument grows sqliteURL and tracesDirectory accessors so the
project file can be a directory holding both the index and
per-trace blobs.
Sessions in a lab are now collaborative artifacts. Any owner can
host (own the local Frida attachment) and any owner can drive
(issue REPL evals and instrument commands); a driver's actions
are relayed to the current host. Members watch read-only and see
an "Only lab owners can host sessions" CTA when they try the new
session button.
Re-host (Run on My Device…) lets an owner take over a running
session by attaching to their own equivalent process; the prior
host's Frida attachment detaches and the new host auto-loads the
session's instruments. Driver auto-reverts to host when the
current driver fully disconnects from the lab.
Add a javaMethod anchor kind and the matching java-method scope
so the tracer can target JVM methods. Resolution uses
Java.enumerateMethods through the bridge; attachment replaces
each method's implementation and restores it on detach.
The bridge isn't bundled into our agent, since only one copy
can live in a process at a time. Instead it's marked external
so frida-compile leaves the imports as runtime lookups, and our
agent does `await import("frida-java-bridge")` on demand. If
the user hasn't installed the package via the Packages panel,
we surface a message pointing them there.
To let frida-compile honour the external, LumaBundleCompiler
grew a --external <name> flag that forwards to
BuildOptions.externals. Both the Xcode Run Script phase and the
GTK Makefile now pass --package [email protected] (for
TypeScript types) alongside --external frida-java-bridge (so
the runtime code isn't embedded).
Java resolves to no native address, so ProcessNode.resolve and
the instruction annotation map throw / skip rather than trying
to materialise one. The seed code for new Java hooks matches
frida-tools' pattern: log args via JSON.stringify.
Extend the resolver path so traced targets can be described by more
anchor kinds than just module exports. The agent now exposes a
unified resolveTargets RPC on top of Frida's ApiResolver (module,
objc, swift) plus DebugSymbol.findFunctionsMatching, and a
lookupAnchorAddress entry point that resolves a stored anchor back
to an address.
AddressAnchor gains objcMethod, swiftFunc, and debugSymbol cases
with matching Codable round-tripping. ProcessNode.resolve routes
the new cases through the agent; the sync fast-path only handles
anchors that don't require the target runtime.
Both Add-Hook UIs keep using scope:"function" for now so behaviour
is unchanged; Stage 2 will expose a scope picker. The search-result
rows now carry a pre-resolved AddressAnchor instead of reconstructing
module+export pairs, and subtitle switches are exhaustive over the
new cases.
Fresh Compiler() per compile triggers frida_compiler_backend_init's
g_module_close, which can dlclose a module whose threads are still
executing — observed crashing on tracer instrument add / re-attach.
Cache one instance in CompilerWorkspace so the backend init runs
once and the runtime threads survive.
Also silence "result of 'try?' is unused" warnings on the
disposeInstrument calls by explicitly discarding the optional.
Engine and ProcessNode are @Observable. Workspace
observes engine.processNodes via withObservationTracking
to sync VMs reactively. Views call workspace.engine.*
directly for spawn, attach, resume, instrument lifecycle,
insights, and tracer hooks. Engine instrument methods
take InstrumentInstance directly instead of split params.
Engine now handles the full instrument lifecycle (add,
remove, enable, config, load via RPC, dispose), tracer
instruction hook creation, session reestablishment,
and address-to-hook resolution. InstrumentDescriptor
is the UI-agnostic instrument metadata type.
Workspace is reduced to a thin SwiftUI shell: template
view factories, address decoration UI, ProcessNode VM
wrapping, and @Published state for observation.
ProcessNode is now in LumaCore, exposing AsyncStream for
events, REPL results, ITrace captures, module snapshots,
and session detach notifications. No delegates, no
@Published — consumers subscribe with for-await.
Luma retains ProcessNodeViewModel as a thin ObservableObject
wrapper that subscribes to the streams and handles SwiftData
persistence (inserting REPLCells, ITraceCaptures) and R2
integration.
RuntimeEvent is now a self-contained LumaCore value type
with source and payload enums instead of object references.
The Luma-side DisplayEvent bridges to views by carrying a
ProcessNodeViewModel reference alongside the core event.
WIP: views not yet updated for the rename — the Xcode build
needs mechanical updates across ~14 files to use
ProcessNodeViewModel and DisplayEvent.
Move pure data types out of Luma/ into Sources/LumaCore/
so they can be shared with the GTK frontend:
- AddressAnchor, CFGGraph, ITraceDecoder (+ all related
types: TraceEntry, ITraceMetadata, RegisterState, etc.)
- JSInspectValue, SpawnConfig, ProcessModule
- HookPackConfig, HookPackManifest, InstrumentConfigCodec
- InstrumentIcon, InstrumentKind, ConsoleLevel
CFGGraph uses its own Point2D instead of CoreGraphics
types, keeping the core free of Apple frameworks.
Remove premature stubs (Engine, ProcessNode,
InstrumentRuntime, EventStream) that will be properly
built in a later phase once the real implementations
are ready to move.
Lay the groundwork for a GTK/Adwaita frontend by extracting
shared infrastructure into a LumaCore Swift package. Agent
sources and LumaBundleCompiler move to top-level locations
so both the Xcode project and SPM can reference them.
New structure:
- Package.swift: root manifest with LumaCore lib + compiler
- Sources/LumaCore/: engine, persistence, models, and
platform-abstracted token store
- Sources/LumaBundleCompiler/: moved from LumaBundleCompiler/
- Agent/: moved from Luma/Agent/
- LumaGtk/: skeleton GTK app package