mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
@jpiasecki/sdk
414 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ec8321206c | Sdk | ||
|
|
232762ebd0 |
fix(tool-server): make debugger-component-tree work on New Arch / multi-renderer apps (#316)
## Summary
`debugger-component-tree` returns **"No visible components found on
screen"** on React Native **New Architecture (Fabric / bridgeless)**
apps that also use a secondary React reconciler such as
`react-native-skia` — even when the app is fully rendered and CDP is
connected. This PR fixes the two root causes so the tool returns the
real component tree with names, testIDs, and accurate tap coordinates.
## Root cause
The injected fiber-walk script
(`packages/tool-server/src/utils/debugger/scripts/component-tree.ts`)
made two assumptions that don't hold on modern RN:
1. **Hardcoded renderer id.** `hook.getFiberRoots(1)` assumes the React
Native renderer is DevTools renderer id `1`. Libraries that ship their
own React reconciler (`react-native-skia`, `react-native-svg`,
`react-three-fiber`, …) register a renderer too and frequently take id
`1`. On such apps `getFiberRoots(1)` returns only that library's roots
(e.g. Skia shapes), so the walk never reaches the app UI → `No visible
components`.
2. **`nativeFabricUIManager.measure` under bridgeless.** Layout was read
via `nativeFabricUIManager.measure(stateNode.node, …)`. Under
bridgeless, `globalThis.nativeFabricUIManager` is an **empty object**
(no `.measure`), so every measure threw and all rects became `null` —
components were then filtered out (and the few kept by testID reported
no usable rect).
## Fix
- **Renderer-agnostic root selection:** enumerate `hook.renderers`,
collect roots from every renderer, and walk the mounted root with the
largest fiber subtree (the real app UI) instead of hardcoding id 1.
- **Fabric layout via the public host instance:** measure through
`stateNode.canonical.publicInstance.measureInWindow()` (a
`ReactNativeElement`) — the **same approach this repo's
`inspect-at-point.ts` already uses** — falling back to
`nativeFabricUIManager.measure(shadowNode, …)` when that global is
functional (non-bridgeless).
- **Hang guard:** a per-measure 1200 ms timeout so `Promise.all` can't
stall on a measure callback that never fires (detached / off-screen
nodes).
No change to the output schema or the Paper (old-architecture) path.
## How to reproduce
On a Fabric + bridgeless RN app that also renders `react-native-skia`:
```
argent run debugger-connect --device_id <udid> --port <metro>
argent run debugger-component-tree --device_id <udid> --port <metro>
# before: "No visible components found on screen."
```
The DevTools hook reports multiple renderers — e.g. `getFiberRoots(1)`
is the Skia reconciler (only canvas nodes), while the real app UI lives
in a different react-native renderer that the hardcoded id never
reaches.
## Testing
- Full `npm run build` (`tsc --build`, project references) passes; the
modified file typechecks under `strict`.
- Verified live against a **React 19.2.0 / Fabric (bridgeless)** app
whose DevTools hook exposes 3 renderers (id 1 = `react-native-skia`
`0.0.1`; id 3 = `react-native` `19.2.0` holding the real ~2,900-fiber UI
tree).
Before (unpatched): `No visible components found on screen.` — even with
`--includeSkipped` and after forcing a fresh commit.
After (this change) — names + testIDs + accurate coords (example
genericized):
```
Screen: 440x956
OnboardingQuestion "<question text>" (tap: 0.50,0.17)
SelectableCard [testID=onboarding.option.yes] (tap: 0.50,0.84)
Text "Yes" (tap: 0.50,0.84)
SelectableCard [testID=onboarding.option.no] (tap: 0.50,0.93)
Pressable [testID=screen.back-button] (tap: 0.09,0.09) ← top-left ✓
TouchableOpacity [testID=dev-settings.trigger] (tap: 0.06,0.97) ← bottom-left ✓
```
Coordinates were spot-checked against the visible layout (back button
top-left, an off-screen/hidden trigger bottom-left).
## Notes
`inspect-at-point.ts` has the same hardcoded `getFiberRoots(1)` /
`renderers[0]` renderer assumption (its Fabric layout path already uses
`publicInstance`, which is what motivated this fix). I scoped this PR to
`component-tree`; happy to follow up on the inspect path separately.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Filip131311 <contact@filipkaminski.com>
Co-authored-by: filip131311 <159789821+filip131311@users.noreply.github.com>
|
||
|
|
9267940391 | style: prettier formatting in inspect-at-point-script test | ||
|
|
d2071fafc5 |
fix(tool-server): select the correct renderer in debugger-inspect-element (#317)
## Summary Companion to #316. `debugger-inspect-element` has the same hardcoded-renderer assumption as the component-tree script, so it inspects the wrong React renderer on multi-renderer apps. ## Root cause `makeInspectScript` (`packages/tool-server/src/utils/debugger/scripts/inspect-at-point.ts`) picks: ```js var renderer = Array.from(hook.renderers.values())[0]; var roots = hook.getFiberRoots(1); var root = Array.from(roots)[0]; ``` On React Native **New Architecture** apps that also use a secondary React reconciler (`react-native-skia`, `react-native-svg`, `react-three-fiber`, …), that library registers its own renderer and frequently takes id `1`. `renderer` then points at the Skia/SVG reconciler and `getFiberRoots(1)` returns only its roots, so `renderer.rendererConfig.getInspectorDataForViewAtPoint(...)` runs against the wrong tree and returns no app component at the touch point. ## Fix Enumerate `hook.renderers`, walk each renderer's roots, and select the **(renderer, root)** pair with the largest fiber subtree (the real app UI) before inspecting — then call `getInspectorDataForViewAtPoint` on that renderer. Falls back to the previous first-renderer behavior if enumeration yields nothing. Unlike the component-tree script, the Fabric layout path here was already correct (it uses `hostFiber.stateNode.canonical.publicInstance`), so no measurement change is needed — this PR is purely the renderer/root selection. ## Testing - Full `npm run build` (`tsc --build`) passes; the modified file typechecks under `strict`. - Same environment as #316: a React 19.2 / Fabric (bridgeless) app whose DevTools hook exposes a `react-native-skia` renderer at id 1 alongside the real `react-native` UI renderer. With the fix, inspection resolves against the app renderer instead of the Skia one. No change to the output schema or the Paper path. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
20cb452fd4 |
feat(remote): artifact transport for files produced by a remote tool-server (#278)
## Why
Files produced on the **tool-server host** (screenshots, profiler
exports) were surfaced to the agent as host paths / `127.0.0.1` media
URLs, which the MCP client (`argent-mcp`) then fetched a *second* time
to get the bytes. When the tool-server runs **remotely**, that localhost
URL resolves to the client's own machine — nothing there — so
screenshots fail with `(Screenshot unavailable: no valid PNG …)`.
Profiler exports have the same shape: paths that only exist on the
remote host.
## What
A reusable **artifact boundary layer** so any file-producing tool works
regardless of where the tool-server runs:
**Foundation**
- **tool-server** (`src/artifacts.ts`, `http.ts`): in-process artifact
registry + `GET /artifacts/:id` streaming route.
`registerArtifact(hostPath)` returns a wire handle (`{ __argentArtifact,
id, filename, mimeType, size }`) instead of a raw path.
- **argent-mcp** (`src/artifacts.ts`, `content.ts`, `mcp-server.ts`): a
client materializer deep-walks tool results for handles, downloads each
over the remote-aware `TOOLS_URL`, writes it into a **structured
cache**, and rewrites the result to the **local** path (+ inline image
rendering).
**Cache layout** (override root with `ARGENT_ARTIFACTS_DIR`):
```
~/.argent/artifacts/<project>/<session>/<device>/<filename>
```
- `project` = `basename(cwd)` + short hash of the full path (readable +
collision-safe)
- `session` = minted once per argent-mcp process (groups a run, easy GC)
- `device` = udid/serial when device-scoped, omitted otherwise
**Consumers wired**
- **screenshot** → returns an image artifact; rendered inline from
downloaded bytes with **no second `127.0.0.1` fetch**. Fixes
`auto-screenshot` and `flow-execute` screenshots via the same path.
- **native-profiler-stop** → `exportedFiles` (XML) become downloadable
artifacts materialized client-side.
## Deferred (follow-ups)
- **`.trace` bundle** — it's an Instruments bundle (a directory);
streaming needs archiving (no archiver dep yet). `native-profiler-stop`
now returns a `traceFileNote` explaining it stays on the profiling host.
- **Project source on remote** — `react-profiler-component-source` /
`react-profiler-analyze` read the user's source tree on the tool-server
host. The profiler analysis chains already work remotely
(server-internal); tracked separately.
## Compatibility
Legacy `{ url, path }` screenshot rendering is retained as a fallback
when no artifact context is present (older tool-servers / unit tests).
## Tests
- tool-server: artifact registry (infer/override), `GET /artifacts/:id`
(200/404/410), updated `stop-recovery` assertions.
- argent-mcp: materializer
(download/rewrite/nested/failure/device-scoping), artifact-aware
`toMcpContent`.
- argent-mcp typecheck + full suite green; tool-server artifact/profiler
tests green. (Note: a full local tool-server typecheck trips a
pre-existing zod-v4-vs-v3 mismatch in `run-sequence` that's unrelated to
this change and resolves on this branch's own lockfile.)
## Notes
Routes around the `argent-private` native simulator-server binary (which
emits the `127.0.0.1` media URL) rather than changing it — no submodule
change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Paweł Fornagiel <85541429+pFornagiel@users.noreply.github.com>
|
||
|
|
07335efa87 |
docs: add dynamic banner serving to readme (#318)
Adds dynamic banner serving to the readme, server-side content swaps update banner without repo changes. Banners are currently hidden. |
||
|
|
869f341735 |
fix(tool-server): reject platform-absent hardware buttons in the button tool (#283)
## Problem
The `button` tool's description states that buttons not present on the
target platform "are rejected with a clear error", but the
implementation accepts any button in the enum on any device. On iOS,
`button: "back"` (there is no hardware back key) returns `{ pressed:
"back" }` — a **silent no-op reported as success**, so an automated
caller believes a back-navigation happened when nothing did. Reproduced
live on an iOS 18.5 simulator.
## Root cause
`execute` (`tools/button/index.ts`) calls `sendCommand` and returns `{
pressed }` unconditionally, with no per-platform validation. The
"rejected … from the backend" promise is also architecturally
impossible: `sendCommand` (`utils/simulator-client.ts`) is
fire-and-forget — it pushes a WebSocket frame and never reads a reply,
so a backend rejection cannot surface to the caller.
## Fix
Validate the requested button against a per-platform set in the tool
layer (the platform is known synchronously from the UDID via
`resolveDevice`), throwing the existing `UnsupportedOperationError` (→
HTTP 400) for buttons that don't exist on the device:
- **iOS:** `home`, `power`, `volumeUp`, `volumeDown`, `appSwitch`,
`actionButton` (no `back`)
- **Android:** `home`, `back`, `power`, `volumeUp`, `volumeDown`,
`appSwitch` (no `actionButton`)
Cross-platform correctness is preserved — `back` still works on Android.
The zod enum stays the union of both platforms' buttons (a flat enum
can't express the dependency); the per-platform `Set` enforces the
refinement. The description is reworded to match (validation lives in
the tool, not "the backend").
## Testing
Adds `test/button.test.ts` — the tool previously had no test.
```
vitest run test/button.test.ts → 4 passed
```
Verified fail-before/pass-after: with `src/tools/button/index.ts`
reverted to `main`, the two rejection tests fail (old code resolves `{
pressed: "back" }` / `{ pressed: "actionButton" }`); with the fix, all
four pass.
---------
Co-authored-by: Filip131311 <contact@filipkaminski.com>
|
||
|
|
3ccd89f333 |
feat: add native android profiling (#275)
# Android native profiling via in-process Perfetto WASM Connected with: https://github.com/software-mansion/argent-private/pull/16 ## TL;DR Adds **Android** to the native profiler (previously iOS-only) and does it with a brand-new engine: instead of shipping Perfetto's `trace_processor_shell` native binary and forking it per query, we now run **Perfetto's trace-processor compiled to WebAssembly, in-process** inside the tool-server. - **One ~13 MB `trace_processor.wasm`** runs on every OS/arch — no per-platform binary, no subprocess, fully offline. - The native profiler is refactored from an iOS-only implementation into a **platform-dispatch facade** (`platforms/ios.ts` + `platforms/android.ts`) over a new **`profiler-shared/`** layer, so iOS and Android share the aggregator, types, lifecycle, and time-alignment code. - New **release/pack pipeline** fetches + sha256-verifies the WASM bundle from `argent-private-releases`, and bundles it (plus SQL queries and the Perfetto TraceConfig) into the published `@swmansion/argent` package. End-to-end on a 76 MB trace with 1013 jank rows: **6.3 s** total analyze (the older per-hang-subprocess approach was projected at ~47 minutes and never finished within the tool-call deadline). --- ## TESTING MANUALLY To test manually run the following packing command checked-out on this branch: ``` npm run pack:mcp -- argent-@pFornagiel-native-android-devtools-perfetto ``` This will bundle the package with the new binaries. After this PR is fixed and the binaries are resolved from the main release, the `argent-@pFornagiel-native-android-devtools-perfetto` release can be deleted. ## Why Android needed native CPU/hang/memory profiling to reach parity with iOS. The natural engine is Perfetto's trace-processor, but the obvious ways to ship it are both bad: - **`trace_processor_shell` binary** — per-platform (a mac-arm64 pack shipped a mac-arm64-only tarball), pinned an OS/arch matrix, and cost a fresh ~1.3 s subprocess fork **per query**. With one subprocess forked per detected hang, a large trace projected to ~47 minutes. - **`get.perfetto.dev/trace_processor` Python launcher** — adds a runtime Python 3 dependency, resolves "latest" at first run (defeats version pinning), and makes the first analyze slow via a network fetch. The WASM engine is one cross-platform artifact, in-process and warm-cached: the trace is parsed **once** and reused across the whole analyze run plus every drill-down query. --- ## The three big pieces ### 1. In-process Perfetto WASM engine New package surface in `@argent/native-devtools-android`: | File | Role | | --- | --- | | `src/wasm-trace-processor.ts` (+476) | The whole engine: boot, glue patching, warm-cache, fault containment, chunked parse, cell decoding | | `src/perfetto-engine.d.ts` (+49) | Hand-written types for the version-agnostic RPC decoder (imported by path, so tsc can't see its own `.d.ts`) | | `src/errors.ts` (+68) | `TraceProcessorUnavailableError` (`wasm_load_failed` / `wasm_path_invalid`) | | `src/bundled-meta.ts` (+12) | `PERFETTO_VERSION` stamp, regenerated at pack time | | `src/index.ts` | Re-exports + package-root accessors (`traceProcessorQueriesDir`, `traceConfigPath`) | Public surface: `resolveTraceProcessorAssets()`, `queryWarm<Row>(tracePath, sql)`, `ensureTraceProcessorReady(tracePath)`, `disposeWarmEngine(tracePath)`. Highlights of how it works: - **Boots Google's prebuilt web/worker wasm under Node.** Installs a minimal worker scope (`self`, `WorkerGlobalScope`, `location`), reads the wasm bytes, patches the Emscripten glue (`useMemory64=false`, hand the bytes in directly, expose the bridge), and drives it over a `MessageChannel` + `EngineBase` subclass. Glue patches are applied **by unique string anchors that throw if they don't match** — the canary for glue-format drift on a Perfetto bump. - **Warm-engine cache, one per trace path.** LRU with `MAX_WARM_ENGINES = 3` (bounds resident memory at ~26–76 MB trace + wasm heap each), 5-min idle dispose (timer `unref()`'d), no failed-promise caching. - **Fault containment (`fatal` promise).** `EngineBase.fail()` throws synchronously inside the `MessagePort.onmessage` handler (e.g. an RPC framing error); left unhandled it becomes a Node `uncaughtException` and would **kill the entire tool-server**, wiping every device + profiler session. Each engine exposes a `fatal: Promise<never>`; the handler try/catches, rejects `fatal`, evicts the engine, and every load/query is `Promise.race`'d against it — so a contained fault is a clean rejection and the server stays up. - **Chunked trace parse.** Perfetto's RPC ring buffer rejects any frame > 64 MiB, so the trace is fed in 32 MiB chunks before `notifyEof()`. ### 2. Android native profiler + the shared/dispatch refactor **Capture** (`utils/android-profiler/capture.ts`, `detect-app.ts`): spawns `adb shell perfetto --txt -c - --background-wait -o /data/.../argent-<ts>.pftrace`, piping the TraceConfig on **stdin** (SELinux denies `shell:s0` writes under `/data/misc/perfetto-traces/`, so the config can't be staged as a file), waits for the PID on stdout, then on stop sends `kill -TERM`, polls `/proc/<pid>`, and `adb pull`s the trace. **Pipeline** (`utils/android-profiler/pipeline/`): PerfettoSQL does the parsing and aggregation, so the Android pipeline is intentionally **two files, not iOS's four**. The per-hang annotation pass is **batched** — `runBatchedHangFolds` inlines all hang windows as a `VALUES` table into one SQL script (two derived views unioned in a single terminal SELECT, because the engine returns only the final statement's rows) and runs it once against the warm engine. **The platform-dispatch refactor** — `native-profiler-start/stop/analyze` shrank from monolithic iOS files (~160–330 lines each) into **thin routers** (~60–110 lines) that branch on `api.platform`: ``` native-profiler-start ─┬─ ios? → platforms/ios.ts (xctrace) └─ android → platforms/android.ts (adb perfetto) ``` A new **`utils/profiler-shared/`** layer holds everything both backends use: | File | What was hoisted | | --- | --- | | `types.ts` | Unified `Bottleneck = CpuHotspot \| UiHang \| MemoryLeak \| MemoryRssGrowth`, each now carrying a `platform` field; shared `NativeProfilerAnalyzeResult`; `RECORDING_CAP_MS` | | `aggregate.ts` | Generic `aggregateCpuHotspots(AggregatorInputRow[])` — burst windowing (`BURST_GAP_MS=500`), severity banding (RED >15%, YELLOW 3–15%, <3% dropped). iOS feeds it via a pre-pass; Android feeds it precomputed SQL rows | | `lifecycle.ts` | `shutdownChild` SIGINT→SIGTERM→SIGKILL ladder + `waitForChildExit` (no PID-reuse hazard) | | `thread.ts` | `normalizeThreadName` for both xctrace and Perfetto thread names | | `time-align.ts` | New `buildPerfettoAnchor()` + `windowsOverlap()` alongside the existing iOS/React anchors | | `format.ts` | `formatBytes` (moved out of the combined report) | iOS was refactored to fit this shape: `ios-profiler/lifecycle.ts` and `types.ts` become thin re-exports; `pipeline/02-aggregate.ts` shrank from ~200 to ~60 lines (keeps its iOS-only dominant-function pre-pass, delegates grouping/bursting/ severity to the shared aggregator). **No iOS behavior change** — same outputs, shared internals. **Session blueprint** (`native-profiler-session.ts`): generic `capturePid` / `captureProcess` (was `xctracePid` / `xctraceProcess`), new `platform` and `androidOnDeviceTracePath` fields, and **platform-branched dispose** (iOS SIGKILLs xctrace; Android `kill -KILL`s perfetto, `rm`s the device trace, and disposes the warm WASM engine). **Drill-down tools**: `profiler-load.ts` learns to restore Android `.pftrace` sessions (with an optional metadata sidecar / `app_process` override); `profiler-stack-query.ts` splits into iOS and Android branches. Unlike iOS (which caches `parsedData` in memory), Android **re-queries the `.pftrace`** per drill-down — the warm engine is the cache, so each query costs only SQL execution time. ### 3. Packaging & release pipeline The WASM bundle's three third-party artifacts (`trace_processor.wasm`, `engine_bundle.node.js`, `engine.mjs`) + `LICENSE` are **never committed** (`.gitignore` lists the generated files but not the dir). They're built and sha256-checksummed in `argent-private` CI, published to `argent-private-releases`, and on our side: - **`scripts/download-trace-processor.sh`** (+88, new) — `gh release download` → verify tarball sha256 (**FATAL on mismatch** — the blobs are unsigned) → extract → re-verify via `SHA256SUMS` → assert all four present. - **`scripts/pack-mcp.cjs`** (+60, new) — single pack entry point: downloads simulator-server, native binaries, **and** the trace-processor bundle, then builds and `npm pack`s `@swmansion/argent`. `package.json`'s `pack` / `pack:mcp` now call this script. - **`packages/argent/scripts/bundle-tools.cjs`** (heavily reworked, +574/-...) — copies `assets/trace-processor/`, `assets/queries/`, and `argent.tracecfg.pbtxt` into `packages/argent/assets/`, and regenerates `bundled-meta.ts` to stamp the Perfetto version. - **`publish.yml` / `publish-next.yml`** — run the download step and then **verify**: all four WASM files present, the wasm magic-byte check (`head -c4 … | grep $'\x00asm'`), plus the queries dir, TraceConfig, manifest, and Android helper APK. Asset reorg: `native-devtools-android` now ships `dist/`, `bin/`, and `assets/` (SQL queries, TraceConfig, manifest moved under `assets/`); `.gitignore` swaps the old per-file `packages/argent/manifest.json` entry for the whole `packages/argent/assets/` dir. --- ## Testing 15 new test files under `packages/tool-server/test/android-perfetto/` cover the engine and pipeline, including: - `dispatch.test.ts` — start/stop/analyze route to the right platform backend. - `aggregate-shared.test.ts` — the shared aggregator on both iOS and Android-shaped input. - `hang-folds-batched.test.ts` / `hang-fold.test.ts` — the batched per-hang SQL fold. - `hang-severity.test.ts` — jank-reason → RED/YELLOW classification. - `dispose.test.ts` / `dispose-warm-engine.test.ts` — session dispose kills the daemon, rms the device trace, and tears down the warm engine. - `stop-recovery.test.ts` — `profilingActive` is forced false even if `adb pull` fails (so a retry-start doesn't wedge). - `run-tp.test.ts` — `renderSqlTemplate` token rendering + the "unused substitution" guard. - `manifest-hint.test.ts` — zero-callstack detection emits the manifest hint. - `profiler-load.test.ts` — `.pftrace` session restore with/without the metadata sidecar. Plus `native-profiler-analyze-failure.test.ts` for the engine-unavailable banner, and updated iOS tests for the shared-lifecycle refactor. `@argent/native-devtools-android` gains its own vitest setup (`vitest.config.ts`, `tsconfig.test.json`, `test/errors.test.ts`). --- ## Notes & follow-ups - **Manifest requirement.** Perfetto's `linux.perf` data source needs `/proc/<pid>/mem` access, granted only to a **debuggable** or `<profileable android:shell="true"/>` app. A release build without either silently produces zero callstacks; the pipeline detects this and emits a **manifest hint** instead of an "All clear" report. - **Version pinning lives in the `argent-private` submodule** (`PERFETTO_VERSION` / `PERFETTO_WASM_TAG` / `LYNX_TRACE_PROCESSOR_VERSION`). Bumping is documented in `argent-private/docs/ANDROID_PERFETTO.md`. JS↔engine version skew is structurally impossible (version-agnostic decoder + vendored wasm). - **`disposeWarmEngine` is exported but not wired into session-stop** — warm engines are reclaimed only by the idle timer + LRU cap. Acceptable today (bounded memory); wire it into session dispose if explicit teardown is ever needed. - **Submodule bump.** This PR moves `packages/argent-private` to `22e55f90` — see the companion `argent-private` PR description, including its merge-hygiene note (that branch should pick up latest `argent-private` main before the pointer is finalized). ### Companion docs added in this branch - `utils/android-profiler/ANDROID_PROFILER_REFERENCE.md` — stack, capture, queries, manifest requirement. - `utils/android-profiler/PIPELINE_DESIGN.md` — the "why" (two-file pipeline, shared-aggregator hoist, re-query-don't-cache, batched fold). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8c080c9189 |
fix(screenshot-diff): make live capture work on Android + surface real capture errors (#307)
## Problem
`screenshot-diff` with `baselinePath` + `captureCurrent: true` — the
tool's own
documented *"common visual-regression flow"* — fails reproducibly on
Android with:
> Screenshot failed: server response missing url or path. The
simulator-server may be misconfigured. Try restarting it.
That message is misleading (the server is healthy), and the flow is 100%
broken on Android.
## Root cause (verified against a live emulator)
`captureLiveInput` requests a **full-resolution** capture (`scale:
1.0`). The Android
simulator-server can't stream some framebuffers at full res and returns
**HTTP 200**
with an `error` field. Confirmed directly against the running
simulator-server:
| request | response |
|---|---|
| `{"scale":0.5}` | `{"path":…,"url":…}` ✅ |
| `{}` (full res) | `{"error":"wrong data size, expected 7853760 got
17627328"}` ❌ |
| `{"scale":1.0}` | `{"error":"wrong data size, expected 7853760 got
17627328"}` ❌ |
(`7853760 = 1080×2424×3`; the emulator returns ~2.24× that.)
Two compounding bugs:
1. **`httpScreenshot`** only reads `error` on a non-2xx status. On the
`200 + {error}` path it falls through to the generic *"missing url or
path … restart it"*, hiding the real cause.
2. **`captureLiveInput`** hard-codes `scale: 1.0` with no fallback, so
the whole diff fails on Android.
## Fix (TS-only)
- **`httpScreenshot`**: when `url`/`path` are missing, surface
`resBody.error` if present (so *"wrong data size …"* reaches the caller)
before the generic hint.
- **`captureLiveInput`**: attempt full-res, and on any failure **retry
at the server's default scale**, which captures reliably. #285's
same-aspect normalization already makes a scaled capture diff-compatible
with a baseline saved at any scale, and full-res is preserved wherever
it works (iOS). If the fallback also fails, its error propagates.
## Tests
`screenshot-diff-tool.test.ts` — full-res→default-scale fallback
(asserts the `1.0` call then a default-scale retry), and error
propagation when both fail.
`simulator-client.test.ts` (new) — `httpScreenshot` surfaces the server
`error` on a `200+{error}` response, still uses the generic hint when
there's genuinely no info, and returns `url`/`path` on success.
All 21 screenshot-diff/simulator-client tests pass; `tsc` clean on the
changed files.
## Scope note
The underlying inability to capture a full-resolution Android frame is a
**simulator-server (native)** issue — the *"wrong data size"*
framebuffer mismatch. This PR makes the tool **degrade gracefully**
(works at the default scale) and **diagnose clearly** instead of failing
outright; a proper native fix for full-res Android capture is a separate
follow-up.
|
||
|
|
25975637dd |
fix(boot-device): gate -crash-report-mode on emulator -help support (#306)
## Problem The Android emulator's `-crash-report-mode` flag is only present in newer emulator builds (~36.x and late 35.x) and is **undocumented** in the release notes, so support can't be inferred from a version string. Passing it to an emulator that doesn't recognize it makes the binary **abort before boot**. ## Change - New `emulatorSupportsFlag(flag)` helper in `utils/adb.ts`: resolves the emulator binary, runs `emulator -help`, and reports whether the flag appears in the listing. Memoized per `binary|flag`; best-effort (returns `false` and never throws if the binary can't be resolved or `-help` fails — including non-zero-exit builds, whose output is still read from the error). - `boot-device.ts` now gates `-crash-report-mode never` on that check and spreads it into **both** the hot- and cold-boot arg lists. The probe is computed after the already-running reuse fast-path returns, so it's skipped when no emulator is spawned. ## Verification - `tsc --build` clean. - Functionally checked against a real Emulator **36.5.11**: `-crash-report-mode` → detected (flag added); an unknown flag → not detected (flag omitted). - `boot-device` tests 7/7; full tool-server suite 690 passed. The single failing `android-binary` test is pre-existing and environmental (host has a real `/usr/bin/adb` that defeats the test's PATH stripping); this diff doesn't touch `android-binary.ts`.v0.10.0 |
||
|
|
18974cab93 |
fix(release): ship ax-service in bin/darwin/ so describe works in published packages (#303)
## Summary `describe` is broken in every published package since **0.9.0**: on a normally-booted simulator it returns only a bare `ROOT AXGroup` (empty tree) or silently falls back to native-devtools, always accompanied by the misleading hint *"this simulator was not booted through argent … call boot-device with force=true"* — even right after a forced argent boot, so there is no recovery. **Root cause:** the published npm package is **missing the `ax-service` binary** entirely. ## Bisect | Release | `ax-service` in tarball | `describe` | |---|---|---| | 0.7.1 | `bin/ax-service` (flat) ✅ | works | | 0.8.1 | `bin/ax-service` (flat) ✅ | works | | **0.9.0** | **absent** ❌ | **broken** | | 0.10.0 | **absent** ❌ | **broken** | - **Last good release:** 0.8.1 - **First broken release:** 0.9.0 - **Breaking commit:** `80661fad` — *feat: Linux support (#249)* ## Why it broke #249 migrated native binaries to a per-platform `bin/<platform>/` layout and updated **two of the three** places that touch the ax-service path: - `packages/native-devtools-ios/src/index.ts` — `axServiceBinaryPath()` now resolves `bin/darwin/ax-service` (was flat `bin/ax-service`). - `packages/argent/scripts/bundle-tools.cjs` — copies the published binary **from** `bin/darwin/ax-service`. …but it **did not** update `scripts/download-native-binaries.sh` — the script the standard `pack` / CI release path runs — which kept downloading ax-service to the **flat `bin/` root**. Producer (download script) and consumer (bundler/resolver) disagreed. During `pack`, the bundler looked under `bin/darwin/`, found nothing, **warned and skipped the copy**, and the tarball shipped with no ax-service. At runtime the ax-service factory throws *(binary not found)* → `describe` catches it → empty tree + degraded hint → native-devtools fallback (which itself fails on plain screens like SpringBoard). `pack:mcp:local` (which runs `build:ios-binaries` → `build.sh`, already writing to `bin/darwin/`) was unaffected — only released/downloaded builds were broken, which is why it wasn't caught locally. ## Fix `download-native-binaries.sh` now downloads ax-service into `bin/darwin/`, matching the resolver and the bundler. Plus a regression test (`package-manifest.test.ts`) that pins the producer→consumer path agreement so the two scripts can't silently drift again (verified it fails on the pre-fix path and passes after). ## Verification Rebuilt via **`npm run pack:mcp`** → tarball now contains `package/bin/darwin/ax-service` (byte-identical, signed Mach-O). Drove the **packaged** binary's `describe` RPC directly and via the full MCP `describe` tool: - iPhone 17 Pro — iOS 26.4 → `source: ax-service`, full tree (32 elements), no degraded hint ✅ - iPhone Air — iOS 26.4 → `source: ax-service`, full tree ✅ - iPhone 16 Pro — iOS 18.5 → `source: ax-service`, full tree (33 elements) ✅ Before the fix all three returned a bare `ROOT AXGroup` (or native-devtools fallback) + the degraded hint. Tests: `packages/tool-server` ax-service/describe suites (40) green; `packages/argent` suite (12, incl. new guard) green. ## Notes - Script-only change to release tooling + one test; no runtime TS changes. The bundled `bin/` artifacts are gitignored (produced by `pack`), so the diff is just the script and the test. - Independent of `fix/boot-device-refresh-stale-services` (stale-service eviction after reboot) — that fix can't help while the binary is absent from the package; both are needed for fully healthy `describe`. |
||
|
|
01eca45eb4 |
fix(tool-server): compare same-aspect screenshots in screenshot-diff instead of failing on a resolution mismatch (#285)
## Problem
`screenshot-diff` rejects the workflow its own docs call "the common
visual-regression flow" — `baselinePath` + `captureCurrent: true`.
The `screenshot` tool saves PNGs at a default scale of `0.3` (e.g.
`396x860`), while `screenshot-diff`'s live capture is hardcoded to full
resolution `1.0` (e.g. `1320x2868`). Feeding a saved `0.3x` baseline
together with a `1.0x` live capture returns `status: dimension_mismatch`
and refuses to compare — even though both images are uniform scalings of
the same framebuffer and share an identical aspect ratio. The advertised
default workflow is therefore broken out of the box.
## Root cause
`diffPngFiles` (in
`packages/tool-server/src/tools/screenshot-diff/screenshot-diff.ts`)
treated **any** difference in pixel dimensions as a hard failure:
```ts
if (baseline.width !== current.width || baseline.height !== current.height) {
return summarizeResult(buildDimensionMismatchResult({ baseline, current }));
}
```
There was no allowance for two images that depict the same content at
different scales.
## Fix
Before the hard dimension-equality check, normalize the two decoded
images to a common size when their aspect ratios match (within a 1%
tolerance, to absorb integer-rounding of pixel dimensions). The
**larger** image is downscaled to the **smaller** one's exact dimensions
using the existing Lanczos3 resampler — we never upscale, so we never
invent detail. When the aspect ratios genuinely differ, the existing
`dimension_mismatch` hard-fail is preserved unchanged.
- New pngjs-free module
`packages/tool-server/src/tools/screenshot-diff/resize.ts` exports
`normalizeToCommonSize(baseline, current)` (returns same-sized images,
or `null` when aspects differ) and `resizeDecodedPng(src, width,
height)`. The pure Lanczos3 resampling math (which has no `pngjs`
dependency) was moved here; `screenshot-diff.ts` now imports it, and its
`downscalePng` delegates to `resizeDecodedPng`, so there is a single
resampler implementation.
- The OCR / font-geometry pass (`analyzeScreenshotTextChangesSafely` →
`detectFontGeometryChange`) re-reads the original files from disk and
samples in-memory pixels using OCR-derived bounds in those files'
coordinate space. It therefore keeps receiving the **originally
decoded** images (not the normalized copies), and its top cutoff is
derived from the original baseline height. Only the pixel-diff path
operates on the normalized, same-sized images. This keeps the text pass
byte-for-byte consistent with prior behavior.
## Testing
- **`packages/tool-server/test/screenshot-diff-resize.test.ts`** (new,
pngjs-free, runs locally): unit tests for the new pure helpers —
same-aspect different-size pairs normalize to the smaller common size;
uniform color survives the resample; transposed and out-of-tolerance
aspect ratios return `null`; an in-tolerance (~0.1%) aspect difference
still normalizes. **11/11 passing locally.**
- **`packages/tool-server/test/screenshot-diff.test.ts`** (existing):
added an integration case asserting that a same-aspect,
different-resolution baseline/current pair is **not** reported as
`dimension_mismatch` and produces a diff at the smaller common size.
This is the true before/after for the bug.
### CI caveat
`screenshot-diff.ts` imports `pngjs` at module load, so
`screenshot-diff.test.ts` (and the integration case above) can only run
in an environment where `pngjs` is installed — i.e. **CI**. The new
`resize.ts` and `screenshot-diff-resize.test.ts` are intentionally
`pngjs`-free so the core normalization logic is verifiable without that
dependency. Typecheck (`tsc --noEmit -p tsconfig.test.json`) is clean
for the changed files.
|
||
|
|
1706fb5c2a |
fix(boot-device): don't tear down a healthy-but-slow emulator on the final boot attempt (#305)
## Problem
On Linux (headless, software-GPU) I hit `boot-device` repeatedly failing
with
`PackageManager did not respond ... Emulator has been terminated`, even
though
the emulator had booted fine. Root cause: **Stage 5** of `attemptBoot`
ran a
single `pm path android` probe with a 10 s timeout and, on failure,
killed the
emulator and threw.
On a loaded host (or a freshly `-wipe-data`'d image still finishing its
first-boot package scan), `pm` can take well over 10 s to answer while
the guest
is perfectly healthy — it already reached `sys.boot_completed` and
registered
with adb. On the **final cold attempt** there is no fallback, so a
momentarily
slow PackageManager turned a usable emulator into a hard boot failure
(and
destroyed it, so the next try started from a cold boot again).
## Fix
- Stage 5 now **retries** `pm path android` within a budget instead of a
single
10 s window.
- Threads a `tearDownIfUnready` flag through `attemptBoot`:
- **hot-boot attempt** — tight ~10 s budget, tear down on failure so we
still
fall through to the cold boot.
- **final cold attempt** — retry up to 45 s and, if PM is still slow,
**return
the emulator as booted rather than destroying it**. gRPC
screenshots/gestures
work without PM, and killing it just guarantees failure with nothing to
fall
back to. A confirmed QEMU crash (mid-probe or via the early-exit racer)
still
tears down and rethrows the real cause.
Also adds a note to the `argent-android-emulator-setup` skill:
hard-killing qemu
(`pkill -9`) leaves the userdata image dirty — the usual cause of a cold
boot
that hangs for minutes with runaway writes — and `-wipe-data` resets it.
## Tests
`packages/tool-server/test/boot-device-hotboot.test.ts` — added a
regression
test asserting the final cold attempt returns the emulator as `booted`
(and does
**not** issue `adb emu kill`) when PM stays slow. All 28 boot-device
tests pass.
## Out of scope / follow-ups (found during the same Linux session)
- `simulator-server` (Rust) **panics** (`android_emulator.rs:104 "should
be
running"`) when asked to attach to a pre-existing emulator launched with
`-read-only` that never registered gRPC discovery. Better to return a
clear
"can't attach, reboot via Argent" error. Separate (cross-language)
change.
- `screenshot-diff` with `captureCurrent: true` fails reproducibly with
`"server response missing url or path"` while plain `screenshot` and
file-vs-file diff both work.
- `react-profiler-component-source` returns `found: false` for valid
top-level
`export function` components.
|
||
|
|
3d2d254004 |
fix(wayland-e2e): use ARGENT_PORT instead of PORT to start tool-server (#304)
## Summary - The tool-server reads its port from `ARGENT_PORT` (index.ts:72), not the generic `PORT` env var - The CI step was setting `PORT=3033` which the process silently ignored, causing it to bind on the default 3001 - The health-poll loop and every downstream curl targeted 3033, so startup always timed out |
||
|
|
d87fbcc973 |
chore: bump version to 0.10.0 (#301)
Bumps the version from `0.9.0` to `0.10.0` across all versioned packages in the workspace. ## Packages bumped (0.9.0 → 0.10.0) - `@swmansion/argent` - `@argent/cli` - `@argent/installer` - `@argent/mcp` - `@argent/tools-client` - `@argent/native-devtools-android` - `@argent/native-devtools-ios` - `@argent/registry` - `@argent/skills` - `@argent/tool-server` - `@argent/update-core` `package-lock.json` updated to match. ## Left unchanged - `@argent/ui` — on a separate version track (`0.0.1`) - `packages/argent-private` — has no `version` field Internal dependencies use `file:` references, so no dependency specifiers required updating. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
8efd7c6346 |
feat: add argent server start for long-lived tool-server (#182)
## Human summary:
This adds `argent server start` command to enable us to spawn tools
server in arbitrary location. we want this to enable using remote
tools-server via our mcp not only local ones.
## Summary
- Adds `argent server start` to spawn a tool-server that does **not**
auto-shutdown on idle, with configurable bind port and host — the
missing piece for running tool-server on a remote/dedicated host.
- Foreground by default so process supervisors (systemd, Docker,
supervisord) own the lifecycle; `--detach` keeps the existing
daemon-spawn behavior. Refuses to clobber a healthy running server
unless `--force`.
- Phase 2 (consumer-side `--tools-url` flag pointing the MCP / CLI at a
remote tool-server) is now a one-flag change away — `tools-client.ts`
and `mcp-server.ts` already honor `ARGENT_TOOLS_URL`. Out of scope for
this PR.
## What changed
- **`tool-server`** — read `HOST` env var (default `127.0.0.1` so the
existing auto-spawn path is unaffected), bind to it, attach an `error`
listener on `app.listen` so `EADDRINUSE` / `EACCES` exit cleanly instead
of routing through `uncaughtException`.
- **`argent-tools-client`** — broaden the readiness regex to match any
host; extend `buildToolsServerEnv` with optional `{ host,
idleTimeoutMinutes }`; record `host` in `~/.argent/tool-server.json`
(backward-compat: missing field defaults to `127.0.0.1`); export
`spawnToolsServer`, `findFreePort`, `isToolsServerHealthy`,
`isToolsServerProcessAlive`, the state read/write/clear helpers, and
`formatToolsServerUrl` so the CLI can reuse them.
- **`argent-cli`** — new `server start [flags]`: `--port`/`-p` (default
3001, `0` = pick free), `--host` (default `127.0.0.1`), `--idle-timeout`
(default `0` = never), `--detach`/`-d`, `--force`, `--help`. Warns when
host is non-loopback. Foreground mode forwards `SIGINT`/`SIGTERM` to the
child and writes the state file so local `argent run` / `argent mcp` can
attach.
- **`argent` dispatcher** — pass `BUNDLED_RUNTIME_PATHS` to `server()`
so the new subcommand can find the bundled tool-server.
## Test plan
- [x] `argent server --help` lists the new `start` subcommand
- [x] `argent server start --help` shows full flag reference
- [x] `argent server start --detach --port 0` → status alive/healthy,
`/tools` reachable, state file populated with `host` field
- [x] Second `argent server start --detach` refuses (`exit 1`) with
helpful message
- [x] `argent server start --detach --force` kills the old PID and
starts a new one
- [x] Foreground `argent server start --port 0` runs with inherited
stdio; child PID is the actual tool-server (not the wrapper); `argent
server stop` from a sibling shell tears down both
- [x] Existing auto-spawn path (`argent tools`) still works — verifies
the regex broadening + default-host change is backward-compatible
- [x] `tool-server` (444 tests) + `argent-tools-client` (1) + `argent`
dispatcher (1) suites pass
- [ ] Manual smoke on `--host 0.0.0.0` — could not exercise locally
(sandbox declined the public-interface bind); the warning + bind path is
identical to loopback besides the listen address
---------
Co-authored-by: pFornagiel <pawel.fornagiel2@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Paweł Fornagiel <85541429+pFornagiel@users.noreply.github.com>
|
||
|
|
2be58a5cf1 |
fix(boot-device): five correctness fixes from post-merge Linux review (#296)
## Summary Five issues found in the post-merge code review of #249 (Linux support). All confirmed present on main; none fixed by #194. ### H1 — `awaitFirstRealFrame` timeout leaves emulator running `awaitFirstRealFrame` is called after the cold-boot `try/catch` closes. On a SurfaceFlinger timeout the emulator process stayed alive: `inFlightBoots.finally` cleared the map entry, so the next `boot-device` call found the stale serial via the `alreadyRunning` fast path, had to run `assertScreencapAlive`, killed it there, and then fell through to another full cold boot — doubling latency and surfacing a confusing "wedged framebuffer" error instead of the original timeout. **Fix:** wrap the `awaitFirstRealFrame` call in a try/catch that calls `killEmulatorQuietly` before re-throwing. ### H2 — `awaitFirstRealFrame` budget not clamped against `overallDeadline` Every `attemptBoot` stage clamps via `Math.min(params.attemptDeadline, …)`. `awaitFirstRealFrame` received the fixed `STAGE_BUDGET.firstRealFrame` (90 s) with no knowledge of `overallDeadline`, allowing total wall time to silently exceed `bootTimeoutMs` by up to 90 s — violating the documented contract and the Zod schema's `max(900_000)`. **Fix:** pass `Math.min(STAGE_BUDGET.firstRealFrame, Math.max(0, overallDeadline - Date.now()))`. ### H3 — `ARGENT_SIMULATOR_SERVER_DIR` semantic changed without migration hint The env var previously pointed directly at the directory containing `simulator-server`; it now points at the root of the per-platform tree. A user with the old flat layout gets a bare "binary not found" with no explanation of what changed. **Fix:** detect the old flat layout and include a migration hint in the error message; update the env var's doc comment. ### M4 — `ARGENT_EMULATOR_NO_WINDOW=false` silently enables headless mode `selectExtraEmulatorArgs` used a deny-list (`!== "0"`) so `"false"` and `"no"` — natural shell idioms for "disabled" — both mapped to `-no-window`. Test suite covered `""`, `" "`, `"0"` but not `"false"`. **Fix:** switch to an allowlist `["1", "true", "yes"]`; add `"false"` and `"no"` to the parameterized off-test. ### M5 — `bootstrapDylibPathTcp` / `nativeDevtoolsDylibPathTcp` missing `requireDarwin` All six non-TCP dylib/binary exports gained explicit `requireDarwin()` guards in #249. The two TCP dylib exports did not. A Linux caller gets "dylib not found" instead of "requires macOS host". **Fix:** add `requireDarwin()` as the first statement in both TCP dylib functions; add a test. ### S7 (bonus) — `selectGpuMode` validation fires mid-function after slow I/O `selectGpuMode()` and `selectExtraEmulatorArgs()` were called inside deep nested code after several seconds of I/O. A typo in `ARGENT_EMULATOR_GPU_MODE` surfaced late with a spurious "emulator has been terminated" suffix appended. **Fix:** call both once at the top of `bootAndroidImpl`, just after `ensureDep`, and reuse the captured values — eliminates the late error and the redundant double-call. ## Test plan - `npm run test -w @argent/tool-server` — **847 passed** (was 845; +2 for `false`/`no` NO_WINDOW cases) - `npm run test -w @argent/native-devtools-ios` — **10 passed** (was 8; +2 for TCP dylib darwin gate and migration hint) |
||
|
|
372f7c123f |
feat(boot-device): Linux boot diagnostics for KVM access and AVD sizing (#294)
## Summary - Adds `linux-preflight.ts` with two non-blocking diagnostics run at boot time on Linux hosts: - `/dev/kvm` readability check — warns when KVM is missing or inaccessible, which causes 10–50× TCG software-emulation slowdown - AVD `hw.ramSize` / `vm.heapSize` floor check — warns when values are below the threshold where Android's Watchdog kills `system_server` under Hermes+Metro+swiftshader load - Both warnings name the exact fix command; neither throws nor blocks - Wired into `boot-device.ts` via `linuxBootDiagnostics(avdName)` immediately after `ensureDep` succeeds ## Stacking Stacked on `feat/linux-host-support`. Base will be retargeted to `main` after that PR merges. ## Test plan - [ ] `linux-preflight.test.ts` covers the pure `diagnoseAvdSizing` helper for all four sizing combinations - [ ] `npm run test -w packages/tool-server` passes |
||
|
|
f2b0c0be1a |
feat(android-binary): probe OS-default SDK install locations when env vars are absent (#293)
## Summary - Extends `androidRoots()` to fall back to OS-default Android SDK install paths after `ANDROID_HOME` / `ANDROID_SDK_ROOT` come up empty - Covers Android Studio defaults (`~/Library/Android/sdk` on macOS, `~/Android/Sdk` on Linux) and common system-wide paths (`/opt/android-sdk`, `/usr/lib/android-sdk`, etc.) - Fixes the case where an MCP server spawned by a GUI process (Claude Code desktop, VS Code) doesn't inherit the shell-exported `ANDROID_HOME`, causing argent to silently fail to find `adb`/`emulator` even on a correctly configured machine ## Stacking Stacked on `feat/linux-host-support`. Base will be retargeted to `main` after that PR merges. ## Test plan - [ ] New tests in `android-binary.test.ts` cover the Studio-default paths and the `ANDROID_HOME` priority invariant - [ ] `npm run test -w packages/tool-server` passes |
||
|
|
74c2065ca0 |
fix: multiple minor bugs (#194)
## Human Summary
This PR aims to fix bugs that are benign for now, could become serious
in the future:
- multiple CSRF via CORS misconfiguration on `tool-server`
- multiple SSRF that were possible due to the CSRF
- path traversal via flow creator
- multiple shell injections
- lack of auth on `MCP/CLI <-> tool-server` path (bug details discussed
in person). After the other fixes this is somewhat redundant, but
doesn't hurt and protects against theoretical access attempts from
within other processes running on the user's device.
## AI Summary
<details>
Bundles six independent fixes that together close a CSRF cluster on the
local tool-server. The vulnerabilities chain because the HTTP API has no
auth and serves wildcard CORS, so any web origin (Firefox
unconditionally; Chrome via PNA-disabled or loopback-served) can drive
the tool-server from a cross-origin tab. Each individual fix lives on
its own branch (linked below) and stands alone — this PR exists so the
cluster can be reviewed and shipped as one piece.
### Fixes
1. **`fix(http): require auth token + drop wildcard CORS`** —
per-process 32-byte hex Bearer token, generated by the launcher,
persisted in `~/.argent/tool-server.json` with mode `0600`, required on
every route except `/preview`. Constant-time compare. Wildcard CORS
dropped. Threaded through MCP server + CLI + tools-client.
Backwards-compat: when `ARGENT_AUTH_TOKEN` is unset (e.g. `npm run
dev`), the server logs a one-shot warning and skips the gate.
2. **`fix(http): reject non-loopback Host headers`** — middleware
(before the auth gate) returns 403 for any request whose Host is not
`127.0.0.1`, `localhost`, or `::1`. Closes DNS-rebinding bypass.
3. **`fix(ios-profiler-start): prevent shell injection via device_id`**
— `detectRunningApp(udid)` no longer interpolates `params.device_id`
into `execSync` template strings. Both `xcrun simctl spawn ${udid} …`
and the piped `xcrun simctl listapps ${udid} | plutil …` are rewritten
to `execFileSync` array form, with the pipe to `plutil` happening in JS
(input option) rather than the shell.
4. **`fix(flows): validate name to close path-traversal in
getFlowPath`** — `getFlowPath()` requires `^[A-Za-z0-9_\\-]+$` and
verifies `path.relative(getFlowsDir(), filePath)` does not start with
`..`. Centralizing the check in `getFlowPath` covers every caller
(start-recording, add-step, insert-echo, finish-recording, run,
read-prerequisite) without per-tool churn.
5. **`fix(source-resolver): reject paths outside projectRoot +
non-source extensions`** — `readSourceFragment()` previously accepted
any `file` field on the SourceLocation it was given (the field traces
back to a React fiber's `_debugSource.fileName`, which is
attacker-controllable). Now: containment check via
`path.relative(projectRoot, abs)` plus an extension allowlist
(`.js/.jsx/.ts/.tsx/.mjs/.cjs/.json`).
6. **`fix(source-maps): reject non-loopback URLs (close SSRF)`** —
`doRegister()` validates `sourceMapURL` before fetching: only
`http://`/`https://` URLs whose hostname is loopback. Closes a
blind-SSRF probe of the host network (cloud metadata at
`169.254.169.254`, intranet pages, etc.).
### Individual branches
- `fix/auth-token-drop-cors`
- `fix/host-header-validation`
- `fix/profiler-command-injection`
- `fix/flow-name-validation`
- `fix/source-resolver-traversal`
- `fix/source-map-ssrf`
If you want to land them as separate PRs, the integration branch can be
dropped after each lands.
## Test plan
Unit tests (already in CI):
- [x] `npm run build` passes (workspace tsc clean)
- [x] `npm run test -w @argent/tool-server` — **510 passed (was 473 on
`main`)**, +37 new tests across:
- `test/http-auth.test.ts` — 10 tests (auth, CORS removal, dev-mode
escape hatch)
- `test/http-host-validation.test.ts` — 7 tests (loopback allow,
DNS-rebinding reject)
- `test/flows/flow-utils.test.ts` — +5 tests (path-traversal name
rejection)
- `test/metro/source-resolver.test.ts` — +5 tests (containment +
extension allowlist)
- `test/metro/source-maps-ssrf.test.ts` — 10 tests (loopback-only allow)
Live end-to-end (run locally against this branch's bundled artifacts):
- [x] Launcher spawns tool-server; `~/.argent/tool-server.json` contains
a 64-char hex token and is mode `0600`
- [x] `GET /tools` without `Authorization` → **401**
- [x] `GET /tools` with wrong token → **401**
- [x] `GET /tools` with correct token → **200** (61 tools)
- [x] Request with `Host: attacker.example` → **403**
- [x] No `Access-Control-Allow-Origin` header on responses
- [x] Cross-origin command-injection probe via `device_id=\"x;touch
/tmp/marker;#\"` does NOT create the marker file (Vuln 4 closed)
- [x] `flow-start-recording` with `name=\"../../../tmp/escape\"`
rejected with `Invalid flow name`
- [x] `flow-start-recording` with a valid name still works
- [x] `tools-client.fetchTools()` and
`tools-client.callTool('list-simulators')` succeed via the auth-token
threading (the path the MCP server uses internally)
- [x] MCP-stdio entry point (`startMcpServer`) end-to-end: `initialize`
→ `tools/list` (61 tools) → `tools/call('list-simulators')` returns
content. Confirms the launcher → token → fetch → tool-server flow is
intact for the production `argent mcp` path.
## Notes for reviewers
- `/preview` is intentionally exempt from the auth gate (it was not
gated before; tightening it deserves a separate PR).
- The `ARGENT_AUTH_TOKEN`-unset escape hatch keeps `npm run dev`
workable; production `argent mcp` always passes a token via env, so the
warning never fires there.
- The auth bundle changes the public return shape of `ensureToolsServer`
from `Promise<string>` to `Promise<{ url, token }>`. The two in-tree
callers (`argent-mcp/mcp-server.ts`,
`argent-tools-client/tools-client.ts`) were updated; external consumers
using `ARGENT_TOOLS_URL` should also set `ARGENT_AUTH_TOKEN` to match
the server.
</details>
|
||
|
|
80661fad8f |
feat: Linux support (#249)
This PR makes Linux work.
# AI Summary
<details>
Brings Argent up on Linux as a host: per-platform npm bundle, renderer
selection that actually works on hybrid-GPU / Wayland / containerized
Linux, hot-boot guarding that survives the SwiftShader composite gap, a
real Linux preflight, and a Wayland-headless E2E job in CI. macOS stays
on
the path it was on before (no `-gpu` flag was passed; AVD config
decided);
nothing about the iOS Simulator behavior changes.
The diff is broader than "ship the Linux artifact" — calling it out
below
because several pieces affect macOS too and reviewers should be primed
for
that rather than skim past expecting "just packaging."
### What's actually in here
**npm packaging (the original framing — still load-bearing)**
| File | Change |
|---|---|
| `packages/argent/scripts/argent-simulator-server.cjs` (new) | Node
dispatcher exposed as the `argent-simulator-server` bin entry. Joins
`process.platform` into the bin path and execs. Forwards
SIGTERM/SIGINT/SIGHUP parent→child so non-TTY supervisors can't orphan
the binary. |
| `packages/argent/scripts/bundle-tools.cjs` | Copies per-platform
binaries from `packages/native-devtools-ios/bin/<platform>/` into
`packages/argent/bin/<platform>/`. darwin required, linux best-effort. |
| `packages/argent/package.json` | `bin.argent-simulator-server` points
at the dispatcher. |
| `packages/native-devtools-ios/src/index.ts` | Resolver joins
`process.platform` into the simulator-server bin path. iOS-only dylibs
and `ax-service` throw `"requires a macOS host"` on Linux callers so
they get the root cause, not "file not found". |
| `scripts/download-simulator-server.sh` | Downloads
`simulator-server-argent-{macos,linux}` into platform-keyed subdirs.
**Purges each platform dir before its download attempt** so a failed
download can't leave stale binaries in place from a prior run. |
| `scripts/dev.cjs` | Local dev copies the current host's binary into
`bin/<process.platform>/` to match the resolver layout. |
| `.github/workflows/publish{,-next}.yml` | Replaces single `test -x
darwin/...` with a per-platform inventory loop. darwin always required;
Linux gated by `ARGENT_REQUIRE_LINUX_BINARY=1` (flip once radon#118
lands). |
**Linux runtime — emulator boot path**
| File | Change |
|---|---|
| `packages/tool-server/src/tools/devices/boot-device.ts` |
`selectGpuMode()`: Linux defaults to `-gpu swiftshader` (`auto` lands on
lavapipe with ~10× cold-boot slowdown; `-gpu host` produces silent
black-window failures on Optimus / NVIDIA+libglvnd / Wayland-on-hybrid /
containers). macOS keeps `auto` (ANGLE→Metal).
`ARGENT_EMULATOR_GPU_MODE` overrides, **validated against `emulator
-help-gpu`'s whitelist** so a typoed value fails fast instead of burning
the whole boot budget. `ARGENT_EMULATOR_NO_WINDOW=1` appends
`-no-window` for headless contexts (CI / containers / Wayland sessions
with no Qt wayland plugin). |
| `packages/tool-server/src/tools/devices/boot-device.ts` (cont.) |
**Cold-boot:** `awaitFirstRealFrame` polls SwiftShader's lockscreen
composite (PNG byte-count via `screencap -p`); blocks `booted:true`
until a real frame shows up. Without this, callers chaining `boot-device
→ screenshot` get an all-black PNG because `sys.boot_completed=1` fires
5–60 s before the first composite. |
| `packages/tool-server/src/tools/devices/boot-device.ts` (cont.) |
**Hot-boot:** `assertScreencapAlive` polls within an 8 s budget instead
of single-probing. The earlier single-probe caught the sticky-blank
state but also killed the emulator on every transient snapshot-restore
blank, forcing "every boot is cold" cycles. Sticky vs transient is
discriminated by whether the budget exhausts. |
| `packages/tool-server/src/utils/linux-preflight.ts` (new) |
`linuxBootDiagnostics()` checks `/dev/kvm` access. `diagnoseAvdSizing()`
warns when a target AVD's `hw.ramSize`/`vm.heapSize` is below the floor
an aggressive RN/Expo app needs (2G/228M avdmanager defaults wedge under
load). |
| `packages/tool-server/src/utils/android-binary.ts` |
`defaultAndroidRoots()` probes typical SDK install locations when
`ANDROID_HOME`/`ANDROID_SDK_ROOT` are unset — **cross-platform**,
benefits macOS launches from GUI-launched MCP hosts (Claude Code Desktop
etc.) where shell rc env doesn't propagate. |
**Cross-platform fixes that landed alongside**
| File | Change |
|---|---|
| `packages/argent-installer/src/utils.ts` | `getLatestVersion()` now
redirects `npm view`'s stderr to `ignore` so `EBADDEVENGINES` warnings
from a consumer project's `package.json` (e.g. Bluesky's pnpm pin) don't
leak to the user's terminal. **Cross-platform fix**, not Linux-specific.
|
**CI**
| File | Change |
|---|---|
| `.github/workflows/wayland-e2e.yml` (new) | Boots an AVD through the
tool-server under headless Weston; asserts `hw.gpu.mode = swiftshader`
and that screenshots + gesture-tap work without a display. Currently
runs on `pull_request` for the affected paths + `workflow_dispatch`.
Pinned to `radon-daily` for the simulator-server until radon#118 ships a
Linux asset to `radon-main`. |
### Tests added in this PR
- `packages/argent/test/dispatcher.test.ts` — e2e against tmpdir-staged
dispatcher + fake `<platform>/simulator-server`: missing-binary error
path, exit-code passthrough, argv passthrough, SIGTERM forwarding.
- `packages/tool-server/test/native-devtools-ios-resolver.test.ts` —
`requireDarwin` gates for dylibs and `ax-service` on Linux, per-platform
`simulator-server` path resolution, missing-binary error,
`ARGENT_NATIVE_DEVTOOLS_DIR` override.
- `packages/tool-server/test/linux-preflight.test.ts` — KVM diagnostic
shape + `diagnoseAvdSizing` (ram low, heap low, both, neither, missing
keys).
- `packages/tool-server/test/boot-device-hotboot.test.ts` — extended
with `it.each([linux→swiftshader, darwin→auto])` so a single CI runner
exercises both GPU branches (was runner-platform-dependent, regressions
could pass on both runners individually). New cases for the wedged-frame
fallback (under fake timers because the polling budget is short but
real-time enough to exceed vitest's 5 s default), the GPU-mode whitelist
rejection, and the cold-boot composite-wait timeout.
- `packages/tool-server/test/android-binary.test.ts` — extended for
`defaultAndroidRoots` probe ordering and HOME pinning.
### Verification
- `tsc --build`: clean apart from pre-existing
`@argent/native-devtools-android` import errors unrelated to this
branch.
- `vitest run` in `packages/tool-server` and `packages/argent`: 689 + 11
= 700 tests passing; the 4 pre-existing test-file load failures
(native-devtools-android missing) reproduce on `main` and are out of
scope.
- `npm pack -w @swmansion/argent`: produces `swmansion-argent-0.8.0.tgz`
with the per-platform layout.
- Wayland-e2e: triggered on this branch — link will be in a PR comment
once the run completes.
- Manually verified end-to-end on a Wayland-on-hybrid-GPU developer
laptop: AVD boots through the tool-server with `-gpu swiftshader`,
screencap returns real frames, gesture tools work.
### Dependencies / what still gates this
- **radon#118** must land before the Linux artifact ships through the
default channel. The download script tolerates the asset being absent
today (best-effort), so this PR is technically safe to merge before —
Linux installs just won't get a working `simulator-server` until the
upstream artifact is published. Once it lands:
- Drop the `radon-daily` arg in `.github/workflows/wayland-e2e.yml`.
- Flip `ARGENT_REQUIRE_LINUX_BINARY` to `1` in both publish workflows.
- **wayland-e2e is not a required status check** today (no branch
protection on `main` requires it). Worth turning on before merging
anything else that touches the Linux boot path.
### Related
- radon#118 — produces the `simulator-server-argent-linux` artifact this
PR consumes.
- argent-private#11 — cross-builds the iOS Simulator dylibs on Linux.
Strictly orthogonal: those dylibs are macOS-runtime-only regardless of
where they're built, and this PR keeps them under `dylibs/` with a clean
error path on Linux callers.
</details>
Fixes #246
|
||
|
|
aed3978c10 |
fix(update): only refresh editors that have argent configured (#241)
## Summary - `argent update` was probing the filesystem for editor dirs (`.gemini`, `.cursor`, ...) and writing MCP entries, allowlists, rules, and agents into every detected editor — including ones the user explicitly unchecked during `init`. - Switch update to read the actual config file for each adapter and only refresh scopes where an argent MCP entry already exists. Editors without one are skipped end-to-end: no MCP write, no allowlist, no rules/agents copy. - Adds `hasArgentEntry(configPath)` to every adapter and a `findConfiguredAdapterScopes()` helper that drives the new branch. Closes #195 ## User experience & product implications **Before:** running `npx @swmansion/argent update` could resurrect editors the user had explicitly unchecked during `init`. For example, a Claude Code-only user with `~/.gemini` on disk would find `.gemini/settings.json`, `~/.gemini/rules/`, and `~/.gemini/agents/` populated after every update. **After:** update only touches editors where argent's MCP entry is already present in that scope's config. The `init` choice is now authoritative across runs. **Alternatives considered:** - *Persist the init selection in a side file.* Heavier — adds a new state file, migration story for existing users, and a recovery story when the file is missing. The MCP entry itself is already a perfect record of "the user opted into this editor in this scope," so we use that. - *Keep `detectAdapters()` but invert the gemini/etc detection rule.* Same root cause (treating editor-dir presence as consent); just shifts the bug to other adapters. **Backwards compatibility / schema changes:** - No tool input/output schema changes. The interface change is internal to the installer package. - New `hasArgentEntry` method on `McpConfigAdapter` is required (not optional). External consumers of `McpConfigAdapter` would need to implement it, but the interface isn't part of the public package API. - For an existing user who runs `update` *and has never run `init`*, nothing happens to their editor configs (correct: nothing to refresh until they opt in). They'll still get the global package version bump and the skills lockfile refresh. **Follow-ups this unlocks:** - Same predicate could power a future `argent status` command that summarizes which editors are configured. - Opens the door to a `--add-editor <name>` flag on `update` for users who want to opt new editors in without re-running `init`. ## Test plan - [x] `npm test -w @argent/installer` — 257 tests pass, including 21 new ones covering `hasArgentEntry` per adapter and the `findConfiguredAdapterScopes` semantics. - [x] `npm run build` — clean tsc build for the whole workspace. - [x] `npx prettier --check` on touched files. - [ ] Manual: in a workspace where `init` was run with gemini unchecked but `~/.gemini` exists, run `argent update` and confirm `~/.gemini/settings.json` is not modified and no `~/.gemini/rules/` or `~/.gemini/agents/` directories are created. - [ ] Manual: in a workspace where `init` configured Cursor + Claude Code, run `argent update` and confirm both still get refreshed (MCP entry, allowlist, rules, agents). - [ ] Manual: run `argent update` in a workspace with no editor ever configured — confirm it still performs the package upgrade and skills refresh, just skips MCP/rules/agents. |
||
|
|
800b781faf |
feat: make ax-service a socket client, add optional TCP connection to native tools (#270)
- flips the server-client relationship between `tool-server` and `ax-service` launched on iOS simulators: now, `ax-service` connects to a listener in `tool-server` - adds an option to connect to `ax-service` / native devtools dylib via TCP instead of UNIX sockets Corresponds to [argent-private PR #12](https://github.com/software-mansion/argent-private/pull/12) |
||
|
|
4d8e3e45ed |
chore(bundle): purge stale Android assets and alias the Android source (#268)
## Summary Two small follow-ups to #237's Android helper bundling. 1. **Stale-artifact purge.** `bundle-tools.cjs` purges `bin/`, `dylibs/`, `skills/`, `rules/`, `agents/` at the top of every run, but the two Android assets live elsewhere — `manifest.json` at the package root and the APK inside the shared `dist/` folder — so they fall through the cracks. A rebuild where the source APK is missing leaves the previous manifest behind, and at runtime `helperManifest()` happily reads it while `bundledHelperApkPath()` points at an APK that no longer exists. Adds an explicit purge of both at startup so a clean tarball either ships both files or neither. 2. **Android source alias.** Adds `@argent/native-devtools-android` to the esbuild alias map so the tool-server bundle resolves from `src/index.ts` directly, mirroring the iOS sibling. Removes the implicit dependency on `tsc --build` having produced `dist/index.js` before `bundle-tools.cjs` runs. ## Context <details> This came out of an E2E review of #237. Of the six findings flagged there: - H1 (missing keystore in the argent-private submodule) — **resolved on main** by `software-mansion/argent-private@9770740` ("chore: update .gitignore to remove Android signing keys and add debug.keystore"). - H2 (no APK in `argent-private-releases`) — **resolved on main**: `argent-android-devtools.apk` is now present in `argent-main` and `argent-v0.8.1`. `bash scripts/download-native-binaries.sh` succeeds end-to-end. - M1 (silent stale-manifest survival across builds) — **fixed by this PR**. - L3 (Android alias missing from esbuild map) — **fixed by this PR**. - L2 (only `console.debug` surfaces the legacy-uiautomator fallback) — the original author intentionally chose debug-level "without leaking into the per-call result"; leaving as-is. - L1 (no Java-side tests for `SnapshotInstrumentation`) — needs a test harness in `argent-private`; out of scope here. ## Test plan - [x] `npx prettier --check packages/argent/scripts/bundle-tools.cjs` — clean - [x] `npm run build` from the workspace root — `tsc --build` succeeds - [x] `npx vitest run` in `packages/tool-server` — 718/718 pass - [x] Repro before/after: built once with APK present, deleted source APK, re-ran `bundle-tools.cjs` standalone — confirmed `packages/argent/manifest.json` (210 B) and stale `packages/argent/dist/argent-android-devtools-0.1.0.apk` are both purged; resulting `npm pack` ships 44 files with no Android stragglers (vs 46 with both present). - [x] Alias check: deleted `packages/native-devtools-android/dist/index.js` and re-ran `bundle-tools.cjs` — bundling still succeeded (esbuild resolved from `src/index.ts` via the new alias). With the alias removed, the same delete causes `Failed to resolve entry for package "@argent/native-devtools-android"` at bundle time. </details> |
||
|
|
57dce88fbf |
chore: bump argent-private (Set-Cookie shared-storage fix) (#289)
Bumps the `packages/argent-private` submodule `6fe3607` → `cc22310` to pull in [software-mansion/argent-private#13](https://github.com/software-mansion/argent-private/pull/13). That fix makes `NativeDevtoolsURLProtocol` copy `Set-Cookie` response headers from the native network inspector's internal ephemeral `NSURLSession` into `NSHTTPCookieStorage.sharedHTTPCookieStorage`, so cookie-based sessions keep working while native network inspection is active (mirrors radon-ide [#122](https://github.com/software-mansion/radon/pull/122)). The fix was validated on-device (before/after) against a real Expo/RN app: with the unfixed dylib a freshly-issued `Set-Cookie` was dropped and the next request sent a stale cookie; with the fix the new cookie was propagated and echoed back on the subsequent request. |
||
|
|
c5c3f7af11 |
feat: feature flags (#260)
## Summary
- New top-level CLI commands `argent enable <flag>` / `argent disable
<flag>` / `argent flags`.
- Storage is a small JSON file at `~/.argent/flags.json` by default;
`--scope project` writes to `<project-root>/.argent/flags.json` instead.
- `enable` writes `true`. `disable` removes the entry at the chosen
scope (and deletes the file + empty `.argent/` when it becomes the last
flag).
- `isFlagEnabled` walks project → global, so a project entry shadows the
same key in the global file. To opt a single project out of a
globally-enabled flag, hand-edit `<project>/.argent/flags.json` to
`{"flags":{"name":false}}` — there is no CLI for an explicit override.
- The dispatcher routes the new commands to `@argent/cli` exports;
bundling needs no config changes (only `node:fs|path|os`).
## Behaviour
- `argent enable foo` → writes `{ "flags": { "foo": true } }` to global
file.
- `argent disable foo` → removes `foo` from the global file (clean round
trip with `enable`).
- `argent enable foo --scope project` → writes to
`<project-root>/.argent/flags.json`.
- `argent flags` → lists effective state with the winning scope;
`--json` for scripting.
- Project root is the nearest ancestor with `.argent`, `.git`, or
`package.json` — falls back to cwd when none found.
- POSIX `--` separator: `argent enable -- weird-name` is supported.
- Help (`--help` / `-h`), invalid `--scope`, invalid flag names, missing
arg, unknown flags all produce a clear stderr error and `exit 2`.
- Flag-name validation: `/^[a-zA-Z][a-zA-Z0-9._-]*$/`.
- Writes are atomic (tmp file + rename) so concurrent invocations cannot
tear the JSON.
## Reader API
`isFlagEnabled(name)` is exported from `@argent/cli` for future
consumers; it walks project → global and returns `false` when unset.
## Files
- `packages/argent-cli/src/flags.ts` (new) — storage + CLI handlers
- `packages/argent-cli/src/index.ts` — re-exports `enable` / `disable` /
`flags` / `isFlagEnabled` / `FlagScope`
- `packages/argent-cli/test/flags.test.ts` (new) — 41 tests
- `packages/argent-cli/vitest.config.ts` (new)
- `packages/argent-cli/package.json` — description bump
- `packages/argent/src/cli.ts` — dispatcher routes + help text
- `README.md`, `CONTRIBUTING.md` — command tables
## Test plan
- [x] `npm test --workspaces --if-present` → 1085 tests pass (41 in
`@argent/cli`).
- [x] `npm run typecheck:tests --workspaces --if-present` → clean.
- [x] `npm run build` (root `tsc --build`) → clean.
- [x] `npm run build -w @swmansion/argent` (dispatcher + esbuild
bundles) → clean.
- [x] `npx prettier --check .` → clean.
- [x] End-to-end against the built dispatcher in a tmpdir:
- `argent enable my-feature-flag` (global) writes to
`~/.argent/flags.json`.
- `argent enable proj-only --scope project` writes to
`<proj>/.argent/flags.json`.
- `argent disable my-feature-flag` removes the entry; round trip leaves
a clean tree (file + empty `.argent/` removed when no other state lives
there).
- `argent disable foo` preserves sibling flags in the same file.
- `argent flags` lists each flag with the winning scope.
- `argent enable --` errors cleanly; `argent enable -- name-after`
accepts the literal.
- `argent enable foo --scope project` walks up to `.git` /
`package.json` / `.argent`; stray editor configs (`.claude`, `.cursor`,
…) do NOT count as project roots.
- Error paths (`--scope bogus`, no name, `1bad-name`) exit `2` with a
clear message.
|
||
|
|
dee49473c8 |
refactor(tool-server): remove ARGENT_DISABLE_UPDATE_NOTIFICATIONS opt-out (#282)
## What Removes the `ARGENT_DISABLE_UPDATE_NOTIFICATIONS` environment variable and everything tied to it: the `DISABLE_ENV` constant, the exported `areUpdateNotificationsDisabled()` predicate, the early-return gate in `startUpdateChecker()`, the reminder-note bullet that advertised it, and the corresponding test. ## Why The env var made `startUpdateChecker()` a no-op, which left `getUpdateState()` stuck at its defaults (`updateAvailable: false`, `installableVersion: null`). Because the `update-argent` tool reads that shared state, **silencing the periodic reminder also blinded the explicit, user-consented update path** — `update-argent` would report *"Argent is already up to date"* even when a newer version existed on npm. A user who'd quietened the nags but then explicitly asked the agent to update was wrongly told there was nothing to do (only the CLI `npx @swmansion/argent update` still worked). Postponing/suppressing reminders is already handled by the time-based `dismiss-update` tool, whose suppression persists in `~/.argent/update-suppression.json` and does **not** disable the underlying check — so the update state stays accurate. That makes the env var both redundant and a footgun, so it's removed rather than worked around. ## Behavior after this change - The 24h update checker always runs; `getUpdateState()` reflects real registry state. - Reminders are still gated by `dismiss-update` (and the 30-minute auto-suppress after each surfaced note). - `update-argent` now sees accurate state regardless of reminder suppression. ## Tests - Removed the now-obsolete "does not check or notify when notifications are disabled" test. - `vitest run` for `update-checker`, `http-update-note`, and `update-argent-tool` passes (42 tests); `tsc --noEmit` clean for `tool-server`.v0.9.0 |
||
|
|
d670751db3 |
chore: bump all package versions to 0.9.0 (#281)
## Summary - Bump all 11 workspace packages from `0.8.1` → `0.9.0` (`package.json` + `package-lock.json`) - Includes `@argent/update-core`, which was added since the last bump (#267) - `@argent/ui` left at `0.0.1` (independent versioning) ## Notes - Ran `npm install` to update `package-lock.json` — resolved cleanly - Ran `npm run format` (prettier) — no files needed reformatting - No `lint` script / eslint config exists in this repo, so there was nothing to lint ## Test plan - [ ] Verify `npm install` resolves cleanly - [ ] Verify `argent --version` reports `0.9.0` |
||
|
|
cb8db6d4ac |
feat(boot-device): pass canonical launch-hardening flags (#269)
## Summary argent's boot-device currently spawns `emulator -avd <name> -gpu auto` plus the snapshot flags — nothing else. Five flags every MCP-driven boot benefits from were missing; add them as `LAUNCH_HARDENING_ARGS` and spread into the snapshot probe, hot boot, and cold boot. ### Performance (3 flags) - `-noaudio` — guest skips pulseaudio init; saves one thread and ~50 MB RSS. - `-no-boot-anim` — skips the Pixel boot animation, a major CPU spike on software-rendered GPU modes for ~30 s. - `-netfast` — disables qemu network shaping; pure overhead for MCP use. Measured on a 4-core i5-6300U with a 4096 MB / 228 MB-heap AVD, warm-cache cold boot, no other workload, `-gpu swiftshader`: | | Boot time | Δ | |---|---|---| | Baseline (no extras) | 66 s | — | | + 3 perf flags | 49 s | −26% | | + all 5 (incl. dialog suppression) | 47 s | −29% | qemu RSS at +20 s post-boot drops ~190 MB (5%) with the perf flags. `android-emulator-runner` (the canonical CI launcher) defaults to the same three. ### Dialog suppression (2 flags) - `-crash-report-mode never` — keeps emulator crashes from popping a Qt consent dialog that blocks the next boot until a human dismisses it. Crash dumps still land in `/tmp/android-unknown/emu-crash-*.db`; only the modal popup is suppressed. - `-no-metrics` — same for the metrics-collection consent dialog. Unrelated to any argent profiler tool: those run guest-side via Perfetto/simpleperf or via Metro CDP, none of which depend on the emulator's host-side Google telemetry. ### Snapshot parity `-noaudio` and `-netfast` change qemu device topology. The snapshot's saved state has to match the runtime config the next boot uses or the snapshot validator rejects perfectly loadable snapshots — the same class of bug as the prior `-gpu auto` parity work that introduced `RENDERER_ARGS`. Plumb the hardening args into the same three call sites (probe `extraArgs`, hot spawn, cold spawn) so the probe agrees with both boot paths. ## Test plan - [x] Unit tests: extended `boot-device-hotboot.test.ts` with two new cases asserting the flags reach the hot-boot and cold-boot spawn argv; tightened the "same renderer args" parity test to also require `-noaudio`/`-netfast` on the probe. **712 tests pass.** - [x] Live-emulator verification on Linux: actual emulator boot with all 5 flags completes in 47 s, no flag-rejection warnings in the emulator log. - [ ] Confirm no regression on a macOS test host (please run locally before merging — I can only verify Linux here). - [ ] Confirm CI green. Pre-existing test failure in `android-binary.test.ts` on `origin/main` reproduces without this change — host-side `$PATH` leakage in the test setup, not in scope for this PR. |
||
|
|
ee97c443b7 |
feat(tool-server): make the update reminder configurable and release-age aware (#276)
## Why The tools-server shows an update reminder (a `note` appended to every tool response) once a newer `@swmansion/argent` lands on npm. Two pain points: 1. **No way to turn it off.** Suppression is in-memory only (`dismiss-update`, 30-min auto), so it comes back on every server restart. 2. **It nags about versions you can't install yet.** Registries with a **minimum-release-age** (package-cooldown) security policy refuse to install a version until it has been public for N days. Until then the reminder repeats every day for the whole cooldown window with nothing the user can do about it — exactly the "reminded for 5+ days" situation. ## What's in this PR **1. A config option to disable the reminder** - New env var **`ARGENT_DISABLE_UPDATE_NOTIFICATIONS=1`** (`"1"` or `"true"`). - When set, `startUpdateChecker()` is a no-op: **no registry request** is made and **no note** is ever attached. - The opt-out is also surfaced inside the reminder text itself, so the agent can tell the user how to silence it permanently. **2. Minimum-release-age awareness** (answers "detect the policy and check the update is actually installable first") - The checker now fetches the **npm packument** (`registry.npmjs.org/@swmansion/argent`) instead of `/latest`, because only the packument carries the `time` map of version → publish timestamp. - It detects the machine's policy and only reminds once the latest version has **aged past** it. Notification is gated on a new **`updateInstallable`** flag (newer **and** old enough) rather than `updateAvailable`. - `updateAvailable` keeps its pure version-comparison meaning; with no policy, `updateInstallable === updateAvailable` (no behavior change for users without a policy). ### Is detecting the user's min-age policy possible? Yes — partially. Each package manager stores it differently, and the tools-server has no reliable signal for *which* PM installed argent, so we probe every PM present and take the **most restrictive** (largest) value. Erring toward over-waiting is intentional: a slightly late reminder beats nagging about a forbidden version. | PM | Config key | Unit | Detect via | Min PM version | |----|-----------|------|-----------|----------------| | npm | `min-release-age` | days | `npm config get min-release-age` | 11.10.0 | | pnpm | `minimumReleaseAge` | minutes | `pnpm config get minimumReleaseAge` | 10.16 | | yarn (berry) | `npmMinimalAgeGate` | minutes | `yarn config get npmMinimalAgeGate` | 4.10 | | bun | `minimumReleaseAge` (bunfig.toml `[install]`) | seconds | no `config get` — not auto-probed | 1.3 | | yarn classic | — (unsupported) | — | — | — | Because bun has no `config get` for this and auto-detection is best-effort, there's also an explicit override: - **`ARGENT_MIN_RELEASE_AGE_DAYS`** — positive number of days; wins outright and skips probing (useful for bun users, locked-down CI, or pinning the value). When a policy holds an update back, a single stderr line explains the silence; no reminder reaches the agent. ## Conservative edge handling - Network/registry failure → state unchanged (never crashes the server). - Policy in effect but publish time missing/unparseable → treated as **not** installable (stay silent rather than nag). - PM probe errors / PM absent → treated as no policy (0). - Probes shell out via PATH (`exec`, 3s timeout, constant commands only — no caller input) so Windows `.cmd`/`.ps1` shims resolve. ## Tests - `test/min-release-age.test.ts` (new): unit-level parsing, override precedence, unit normalization, most-restrictive-wins, PM-absent. - `test/update-checker.test.ts`: rewritten for the packument shape; new cases for held-by-policy, aged-past-policy, unknown publish time, and the disable flag (probe mocked so tests never spawn a real PM). - `test/http-update-note.test.ts`: gate now keys off `updateInstallable`; added a "available but held by policy → no note" case. - Full tool-server suite green (**732 tests**), `tsc --noEmit` and `typecheck:tests` clean. ## Follow-up (not in this PR) The env-var reference lives in the `argent-private` submodule (`docs/environment-variables.md`), which is a separate repo — needs a companion PR. Suggested rows for the tools-server table: ``` | `ARGENT_DISABLE_UPDATE_NOTIFICATIONS` | `"1"`/`"true"` or unset | disabled | When truthy, disables the update reminder entirely (no registry check, no note). | | `ARGENT_MIN_RELEASE_AGE_DAYS` | number | _(auto-detect)_ | Minimum age (days) a newer version must reach before it is advertised. Overrides auto-detection of the npm/pnpm/yarn minimum-release-age policy. | ``` ## Open questions for review - **Most-restrictive-across-PMs** vs. detecting a single PM — is "max wins" the behavior we want, or should it follow `npm_config_user_agent`? - Should an update held by policy stay fully silent (current), or emit a softened "available, installable in ~N days" note? --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
f34ce22b9a |
fix(tool-server): persist update-note suppression across restarts (#273)
## Summary `dismiss-update` (and the auto-suppression after delivering an update note) only stored `suppressUntil` in a module-scoped variable, so the dismissal evaporated the moment the tool-server process exited — which the launcher does after 30 min of idleness, on machine reboot, and on `npx @swmansion/argent update`. Users asking to silence the note for 24 h would see it reappear well within the window, contradicting the tool's own description. This PR persists `suppressUntil` to `~/.argent/update-suppression.json` (alongside the existing `tool-server.json` / `tool-server.log`) and reads it back on module init, so the dismissal survives restarts. - `suppressUpdateNote` now writes through to disk and throws on write failure (matches `dismiss-update`'s documented "Fails if … the suppression state cannot be persisted"). - The HTTP auto-suppress call (after delivering a note alongside an unrelated tool result) is wrapped in try/catch — a disk-write failure there must not 500 the user's tool call. Worst case: the note repeats next request. - Missing/malformed/wrong-typed file all degrade to "no suppression" — never crashes the server. ## Test plan - [x] `npx vitest run test/update-checker.test.ts` — 19/19 pass (10 original + 9 new persistence tests). - [x] `test/http-update-note.test.ts` + `test/http-tools-meta.test.ts` — 14/14 pass (mock update-checker; persistence is transparent). - [x] Full `@argent/tool-server` suite — 726/727 pass. The single failure (`test/android-binary.test.ts:73`, `/usr/bin/adb` resolution) is pre-existing on `main` and unrelated. - [x] `npm run typecheck:tests` clean, `npm run build` clean, `prettier --check` clean. - [x] Restart simulated via `vi.resetModules()` + re-import → suppression loaded back from disk. |
||
|
|
de0dbbda66 |
feat(tools): implement screenshot-diff tool (#229)
## Summary Adds a new `screenshot-diff` tool for comparing mobile screenshots during UI QA and visual regression checks. The tool can compare two saved PNGs, or compare one saved PNG against one live full-resolution device capture, then returns a compact text summary and generated diff artifacts. ## Context Mobile UI validation often needs more than a single screenshot: small layout, color, typography, copy, clipping, and regression changes are easy to miss manually. This feature gives agents a deterministic before/after comparison path while keeping `screenshot-diff` as supporting evidence. ## What Changed - Registered a new `screenshot-diff` tool in the tool server. - Supports saved PNG comparisons and one-sided live capture via `captureBaseline` or `captureCurrent`. - Adds PNG diffing with fixed internal defaults, changed-region detection, status-bar-band ignoring, full-size diff output, and downscaled context diff output. - Adds optional OCR/text analysis for moved, appeared, disappeared, changed, and restyled text. - Updates MCP content rendering to attach the downscaled context diff image plus the text summary. 1. Capture a stable full-resolution baseline. 2. Perform an interaction or apply a UI change. 3. Compare the current screen against the baseline. 4. Review a summarized diff plus generated artifacts. |
||
|
|
05fb58552d |
chore: bump all package versions to 0.8.1 (#267)
## Summary - Bump all 10 workspace packages to version `0.8.1` (`package.json` + `package-lock.json`) - Includes `native-devtools-android` (`0.7.1` → `0.8.1`), all others from `0.8.0` → `0.8.1` - `ui` package left at `0.0.1` (independent versioning) ## Test plan - [ ] Verify `npm install` resolves cleanly - [ ] Verify `argent --version` reports `0.8.1`v0.8.1 |
||
|
|
c6f4995cfe | chore: bump argent-private submodule to latest main | ||
|
|
90c6081204 |
fix: prevent returning empty screenshots (#261)
## What broke `toMcpContent` base64-encoded whatever bytes the simulator-server URL fetch returned and labelled them `image/png` with no checks. A 404 on the `/media/...` endpoint (file missing) returns an empty body — that became `data: ""` shipped as a PNG, which the model API rejects with "Image could not be processed". Claude Code then loops on the error and burns the session limit (cf. anthropics/claude-code#60334). Reproduces directly: ``` $ curl -sI http://127.0.0.1:<port>/media/missing.png HTTP/1.1 404 Not Found content-length: 0 ``` ## What changed One helper added: `fetchPngBytes(url)` returns the bytes only if: - `fetch` resolves (catches ECONNREFUSED / DNS / abort) - `res.ok` is true - the buffer starts with the 8-byte PNG signature If any check fails, `toMcpContent` emits a single text placeholder instead of an image block. Diff: **+103 / −27** across `content.ts` + tests. ## E2E verification Tested against the real MCP stdio JSON-RPC interface (not just unit tests): **Happy path** — real Android emulator, real tool-server: ``` [1] initialize: argent v0.5.3 [3a] screenshot returned 2 block(s) [0] image: mime=image/png dataLen=66968 validPngSig=true [1] text: Saved: /var/folders/.../media/430131000-...png HAPPY PATH overall: PASS ``` **Failure path** — mock tool-server returning a URL pointing at a closed port: ``` mock on :56529 returned 1 block(s) [0] text: (Screenshot unavailable: no valid PNG at http://127.0.0.1:1/stale.png. Take a new screenshot.) hasImage=false, hasPlaceholder=true FAILURE PATH: PASS ``` ## Test plan - [x] `npx vitest run` — 1048/1048 pass (5 new regression tests in `content.test.ts` cover 404, non-PNG body, fetch-throw, and the no-url fallback) - [x] `npm run build` — tsc clean - [x] `npx prettier --check` — clean - [x] Bundled MCP server rebuilt and driven via real JSON-RPC stdio against (a) a real Android screenshot and (b) a mock tool-server reproducing the bug --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> |
||
|
|
324d8c6049 |
fix(android): stop forcing cold boot on every emulator launch (#251)
## Summary `hasDefaultBootSnapshot` enforced a 60s mtime-skew guard between `snapshot.pb` and `ram.bin`. In practice the emulator updates `snapshot.pb` metadata on every load (load count, last-loaded timestamp) even with `-no-snapshot-save`, so the two mtimes drift apart by hours or days after a few hot-boot sessions and the guard rejects every valid snapshot — forcing a cold boot every time. The bug hits macOS too; Linux just makes it painful because cold boot is slow there. Confirmed against my own `Pixel_6_API_34`, whose `snapshot.pb` is 4 days newer than `ram.bin` while the snapshot itself is fully usable. ## What changed - **Drop the mtime-skew guard.** `-check-snapshot-loadable` + the hot-boot spawn's `-force-snapshot-load` already make truncated / corrupt `ram.bin` fail loudly via `attemptBoot`'s `earlyExitError` race. The on-disk pre-check only needs to confirm both files exist and `ram.bin` is non-empty. - **Resolve the `.avd` folder via the AVD's `.ini` `path=` line**, matching what the emulator binary itself does (`external/qemu/android/android-emu/android/avd/util.cpp`). Snap-installed Studio on Linux and users who relocate AVDs to faster disks both put `.avd` outside the convention root, where the old `<root>/<name>.avd` assumption silently failed. - **Honor `$ANDROID_USER_HOME`, `$XDG_CONFIG_HOME`, `$ANDROID_SDK_HOME`** in priority order alongside `$ANDROID_AVD_HOME` and `$HOME/.android/avd`, mirroring the emulator binary's own lookup precedence. - **Reject whitespace-only and relative `path=` values.** The regex's non-greedy `(.+?)` still captures a single space for `path= `, which would have leaked past `if (!avdPath)` as truthy; a relative `path=` would have resolved against `process.cwd()` and silently mis-located snapshots. ## Test plan - [x] 18 new unit tests in `avd-snapshot.test.ts` covering: priority order across all five env-var candidates, relocated AVDs, drifted mtimes (4-day skew explicitly regression-tested), zero-length / missing / directory-shaped files, whitespace-only paths, relative paths, bad-then-good fallthrough. - [x] Full `npm run test -w @argent/tool-server` — 703 passing. - [x] `npm run typecheck:tests -w @argent/tool-server` — clean. - [x] `npm run build` from workspace root — clean. - [x] `npx prettier --check` on changed files — clean. - [x] End-to-end repro against a real local AVD: drifted `Pixel_3a_API_34`'s `snapshot.pb` mtime to 5 days newer than `ram.bin` (the exact pathology in the wild), drove `boot-device` through this branch's built code, observed `qemu-system-aarch64 ... -force-snapshot-load -no-snapshot-save` in the spawned process — i.e. the hot-boot path was taken with a snapshot the old check would have rejected. ## Risk Pre-check is now more lenient on disk shape. If `ram.bin` is non-empty but truncated/corrupt, the hot-boot spawn still catches it via `-force-snapshot-load` (loud early child-exit) and `attemptBoot` falls back to cold boot. So the worst case has not regressed; it just surfaces at spawn-time instead of pre-spawn. |
||
|
|
11df817616 |
test: drop real-time timeouts from the slowest unit tests (#236)
The slowest unit tests were waiting on real wall-clock timers, not doing
work. This makes them deterministic and fast with **no production
behavior change and no production test seams**.
## Speedup
**Suite-level**
| Scope | Before | After | Faster |
|---|---|---|---|
| tool-server — wall clock | 2.72s | ~1.3s | ~2× |
| tool-server — aggregate test time | 9.70s | 4.03s | 2.4× |
| argent-mcp — aggregate test time | 0.90s | 0.28s | 3.2× |
**Per file**
| File | Before | After | Faster |
|---|---|---|---|
| simulator-server-blueprint.test.ts | 2.55s | 0.05s | ~50× |
| boot-device-hardening.test.ts | 2.17s | 0.18s | 12× |
| boot-device-spawn-error.test.ts | 1.11s | 0.005s | ~200× |
| boot-device-hotboot.test.ts | 1.01s | 0.06s | 16× |
| fetch-timeout.test.ts | 0.81s | 0.18s | 4.5× |
| flow-tools.test.ts (delayMs test) | 0.31s | 0.04s | ~7× |
**Worst individual tests, before → after**
| Test | Before | After |
|---|---|---|
| rejects the boot promise…when spawn emits error | 1.06s | ~1ms |
| reports the emulator crash error… | 1.00s | ~2ms |
| falls back to cold boot when hot-boot child exits early | 1.00s | ~1ms
|
| reports a signal-terminated emulator (SIGSEGV)… | 1.00s | ~1ms |
| 5× simulator-server-blueprint tests | ~0.50s ea | ~0.001–0.02s ea |
| throws after retries when POST never responds | 0.60s | 0.14s |
| sleeps the step's delayMs | 0.31s | 0.04s |
No test exceeds ~0.7s anymore (the slowest,
`workspace-reader-integration`, is genuine subprocess I/O — `node
--version` etc. — left as-is on purpose; it is not a timeout).
## What changed
- **`boot-device.ts` (only production change):** the three hardcoded
poll cadences are extracted into a single immutable
`BOOT_POLL_INTERVALS_MS` constant (values unchanged: 1500/1000/500ms)
and referenced at the four `setTimeout` sites. Pure readability
extraction — **no mutable state, no exported test hooks, no runtime
behavior change.**
- **Boot-device tests:** the timing-sensitive tests now use vitest fake
timers (`vi.useFakeTimers()` + `vi.advanceTimersByTimeAsync`).
`boot-device-hardening` / `-spawn-error` add the same partial
`vi.mock("../src/utils/adb")` already used by
`boot-device-hotboot.test.ts`, so pre-flight does no real filesystem I/O
and the timers drive deterministically.
- **`simulator-server-blueprint.test.ts`:** `signalReady` now emits
`stream_ready` then `api_ready` (matching the real streaming server's
ordering) so the blueprint resolves immediately instead of waiting out
the 500ms `STREAM_GRACE_MS` fallback. Adds a dedicated fake-timer test
that explicitly covers the non-streaming grace path (coverage gained,
not lost).
- **`fetch-timeout.test.ts` / `flow-tools.test.ts`:** smaller timing
knobs; same behaviors asserted.
## No regressions
- 1011/1011 tests pass (tool-server 656, argent-mcp 53, argent 6,
registry 64, tools-client 1, installer 231); `tsc --build` and Prettier
clean. The converted boot-device, simulator-server-blueprint, and
flow-tools tests are deterministic under fake timers (verified across
stress runs).
- No production behavior change; `simulator-server.ts` is byte-identical
to `main`; net +1 test.
- Heads-up: `argent-mcp/test/fetch-timeout.test.ts` has a
**pre-existing** macOS flake (~10–20% rate; `connect EADDRNOTAVAIL
127.0.0.1:<port> - Local (0.0.0.0:0)` — kernel source-port exhaustion
under rapid-fire loopback fetches). Stress-testing showed it on `main`'s
pristine file too (2/10 failures vs 1/10 on this branch), so this PR
does not introduce or worsen it. Fixing it (e.g., EADDRNOTAVAIL retry,
or moving the test off real loopback fetch) is worth a follow-up.
|
||
|
|
18cf0f12cd | chore(deps): bump zod to v4 (#265) | ||
|
|
8d17dc9b4b |
fix(init): stop cramming homedir into mcp config (#257)
`argent init` was writing `/Users/<username>/.argent/mcp-calls.log` into
every generated MCP config (Cursor, Claude Code, VS Code, Windsurf, Zed,
Gemini, Codex, Hermes, opencode). Those files get committed and shared,
so collaborators ended up with the original author's username pinned
into their environment.
Drop the env var from `buildMcpEntry` and have each adapter omit the
`env`/`environment` field when the entry carries no env vars. The MCP
server already falls back to `${homedir()}/.argent/mcp-calls.log` at
runtime, so logging keeps working for every user on their own machine.
Fixes #238
|
||
|
|
a96f1f1578 |
feat: refactor android describe to not rely on uiautomator (#237)
## Summary - `formatDescribeTree` was branching on `source === "uiautomator"` to pick nested rendering; the newer `android-devtools` source fell through to flat mode and lost every node below depth 1. Extends the predicate to cover both Android sources so the helper-service path renders with the same hierarchy as the legacy `uiautomator dump` path. - Bumps `packages/argent-private` to pick up the persistent Android describe service that emits `source: "android-devtools"`, plus its build-and-sign CI workflow. - Wires the Android helper APK + `manifest.json` into the published `@swmansion/argent` package via `bundle-tools.cjs`, and adds `pack:mcp:local` / `build:android-binaries` so a local tarball can be produced without going through `download:native-binaries`. - Minor: tightens a couple of comments in `android-helper-install.ts` and `format-tree.ts`. This is connected to argent-private PR: https://github.com/software-mansion/argent-private/pull/8 |
||
|
|
5b147e9c25 |
fix: profiler attribution on older React DevTools backends (#262)
## Summary - Resolve profiler display-name lookup across both `getDisplayNameForElementID` and legacy `getDisplayNameForFiberID` backends. - Add a fallback diagnostic (`displayNameApiAvailable` / `unattributed_reason`) so fully-unattributed captures are reported as an API-surface mismatch instead of a transient lifecycle race. - Extend multi-renderer script coverage for modern, legacy, mixed, and missing display-name accessor scenarios. |
||
|
|
d091421e3a |
fix(describe): unlock iOS 26.5 SpringBoard-hosted dialogs via IgnoreAXServerEntitlements (#244)
## Summary
iOS 26.5 tightened cross-process AX access: SpringBoard's AX server
rejects MIG queries from non-UIApplication clients with kAXError
\`-25215\`. Our \`ax-service\` daemon is a \`simctl spawn\`-launched CLI
binary, so it fails the new check, and \`describe\` returns an empty
ROOT for any SpringBoard-hosted dialog — most visibly the TCC permission
prompts (\`Allow X to use your location?\`, camera, contacts, photos,
microphone, notifications). On iOS 26.4 the same daemon code works fine.
Apple ships a debug switch in \`AccessibilityUtilities\` for exactly
this case: the \`com.apple.Accessibility/IgnoreAXServerEntitlements\`
preference. When set, the server skips the entitlement check, and our
existing \`-[AXElement explorerElements]\` walk returns the dialog tree
as expected.
This PR wires the pref next to the existing \`AutomationEnabled\` write
in \`ensureAutomationEnabled\`, plus a one-time SpringBoard kickstart so
SB re-reads its AX-server config. The pref persists on the sim's disk,
so subsequent ax-service spawns detect it's already set and skip the
kickstart — avoiding the disruptive app-icon reflow on every argent run.
## Before / after on iOS 26.5
**Before** (raw socket dump for the Maps location prompt):
\`\`\`
{ \"alertVisible\": true, \"elements\": [], \"systemElements\": [], ...
}
\`\`\`
→ describe returns \`source: ax-service ROOT AXGroup\` with no children.
**After**:
\`\`\`
Source: ax-service
ROOT AXGroup (0.000, 0.000, 1.000, 1.000)
AXStaticText \"Allow 'Maps' to use your location?\"
AXStaticText \"Your location is used to show your position on the map,
...\"
AXButton \"Allow Once\"
AXButton \"Allow While Using App\"
AXButton \"Don't Allow\"
...
\`\`\`
Matches iOS 26.4 behaviour. Same daemon binary, no entitlement claims,
no resigning.
## What I changed
Single file: \`packages/tool-server/src/blueprints/ax-service.ts\` —
added \~50 lines to \`ensureAutomationEnabled\` that:
1. Read the current value of \`IgnoreAXServerEntitlements\` from
\`com.apple.Accessibility\`.
2. If it isn't \`1\` already: \`defaults write\` it true, then
\`launchctl kickstart -k system/com.apple.SpringBoard\` so SB re-reads.
3. \`kickstart\` emits a warning about user/foreground service
identifier on iOS 26 — tolerated with \`.catch(() => undefined)\`; the
restart still happens.
## Cost characterisation
| Scenario | Cost |
|---|---|
| Fresh sim never touched by argent | One SpringBoard restart (~3-5s,
apps reflow, status bar blinks) |
| Sim with the pref already on disk (post-first-run, post-reboot, etc.)
| Zero — just one \`defaults read\` |
| Sim shut down when argent first targets it | Pref is written, SB picks
it up on next boot — no restart needed |
| iOS < 26.5 sim | Pref-write is harmless;
\`_shouldValidateEntitlements\` doesn't read it on older versions |
The kickstart fires at most once per sim, ever. The pref is per-sim in
CoreSimulator data; \`simctl erase\` wipes it (next run sets it again).
## Background — why this is the right layer
Briefly investigated:
- **Daemon code-path tweaks** (enumerating
\`currentApplicationsIgnoringSiri\`, fresh
\`_AXUIElementCreateAppElementWithPid\`, etc.) — all hit the same
server-side gate and return \`-25215\`. Cf. closed
software-mansion/argent-private#10 and software-mansion/argent#243.
- **Sign daemon with private AX entitlements** — sim AMFI rejects
(\`Security policy issue\` 163) for anything stricter than what we
already have.
- **Re-host daemon as UIApplication** — would pass the new check but is
a substantial refactor (Info.plist, scene config, background-task
management for the socket server, install/launch instead of spawn). Not
worth it when a one-line debug-pref unlocks the same outcome.
The IgnoreAXServerEntitlements path is the only one that's small,
contained, and reversible.
## Test plan
- [x] \`vitest run\` in tool-server passes (175/175)
- [x] \`tsc --noEmit\` clean on tool-server
- [x] Manual verify on iPhone Air iOS 26.5: Maps location prompt now
returns 5 AX elements from \`describe\` with \`source: ax-service\`
- [x] Manual verify on iPhone 17 Pro iOS 26.4: still works (pref-write
is a no-op for the gating logic on that version)
- [ ] CI / unit coverage for the new conditional kickstart path
## Files
- \`packages/tool-server/src/blueprints/ax-service.ts\` — extended
\`ensureAutomationEnabled\`
|
||
|
|
69538e74a3 |
fix(ax-service): remove unnecessary AutomationEnabled pref write (#256)
## Summary - Removes the `ensureAutomationEnabled` function and all its call sites - The ax-service binary uses `AXRuntime`/`AXSpringBoardServer` private frameworks directly and does not need the `AutomationEnabled` preference on iOS 26.x simulators - Writing it pre-boot into the Accessibility plist actively breaks the AX subsystem by interfering with `cfprefsd` domain initialization, causing `describe` queries to hang ## Investigation Tested on erased sims across five device types (iPhone 17e, iPhone 17, iPhone Air, iPad mini, iPad Pro 11-inch) on iOS 26.5 (runtime `23F77`): 1. **No plist at all** → `describe` returns full hierarchy including SpringBoard-hosted TCC dialogs ✅ 2. **Empty plist** → works ✅ 3. **Plist with `AutomationEnabled=true`** → `describe` hangs indefinitely ❌ The pref was originally added in #98 (April 2026) likely cargo-culted from older iOS automation patterns. On iOS 26.x the `AXSpringBoardServer` MIG interface accepts connections from the ax-service binary without it. ## Test plan - [x] TypeScript clean - [x] All affected tests pass (simulator-server-blueprint, describe-missing-xcrun-424) - [x] E2E: erased sim → boot → launch Maps → `describe` returns TCC dialog with all buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
09a18a65ef | bump to v0.8.0 (#245) v0.8.0 | ||
|
|
2180dd8e6a |
ci: typecheck test files in CI (#202)
We've had lots of type errors in our tests, scripts and other non-core
code despite all CI being green. This PR fixes that.
This PR also removes some redundant features like `@ts-check` inline
directives which have been replaced by proper ts config files.
<details>
## AI Summary
Audit of every CI workflow against every package's `package.json` and
`tsconfig` surfaced several gaps where CI did partial coverage. This PR
closes them and fixes every pre-existing type error the new gates
surfaced.
## Gaps closed
| # | Gap | Fix |
|---|---|---|
| 1 | `tsc --build` only covers `src/**`; test files (`test/**`,
`tests/**`) never typecheck. Vitest transforms tests with esbuild —
strips types instead of checking them. | New `tsconfig.test.json` per
test-bearing package (`composite: false`, `noEmit`, `rootDir: "."`,
includes `src/**` + `test/**` (or `tests/**`) + `vitest.config.ts`). New
`typecheck:tests` npm script per package. New CI step `npm run
typecheck:tests --workspaces --if-present`. |
| 2 | Build/publish scripts (`scripts/*.{cjs,mjs}`,
`packages/argent/scripts/*.cjs`, `packages/skills/scripts/install.js`) —
JS, never typechecked. `sync-readme.cjs` runs in `prepack`;
`postinstall.cjs` runs on every install; `bundle-tools.cjs` is
build-critical. A typo only surfaces at runtime. | Root
`tsconfig.scripts.json` with `allowJs` + `checkJs` + `noImplicitReturns`
covers all script paths. Root `typecheck:scripts` npm script. New CI
step. Per-file `// @ts-check` comments removed — tsconfig drives
inclusion. |
| 3 | `argent-cli` had no `typecheck:tests` script. When tests get added
there they'd silently bypass the gate. | Added `tsconfig.test.json` +
`typecheck:tests` script. `--if-present` would have skipped it anyway,
so this is future-proofing. |
| 4 | Local `prettier --check .` walked into the `argent-private`
submodule and reported violations there. CI didn't see this because
`actions/checkout@v4` doesn't init submodules — so local `prettier
--check` did not match CI behavior. | New `.prettierignore` excludes the
submodule plus `dist/`, `node_modules/`, lock files, `tsbuildinfo`.
No-op in CI; aligns local with remote. |
## Test type errors fixed
These were already present. The `npm test` passed because esbuild
stripped the offending types:
- **argent-installer**: widen `scope` literal type for
dead-code-elimination guard; non-null `addAllowlist!`/`removeAllowlist!`
(optional methods on adapter).
- **argent-mcp**: add `.js` extensions for NodeNext module resolution.
- **registry**: broaden `StaticBlueprintResult` blueprint api from `{
id; deps? }` to `Record<string, unknown>` so existing tests can pass
arbitrary api shapes.
- **tool-server**:
- non-null `zodSchema!` on tool defs that always carry one
(`launchAppTool`, `restartAppTool`, etc.)
- cast sentinel `"ignored"` strings to `DeviceInfo` in factory-rejection
tests
- match the 4-arg `dispatchByPlatform<IosServices, AndroidServices,
Params, Result>` signature (tests still passed the old 3-arg shape)
- `assertFlowRunResult` type guard for the `FlowRunResult |
FlowPrerequisiteNotice` union before `.steps` access
- swap broken `typeof import("supertest").default` for static `import
supertest` (supertest is `export = supertest`)
- fill in the 4 fields missing from a `NativeProfilerSessionApi` mock
(`xctraceProcess`, `recordingTimedOut`, `recordingExitedUnexpectedly`,
`lastExitInfo`)
- cast a vitest `Mock` to a plain function at one direct call site where
the `Procedure | Constructable` union loses callability
- fix `update-checker.test.ts` reading `../../package.json` (off-by-one
— went past tool-server; was relying on a Vite resolver quirk)
- replace one cross-package `../../../registry/src/index` import with
`@argent/registry` so registry's private `services` field isn't compared
between src and dist declarations
All fixes are type-level. Full `npm test --workspaces --if-present`
still reports **991 passed across 87 test files**.
</details>
|
||
|
|
7f1c78104b |
fix(describe): surface dialogs hosted by remote-view services (#240)
## Summary Bumps the \`argent-private\` submodule to pull in the ax-service daemon fix in [software-mansion/argent-private#9](https://github.com/software-mansion/argent-private/pull/9). The radon-lite side of this PR is just the submodule pointer — the actual source change is one if-statement in \`ax_service.m\`. \`describe\` returns nothing when a \`_UIRemoteViewController\`-hosted overlay is on top of the foreground app — most visibly, the \`StoreKitUISceneService\` Apple ID auth sheet from the reporter's case. Root cause: \`+[AXElement primaryApp]\` returns nil while the remote-view service is frontmost, so the daemon never reaches \`collectExplorerElements\`. The submodule PR adds a single fallback: when \`primaryApp\` is nil, look up the AXElement via \`+[AXElement applicationAtCoordinate:<screen-centre>]\` instead, which returns the *hosting* app's AXElement regardless of which process owns the frontmost view. AX bridges the XPC boundary, so the hosting app's \`explorerElements\` walk surfaces the remote-view's content in the same flat-leaf shape the daemon already emits. ## Why this PR vs. #234 I closed [#234](https://github.com/software-mansion/argent/pull/234) after empirical reproduction showed the TS-side \`systemElements\` fallback there had no effect on the reporter's case — the daemon's \`isSystemAppShowingAnAlert\` gate doesn't fire for XPC-hosted sheets either, so \`systemElements\` was empty too. The reachability gap was at the daemon layer (\`primaryApp\` selector returning nil), not the TS adapter. This PR addresses that root cause. ## Empirical verification Built the patched daemon + ran against the reporter's exact trigger on an erased iOS 26.5 sim (News → "Get News+" → "Get Started" → "Apple Account Sign in Requested" sheet): - **Old daemon** (md5 \`d5240e8…\`, currently shipped): raw describe socket returned \`{ "alertVisible": false, "screenFrame": {…} }\` — no \`elements\` field, agent has no labels. - **Patched daemon** (md5 \`75fdffcc…\`, dev build): 42 elements, all dialog content labelled: heading "Apple Account Sign in Requested", email field, "Forgot password?", **"Sign In" at normalized (0.500, 0.414)**, **"Cancel" at (0.500, 0.482)**, plus 37 keyboard keys. The agent can now tap by label. ## Risk - No wire-format change, no TS/adapter change. \`@argent/tool-server\` and the describe handler consume the same shape they did before. - Fallback gates on \`primaryApp == nil\`, so every working case stays on its existing path. - If \`applicationAtCoordinate:\` also returns nil at the centre, the daemon emits no \`elements\` — same as current behaviour. The existing native-devtools fallback in \`describeIos\` still kicks in afterwards. ## Open items before un-drafting - [ ] Merge https://github.com/software-mansion/argent-private/pull/9 (the source change lives there) - [ ] Publish a new \`argent-main\` build to \`software-mansion-labs/argent-private-releases\` so \`npm run download:native-binaries\` picks up the patched \`bin/ax-service\` - [ ] Re-verify \`argent run describe\` end-to-end against the same dialog with the released binary - [ ] One regression-style smoke test: an in-process dialog (e.g. Wallet location prompt) to confirm the existing \`primaryApp\` path still fires for the unchanged cases ## Notes - Daemon binary lives in \`packages/native-devtools-ios/bin/ax-service\` but is \`.gitignore\`d; the release pipeline supplies it. This PR cannot ship the new binary in-repo for the same reason. - I empirically introspected the AXRuntime surface inside the simulator before committing to this approach — \`+systemWideElement\` and \`+systemEventDispatchElement\` segfault from a non-UI process, so coordinate lookup is the practical option that doesn't require a daemon-process redesign. Fixes #207 |
||
|
|
2977d2ca77 |
fix(describe): bump argent-private with WKWebView describe fix (#157)
The native ax-service now recurses into `primaryApp.children` beyond the flat `explorerElements` pass, so WKWebView web content (labels from the WebContent process) is surfaced alongside the host app's VoiceOver targets. Details: https://github.com/software-mansion/argent-private/pull/7 |
||
|
|
4255b0bd66 |
fix: stop simctl spawn loop on iOS init failure (#233)
Fixes #210 — `argent mcp` pinning CPU to 100% and spawning hundreds of `simctl` processes that survive `kill -9`. ## The problems 1. **No retry cap.** The simulator watcher polls every 10s; on any failure it forgets the UDID and re-classifies it as "new" on the next tick. Each retry runs 4–5 `xcrun simctl spawn` calls in series. A persistently failing simulator produces an unbounded loop — hundreds of `entered ERROR state` log lines and a steady process pile-up. 2. **No timeouts on `simctl spawn`.** A degraded `CoreSimulatorService` makes simctl block forever. The factory `await`s the hung call, never rejects, never retries, but the stuck child process stays alive. ## How this fixes it - **Bounded retries with terminal "given up" state.** Each `NativeDevtools` service instance now tracks `{attempts, lastError, givenUp}`. After 3 consecutive init failures the service stops trying and reports `init_failed` to any caller. The watcher keeps polling but skips UDIDs in the given-up state. - **10s timeout on every `simctl spawn` call** in `ensureEnv`, `ensureAccessibilityEnabled`, and `ensureAutomationEnabled`. Healthy CSS responds in ~300ms; 10s is well above any plausible legit latency and well below "hung forever." A hung simctl now rejects cleanly, increments the attempt counter, and feeds the bounded retry instead of accumulating children. - **Tools surface `init_failed` as a typed result** instead of throwing. `launch-app`, `boot-device`, `native-devtools-status`, and the rest of the native tools now return a structured error with the failure reason and a hint to restart the simulator, instead of throwing an opaque exception. - **Factory no longer throws on first env-init failure.** Previously a single failed `simctl` call during factory creation leaked the socket server. The factory now tolerates init failure; the watcher's bounded retry handles the recovery. Verified on live simulator and against test simulating `simctl` timeouts. Happy path works as before, `restart-needed` is still signalled and the new failure path works as well. |
||
|
|
fbe7b087e7 |
chore(deps): batch-apply Dependabot updates (#214–#227) (#228)
This PR consolidates every open Dependabot PR that requires **no source-code changes** into a single branch with one regenerated, coherent lockfile. | PR(s) | Update | Status | |---|---|---| | #214 | GitHub Actions group — checkout@v6, setup-node@v6, setup-python@v6, github-script@v9, upload-artifact@v7 | ✅ Applied | | #215 | prettier 3.8.3, @clack/prompts 1.4.0, semver 7.8.0, yaml 2.9.0, @modelcontextprotocol/sdk 1.29.0, esbuild 0.28.0 | ✅ Applied | | #216 / #221 / #224 / #227 | typescript 6.0.3 | ✅ Applied | | #217 / #222 / #223 | @types/node 25.9.0 | ✅ Applied | | #220 / #226 | vitest 4.1.6 | ✅ Applied | | #218 / #225 | zod 4 | ⏸️ Excluded — requires code migration | | #219 | express 5 | ⏸️ Excluded — requires code migration | | #215 (partial) | tree-sitter 0.25.0 | ⏸️ Excluded — see below | **tree-sitter 0.25.0** ships no prebuilt binaries and its `binding.gyp` hard-codes `-std=c++17`, which fails to compile against Node 20+/24 C++20 `cppgc` headers — a hard `npm install` blocker (would also break CI). Kept at `^0.21.1`; all other #215 updates applied. zod 4 and express 5 are excluded here because they require real source changes; tracked separately. Verified: `tsc --build` clean, esbuild bundles, **1010 unit tests pass**, **zero source files modified**. |
||
|
|
896e775e01 |
Fetch argent skills from GitHub for portable lockfiles (#230)
### Description Install and sync argent skills from a GitHub-pinned source (`software-mansion/argent/packages/skills/skills#v<version>`) instead of the local bundle when possible, so `skills-lock.json` stays portable across machines. Falls back to the bundled skills directory when offline or the version is unknown. ### Fixes #208 |