mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
flow-visual-settle
97 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d08c3a3cfc |
fix(cli): name the missing flag instead of printing a Zod dump (#648)
Fixes #645. ## Before ``` $ argent run gesture-tap [ { "expected": "string", "code": "invalid_type", "path": ["udid"], "message": "Invalid input: expected string, received undefined" }, ... two more ] ``` ## After ``` $ argent run gesture-tap Error: missing required flags --udid, --x, --y argent run gesture-tap [flags] ...full per-flag help... ``` ## Scope The issue names the missing-flag case. Reproducing it showed the same raw dump has a second trigger: any constraint the tool itself rejects, which the CLI cannot know in advance. | invocation | before | after | |---|---|---| | `gesture-tap` (no flags) | raw issue array | `missing required flags --udid, --x, --y` | | `screenshot --scale 99` | raw issue array | `--scale Too big: expected number to be <=1` | | `gesture-scroll --udid X --x .5 --y .5` (zero deltas) | raw issue array | `Pass a non-zero deltaX and/or deltaY …` | | `run-sequence --udid X` | raw issue array | `missing required flag --steps-json` | | `gesture-tap --x abc` | already good | unchanged | | runtime failure | plain message, exit 1 | unchanged | Fixing only the first row would have left the CLI's error voice less predictable than before, so both go through the same renderer. ## How it works Two sources, one output. Missing flags are found locally from the tool's published schema — a **presence** check only, never a type or range judgement, which stays exclusively the server's. That is sound because the schema generator excludes defaulted fields from `required` (`debugger-connect` requires `device_id`, not `port`), so it cannot reject a call the server would have accepted. Everything else is read off the rejection the tool-server already returns. Both produce one report rendered by one function and reported through one call site, so the wording, the output channel and the exit code cannot drift between them. Recognition of a rejection is **structural** — the shape of the issue list plus the tool's own schema — never the wording of a message, which is not ours to depend on and changes between Zod releases. Four gates must all hold, and any failure leaves the existing handling untouched. Details worth a reviewer's attention: - **A rule spanning the whole payload reports no field.** `gesture-scroll` and `gesture-rotate` validate at the object level, and `screenshot-diff` is `.strict()`. Treating those as field-scoped would print `Error: --undefined …`; they are stated on their own instead. - **A rule firing on an *optional* field is not called a missing required flag.** `await-ui-element` refines on `expectedText`, which it does not require — reporting that as missing would contradict the help block printed directly underneath, where the field carries no `(required)` marker. - **The check runs after `--args` is merged**, so `argent run gesture-tap --args '{...}'` is not falsely reported as missing everything. Verified live: it reaches the device and taps. - Presence uses own-key semantics, so a required field named e.g. `constructor` is not treated as supplied by the prototype chain. ## Behaviour changes a reviewer should weigh - **A tool-rejected value now exits 2 instead of 1.** `run.ts` already uses 2 for bad input and 1 for a failed run; a Zod 400 means the tool never ran. No in-repo consumer tests for a specific code — `scripts/e2e-full` only checks zero/non-zero. Note this does **not** make 2 universal: an unknown tool name still exits 1, which is untouched here. - **`--json` gains a defined failure shape** (it had none — the blob printed regardless). One object on stderr, nothing on stdout, so `--json | jq` on a failed run reads an empty stream and a non-zero status: ```json { "error": "missing required flags --udid, --x, --y", "missing": ["--udid", "--x", "--y"], "issues": [] } ``` `issues` carries the tool's list verbatim, which is the raw data the issue asked to keep. - **One adjacent fix**, called out because it is not the reported bug: the global-option parse ran *outside* the error handler, so `argent run screenshot --out` printed a raw stack trace. Same bad-input-to-ugly-output defect, one line above the fix. ## Not done - The unknown-flag hole (`argent run gesture-tap --bogusflag 1` is silently accepted and the tool runs). The pass-through is deliberate — see the comment in `flag-parser.ts` — and belongs with #586, the same CLI-argument family. - `argent flow` builds its own payload and has its own required check; it cannot reach this path. - The server still returns the issue list verbatim, so MCP and other clients are unaffected. All prettifying is CLI-side. ## Verification Live against the built CLI for every row of the table above, plus: `debugger-connect` asks only for `--device_id` and never `--port`; `--args '{...}'` reaches the device and taps; a thrown runtime error still prints its bare message with no `Error:` prefix, no help block, exit 1. 43 new tests. `run-telemetry.test.ts` passes **unedited** — it already asserted the non-Zod passthrough, which makes it the regression guard for the untouched path. Full CLI suite 320 passed, registry 87 passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > **#699 is stacked on this branch** — it conflicts with this one in `flag-parser.ts`, so it was rebased on top rather than left to collide at merge time. Merge this first. |
||
|
|
23b9ae5c87 |
chore: bump to 0.18.1 (#603)
Patch bump. First release whose npm tarball carries `mcpName` (added in #598), which unblocks the MCP registry publish (`mcp-publisher publish` rejects `0.18.0` because its manifest predates the field). `server.json` is bumped alongside `packages/*` for the first time — `scripts/check-workspace-versions.mjs` now enforces it (`All workspace packages and server.json are at 0.18.1` locally). | Commit | Change | | --- | --- | | `eea6c27c` | `style`: fix prettier formatting on main (#604) | | `7e4ece69` | `chore`: server.json for the official MCP registry (#598) | | `fc09612f` | `feat(config)`: `recordings.directory` — configurable save location for screen recordings (#601) | | `cccda92d` | `feat(config)`: `ios.additionalDeviceSets` — additive array of extra CoreSimulator device sets (#542) | Version-only diff, rebased on main as of #604. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
11f384d37f |
fix(chromium): make Electron apps testable without window focus (#605)
## Problem Field report: *"electron needs focus to be tested."* Analysis confirmed three focus/visibility dependencies in the Chromium/Electron surface: 1. **Hidden window → throttled renderer.** A minimized/occluded window's compositor throttles: mouse-input acks stall **~5 s per event** (they wait on hit-testing), wheel scrolls hang (`gesture-scroll` already refused), `visibilityState` flips to `hidden`. 2. **Unfocused window → focus-gated app logic breaks.** `document.hasFocus()` is `false` before any input arrives; focus/blur-dependent UI misbehaves. 3. Focus-gated web APIs (clipboard already worked around it locally with `Page.bringToFront`). ## Fixes **1. CDP focus emulation on every page session** (`primePageSession`, né `enableCoreDomains`): `Emulation.setFocusEmulationEnabled(true)`, best-effort, re-applied automatically after tab switches (Playwright precedent). Live A/B on the Electron testbed: identical boot→unfocus→probe sequence gives `hasFocus:false` on main, `true` on this branch. Bonus verified by A/B (kill server → same minimized window flips to `hidden`): in current Chromium/Electron, emulation also pins reported visibility to `visible` and keeps input unthrottled while genuinely minimized — minimized tap latency **~5 s → ~65 ms**, wheel scroll works. *Trade-off (same as Playwright ships):* while attached, the app can observe neither real blur nor real hidden, so pause-on-hidden behavior is unobservable during a test. **2. Anti-throttling switches for every argent-booted Electron app** (`--disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding`). Sessions are created lazily and die with the idle-exiting tool-server while the app outlives it — the flags cover that detached lifecycle plus emulation-less runtimes. Also covers flow-booted apps via the shared `bootElectronApp`. **3. Hidden-window fail-fast extended from gesture-scroll to gesture-tap + gesture-drag** via shared `assertChromiumWindowVisible`, under a new dedicated `CHROMIUM_WINDOW_HIDDEN` failure code (scroll migrated; `failure_stage` strings unchanged so telemetry stays joinable — analytics keyed on `CHROMIUM_INPUT_INVALID` + `chromium_scroll_window_hidden` should switch to the new code). With emulation active the guard is a backstop that fires only where emulation couldn't be applied — exactly where the stall is real. Keyboard/button stay unguarded on purpose: measured live, key events skip hit-testing and stay fast on hidden windows. ## Verification - Unit: 3075 tool-server + 87 registry tests green, `typecheck:tests` green, new tests for emulation priming, boot args, and the guard (all three tools). - Live matrix on the Electron testbed (dev tool-server): focused baseline; unfocused (`hasFocus` true pre-input, typing lands); minimized booted app (tap 69 ms, scroll works, typing + slider drag land, screenshot fine); minimized **external** un-flagged app on :9222 (tap 63 ms, scroll 460 ms — emulation alone suffices while attached). - Plan + implementation went through three adversarial review rounds (scope, CDP semantics, failure-code taxonomy/telemetry compat, doc accuracy). 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
646f726bc4 |
chore: bump to 0.18.0 (#596)
bump debated myself whether this should be a QoL patch or a minor release and i think the latter makes more sense | Commit | Change | | --- | --- | | `7189333a` | `feat(tool-server)`: human-readable log message (#582) | | `5ac69349` | `feat(flow)`: CSS combinator scopes for flow selectors (#569) | | `e23f0e28` | `feat(flow)`: snapshot `cropOn` for single-element visual diffs (#516) | | `6a53e66a` | `feat(flow)`: rotate directive (#546) | | `68ceb314` | `ci`: cut over npm publishing to the unified trusted-publishing workflow (#571) | | `2546788a` | `ci`: unified publish-npm workflow (#565) | |
||
|
|
a0460eeefe |
chore: bump workspace packages to 0.17.0 (#554)
## What
Lockstep-bump all 16 `packages/*` workspace packages (and the matching
`package-lock.json` entries) from `0.16.1` → **0.17.0**.
Diff is exactly 32 version-field swaps across 17 files — no formatting
or
dependency churn.
## Verification
- **Lockfile CI gate reproduced locally on the exact CI toolchain**
(Node
24.14.1 / npm 11.11.0, matching `lockfile.yml`'s `node-version: "24"`):
`npm install --package-lock-only --ignore-scripts` on top of the commit,
then `git diff --exit-code -- package-lock.json` → **clean**. npm's own
regeneration reproduced the hand-edit byte-for-byte, so none of the
`"peer": true` churn that #513/#539 had to keep out reappears here.
- `node scripts/check-workspace-versions.mjs` → `All workspace packages
are
at 0.17.0.`
- Remaining `repo-hygiene.yml` checks pass: no conflict markers, no
focused
tests.
- `prettier --check` on all 17 changed files → `All matched files use
Prettier code style!`
- **Runtime version actually reports 0.17.0**: `getInstalledVersion()`
(`argent-installer/src/utils.ts:384`) and
`getGloballyInstalledVersion()`
(`topology.ts:94`) both read `version` straight out of the resolved
package
root's `package.json` at runtime, so the bump is the whole mechanism —
no
generated constant or second version file to keep in sync. Exercised
that
resolution against the bumped tree: `PACKAGE_ROOT` → `packages/argent`
(`@swmansion/argent`) → `0.17.0`.
- Verified all 16 lockfile hits belong to workspace packages (parsed the
lockfile's `packages{}` and matched every `0.16.1` entry to a
`packages/*` path) — no unrelated dependency happened to sit at 0.16.1.
- No test asserts a release version string; the only `0.1x.x` matches
under
`*.test.ts` are historical references inside comments.
## Note
`packages/argent-private` needs no pin change here. Its release version
is
carried purely as a git tag — the repo has no version field on the
release
line (its only `package.json` is a `private: true` research CLI on an
independent `0.1.0` line) — and `v0.17.0` is now tagged at `2d6c292`,
which
is **already** the commit this superproject pins. `v0.16.0` and
`v0.16.1`
sit on that same commit, so the submodule entry is correct as-is and
stays
out of this diff.
|
||
|
|
98ae93145b |
feat(screen-recording): save recordings to the project's .argent/recordings (#551)
Stacked on #549 (touch visualizer). ## What Screen recordings now land durably under **`.argent/recordings/`** on the client, instead of the disposable temp-artifact cache — and for a remote `argent link` tool-server they land on the **client** host, not the server. ## Why Two rough edges in the recording flow: 1. The finished video was only ever written to the OS temp-artifact cache, so it was easy to lose and not obviously part of the project. 2. Over `argent link`, the video was downloaded into that same temp cache; there was no project-scoped home for it. ## How - New optional `saveDir` hint on the artifact wire contract (`@argent/registry` + `@argent/tools-client`). - `screen-recording-stop` tags its video with `saveDir: ".argent/recordings"` and drops the internal `argent-` temp prefix from the filename. - The client materializer honours `saveDir`: **copies** the file when the tool-server is co-located, **downloads** it over `/artifacts/:id` when it is remote. Either way the bytes end up in the durable directory. - `saveDir` resolves against the client's **project root** (nearest ancestor with `.git`/`package.json`/`.argent`), falling back to the global **`~/.argent`** when run outside a project — matching how the rest of argent scopes per-project config. Adds `findProjectRoot` to `configuration-core` (a null-returning variant of `resolveProjectRoot`) to drive that choice. - Hardened: absolute paths and `..` escapes are rejected, directory bundles excluded — a tool-server can't steer a write outside the base directory. ## Verification - Unit: 28 cases in `argent-tools-client/test/artifacts.test.ts` (co-located copy, remote download, project-root anchoring incl. from a subdirectory, global fallback, path-traversal rejection, integrity). configuration-core suite green (55). - E2E against a booted iOS 18.5 sim + a worktree tool-server, driving the real client materializer path: - in a project, co-located server → `<projectRoot>/.argent/recordings/…mp4` (valid h264) - in a project, forced remote download (`GET /artifacts/:id`) → same durable dir, temp cache left empty - outside a project (HOME redirected) → `~/.argent/recordings/…mp4` - Root `tsc --build`, prettier, and eslint all clean. MoQ / `sim-remote` recording support lands in a separate PR stacked on this one. --------- Co-authored-by: Ignacy Łątka <claude-hubert.gancarczyk@swmansion.com> |
||
|
|
f85d3843aa |
feat(screen-recording): draw touch gestures into the recording (#549)
## What
Draws the touch gestures argent drives directly into the screen
recording, so a captured video shows **where** each interaction landed
instead of a UI reacting to an invisible finger:
- a pulse marks each **tap**
- a fading comet trail follows a **swipe / drag**
- paired markers show the two contact points of a **pinch / rotate**
On by default; pass `showTouches: false` to `screen-recording-start` for
a raw, overlay-free capture.
## How
simulator-server already has a touch visualizer behind its `pointer`
cargo feature (the binary argent bundles ships with it). It renders the
markers into the very MJPEG stream this recording pipeline already
consumes — nothing is composited host-side, and it's driven by the touch
events argent already sends. This PR just turns it on for the life of a
recording:
- `screen-recording-start` builds a small `PointerControl` from the
resolved simulator-server handle and hands it to `startCapture`.
- The overlay is enabled right after the encoder goes live (so the first
interaction is already drawn) and **restored to off** when the capture
ends — stop, time-limit cap, encoder crash, or session teardown all run
the same disable path, so it never outlives the recording it was enabled
for.
- Two thin client helpers (`setPointerVisible`, `setPointerTrail`) POST
to simulator-server's `/api/pointer`. They're best-effort: a build
without the feature (or a remote sim) just yields a `warning` at stop
and a normal overlay-free video, never a failed recording.
## Verification
- **Unit** — `screen-recording.test.ts`: 49 pass, incl. 10 new covering
enable-at-start / disable at stop, cap, unexpected exit, and dispose;
the `pointerFailed` warning; the no-control path; and the `/api/pointer`
wire format (`{show}` / `{trail}`, ok/error/throw handling).
- **Static** — `tsc` (src + `tsconfig.test.json`), eslint, prettier,
full workspace build all clean.
- **E2E** — drove the real tool-server → the bundled (production)
simulator-server on an iOS 18.5 sim:
- `showTouches: true` + a slow swipe → a translucent pointer bubble is
rendered onto the screen at the touch point in the recording.
- identical swipe with `showTouches: false` → paging still happens, **no
bubble** — proving both the toggle and that stop restored the overlay to
off (the control clip never re-enables it, yet draws nothing).
---
Stacked on #548 (static-frame trimming) → base branch
`feat/screen-recording-trim`. Review after that merges.
---------
Co-authored-by: Ignacy Łątka <claude-hubert.gancarczyk@swmansion.com>
|
||
|
|
2251c7cf46 |
feat(screen-recording): screen-recording-start/stop tools with per-call stop reminders (#517)
## What Adds a `screen-recording-start` / `screen-recording-stop` tool pair (naming follows `native-profiler-start/stop`) that records the device screen to an mp4 and returns it as a downloadable artifact. Capture runs through **simulator-server** — the same backend `screenshot` and every interaction tool already drive. simulator-server publishes the device screen as an MJPEG stream (`/stream.mjpeg`), so recording needs no platform-specific recorder: the tool-server subscribes to that stream, paces the frames onto a fixed 30 fps timeline, and pipes them into a single ffmpeg process that encodes — and watermarks — straight to the final mp4. The stream is multi-subscriber, so a preview window can stay open on the same device while recording. - **One path for every device simulator-server drives** — iOS simulators, Android emulators, and physical Android devices. - **Native resolution, constant 30 fps** — h264, `+faststart`, no post-processing pass. - Session state lives in a per-device `ScreenRecordingSession` blueprint (mirrors the native-profiler session): time-limit cap with auto-finalize, unexpected-exit recovery (a capture that ended on its own is still retrievable via stop), always-startable state reset on stop, and dispose-on-shutdown cleanup through the registry. ## Why the frames are paced host-side A device only emits a frame when the screen **changes**. Pointing ffmpeg straight at the stream therefore produces a video whose length is the sum of the moments something moved — a still screen collapses to a fraction of a second — and an ffmpeg blocked on a silent socket does not answer a stop signal promptly either (measured: 12 s late). Re-emitting the last frame on a wall-clock schedule fixes both: **video duration always equals real elapsed time**, and stop is a stdin EOF that finalizes in ~60 ms. Duplicate frames cost almost nothing once encoded (a 10 s recording of a static screen is ~1 MB). ## Finishing pass folded into capture The Argent watermark (logo + `By @swmansion`, bottom-left, sized relative to the frame) is stamped by the same ffmpeg invocation that encodes the capture, so there is no second encode and no temp intermediate. Its colour is chosen **per pixel** from the background luma underneath it — the white logo over dark areas, a runtime-derived near-black copy over light areas — so it keeps maximum contrast against whatever is behind it. It sits at 20 % opacity and is opt-out via `argent disable video-watermark` (read when the recording starts). Frame geometry comes from the first JPEG's SOF header, so no `ffprobe` is needed. ffmpeg is now the recorder rather than an optional finisher, so a missing ffmpeg fails `screen-recording-start` up front with an install hint instead of silently degrading the output at stop time. ## Reminder plumbing While a recording is running — or ended on its own but was never retrieved — **every** tool result carries a `note` telling the agent the recording is still going and how to stop it. It rides the same `note` field as the update reminder but is per-call and unsuppressed; both notes compose when they coincide. The note text switches to "already ended, retrieve the file" once the cap fires or the capture dies. ## Skill New `argent-screen-recording` skill (+ routing stanza in `rules/argent.md`): documents the start → interact → stop workflow and instructs the agent to schedule its own reminder/wakeup for the expected end of the recording immediately after starting one (harness reminder tool if available, otherwise a background `sleep` whose completion notification pulls it back). Scores 10/10 on `scripts/grade-skills.mjs`. ## Verification - Unit: 45 tests across `screen-recording.test.ts`, `mjpeg-stream.test.ts`, `watermark.test.ts`, `http-recording-note.test.ts`; full tool-server suite **2808 tests / 280 files green**; `tsc --noEmit -p tsconfig.test.json`, root `tsc --build`, eslint, prettier all clean. - E2E on a live tool-server (`POST /tools/...`), driving real interactions between start and stop: - **iOS sim** (iPhone 16 Pro Max, iOS 18.5): 1320×2868 h264, `r_frame_rate=30/1`, 338 frames, container duration 11.27 s vs reported `durationMs` 11314 — **within 50 ms of wall clock**. Extracted frames show real screen content (mid app-switcher animation) with the watermark rendered light-over-dark. - **Android emulator** (Pixel 3a, API 34): 1080×2220 h264, 30 fps, 353 frames, duration 11.77 s vs `durationMs` 11793 — within 30 ms. 512 KB for a mostly-static home screen. - **Static screen** (no interaction at all, 10 s): 300 frames, duration exactly 10.000 s — the case the previous `recordVideo` implementation reduced to ~0.03 s. - **Long silence then activity** (15 s untouched, then an app launch): stream survived, no warning, frames after the silence show the new screen. This caught a real bug during review — the connect timeout was also acting as a socket *inactivity* timeout and tore the stream down at 10 s of stillness; fixed and covered by a regression test that fails without the fix. - **Time-limit cap**: `timeLimitSeconds: 5` self-finalized at 4.97 s / 149 frames, the per-call note flipped to "already ended (it hit its 5s time limit)", and a stop issued 11 s later still returned the video with the matching warning. ## Not supported (capability-gated with clear errors) tvOS simulators, physical iPhones, Chromium apps, Vega, and remote (`remote:`-prefixed) simulators — none of them expose a readable frame stream. --------- Co-authored-by: Ignacy Łątka <claude-hubert.gancarczyk@swmansion.com> |
||
|
|
1f5b155229 |
chore: bump workspace packages to 0.16.1 (#539)
## What Lockstep-bump all 16 `packages/*` workspace packages (and the matching `package-lock.json` entries) from `0.16.0` → **0.16.1**. ## Verification - `node scripts/check-workspace-versions.mjs` → `All workspace packages are at 0.16.1.` - Lockfile diff is exactly 16 version-field swaps — no unrelated churn (the `"peer": true` regeneration noise from a newer npm was again kept out, same as #513). The one remaining `0.16.0` in the lockfile is the unrelated `@humanfs/types` dev dependency. - `packages/argent-private` submodule left pinned at `v0.16.0` — no `v0.16.1` tag exists there yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
f0026ce6c8 |
fix(tool-server): stop native-devtools unix socket bind crashing the server (#531)
## Problem
The per-UDID native-devtools unix listener binds with a bare
`server.listen(socketPath)` and **no `error` listener**
(`native-devtools.ts`, the `else` branch of the transport check). A bind
failure — `EADDRINUSE` from a live/concurrent server on the same UDID,
or `EEXIST` from a socket re-created in the window after the pre-unlink
— fires an unhandled `'error'` event, which Node throws as an **uncaught
exception that crashes the whole tool-server at startup**.
The TCP branch right above it already guards its bind with
`server.once("error", reject)`; `ax-service.startListener` does too. The
unix branch was the one that didn't.
## Why it matters — 0.16.0 crash telemetry
The crash-diagnostics shipped in 0.16.0 (`crash_fingerprint` /
`crash_phase` / `error_syscall`) make this the clearly-dominant crash
source. Scoped to `toolserver:stop`, `reason=crash`,
`cli_version=0.16.0`:
- **100% of crashes are socket/fs E-codes**, none are logic bugs.
EADDRINUSE (95%), EEXIST (4%), ENOSPC/EPIPE tail.
- Fingerprint **`dc063fe2`** — the native-devtools socket bind site —
throws both **EADDRINUSE (65)** and **EEXIST (30)** at
**`crash_phase=startup`**, across **~14 distinct users**. That's the
widest-reach source by user count. (A separate single-user
`toolserver_bind` restart loop accounts for most raw *volume*, but only
one machine.)
EEXIST at this site is the known concurrent-agent contention on
`/tmp/argent-nd-<UDID>.sock` — a stale/racing socket file left by
another server.
## Fix
Extract the unix bind into `bindNativeDevtoolsUnixSocket(server,
socketPath)`, mirroring `ax-service.startListener`:
- Attaches an `error` listener so a bind failure **rejects** instead of
throwing uncaught → routes into the factory's existing retry +
failure-telemetry path, failing only this one service.
- On `EADDRINUSE`/`EEXIST`, **unlink the stale entry and retry the bind
once** — self-heals the unlink→listen race with a concurrent same-UDID
server.
- On any other failure (or a second failure), rejects with a coded
`FailureError` (`NATIVE_DEVTOOLS_SOCKET_BIND_FAILED`,
`failure_stage=native_devtools_socket_bind`) so it's cleanly
classifiable in telemetry going forward.
## Testing
- **Unit** (`native-devtools-socket-bind.test.ts`, 4 cases): free-path
bind, stale-file self-heal, coded-FailureError on unbindable path,
no-uncaught-throw when contending a live-held path.
- **Full telemetry + registry suites**: 368 pass (new failure code
accepted by the sanitizer allowlist).
- **Live A/B against built output** — a live server holds the socket,
then two child processes contend for it:
- OLD (pre-fix `server.listen(path)`): **exit 1, uncaught EADDRINUSE
crash** ← bug reproduced
- NEW (compiled `bindNativeDevtoolsUnixSocket`): **exit 0, self-healed
and bound** ← fix works
- **Bundled-artifact check**: ran the esbuild `bundle-tools` step; the
fix's retry logic and coded failure survive bundling intact in
`dist/tool-server.cjs` (no dead-code-elimination surprise).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
b50d51a2de |
chore: bump workspace packages to 0.16.0 (#513)
## What Lockstep-bump all 16 `packages/*` workspace packages (and the matching `package-lock.json` entries) from `0.15.0` → **0.16.0**. ## Why The `packages/argent-private` submodule is already pinned at **v0.16.0**, while the workspace packages sat at 0.15.0. This aligns the whole repo on the single 0.16.0 version. ## Verification - `node scripts/check-workspace-versions.mjs` → `All workspace packages are at 0.16.0.` - `git submodule status` → clean (no `+`), pinned at `v0.16.0`. - Lockfile diff is exactly 17 version-field swaps (no unrelated churn — a full `npm install --package-lock-only` regeneration was rejected because it introduced unrelated `"peer": true` churn from a newer npm). |
||
|
|
af8476c0a9 |
feat(vega): support the JS-runtime debugger and network inspector on Fire TV (#502)
Vega's React Native is a fork of **RN 0.72**, so it serves the *legacy* Hermes inspector rather than the post-0.73 (Fusebox) debugger. The `debugger-*` / `view-network-*` / `react-profiler-*` tools were never gated for it, so every call against a VVD was rejected before any RN code ran: ``` Tool 'view-network-logs' is not supported on vega vvd (no vega support declared). ``` That gate turned out to be the only thing in the way for most of them. The concern that prompted this was that the network inspector needs the new debugger. It does not: argent's network inspector never touches the CDP `Network` domain, it monkey-patches `fetch` over `Runtime.evaluate` and reads back a global array. `Runtime.evaluate` has worked in Hermes since long before 0.73, so the network inspector is precisely the tool that is *unaffected* by the missing new-debugger stack. ## What now works on Vega Gated in via `DEBUGGER_TOOL_CAPABILITY` (these need only `Runtime.evaluate`): `debugger-connect` · `debugger-status` · `debugger-evaluate` · `debugger-log-registry` · `view-network-logs` · `view-network-request-details` ## What stays gated off, and why `debugger-component-tree` · `debugger-inspect-element` · `debugger-reload-metro` · `react-profiler-*` · `profiler-*` Two different reasons, and it is worth keeping them apart: - **`debugger-component-tree` and `debugger-inspect-element` are hard-blocked.** They are the only two callers of `cdp.evaluateWithBinding`, i.e. they deliver their payload over `Runtime.addBinding` / `Runtime.bindingCalled`. RN 0.72's Hermes **ACKs `Runtime.addBinding` and never installs the binding** — confirmed on the VVD: after a successful `debugger-connect`, `typeof __argent_callback` is still `"undefined"`, and no `bindingCalled` ever fires. They would hang until timeout. - **`debugger-reload-metro` and the profiler tools are simply unverified here.** They do *not* use the binding (`reload-metro` is `Page.reload` plus an HTTP `/reload` fallback; the profilers ride `Runtime.evaluate` and the CDP `Profiler` domain). They share `RN_ONLY_TOOL_CAPABILITY` and stay out of scope rather than ship untested. Either way the capability gate turns a hang (or an unknown) into an immediate, explicit "not supported". `describe` remains the way to read on-screen structure on Vega. Worth noting for a follow-up: the component-tree data itself *is* reachable on RN 0.72 — walking the DevTools hook via a plain `Runtime.evaluate` returns the real fiber tree (640 fibers on the sample app). Component-tree is blocked by argent's transport choice, not by a missing runtime capability, so a non-binding fallback would unlock it. ## Two bugs this surfaced **A Vega `device_id` could bind to another device's runtime.** Vega's legacy inspector-proxy emits no `reactNative` block at all, so its targets carry no `logicalDeviceId`. `selectTarget` both matched `device_id` on `logicalDeviceId` *and* counted connected devices the same way — so a Vega device was invisible to the multi-device guard from #397: its id matched no target, the guard counted zero devices, concluded there was nothing to disambiguate, and fell through to the priority target. With a Fusebox iOS/Android app on the same Metro, that target wins the priority list, so `debugger-evaluate` would have run JS **in the wrong app** while `debugger-status` reported the other device's name and `isNewDebugger: true`. Devices are now keyed by `logicalDeviceId || deviceName` (the legacy proxy always reports a name), so the guard sees them and refuses to guess. A lone VVD still falls back to its own targets. This is not Vega-specific: any RN ≤ 0.72 device was invisible the same way. **Metro's missing project-root header.** RN 0.72's Metro never sends `X-React-Native-Project-Root`, and `discoverMetro` treated a missing header as fatal — which took down evaluate, console logs and the network inspector too. The header only feeds source-map / `file:line` resolution, which degrades cleanly to no-location, so it is now optional. Platform-agnostic, not a Vega special-case: a missing source-map hint should never kill an otherwise working debugger session. That made `projectRoot: ""` a reachable state, which two source-resolution paths mishandled. `symbolicate` stripped the root with `file.replace(projectRoot + "/", "")`, which on an empty root degrades to `.replace("/", "")` and deletes the first slash *anywhere* in the path (`src/screens/Home.tsx` → `srcscreens/Home.tsx`). And `readSourceFragment` did fail closed, but only because `fs/promises.realpath("")` happens to reject where the sync API returns the tool-server's cwd — it now bails explicitly instead of leaning on that quirk. Both are reachable from `debugger-inspect-element` on any legacy Metro (not on Vega, where it is gated off). ## Verification Driven end-to-end against a real VVD (`amazon-6b8a…`) running the `react-native-multi-tv-app-sample` Vega app (Debug `.vpkg`, Metro reachable via reverse port-forward), through a tool-server built from this branch: ``` debugger-status -> connected: true, deviceName: kepler-device, isNewDebugger: false, projectRoot: "" debugger-evaluate -> "hermes=object fetch=function global=object" debugger-log-registry -> captured console.log "PR502_E2E_MARKER" === NETWORK LOGS (page 1/1, 2 total) === {id: rn-net-1} "GET /todos/7" 200 Fetch 98 B 55 ms {id: rn-net-2} "GET /users/3" 200 Fetch 520 B 17 ms view-network-request-details -> full request/response headers, status, timing ``` And the gated tools fail fast (0.02s) rather than hanging: ``` debugger-component-tree -> not supported on vega vvd debugger-inspect-element -> not supported on vega vvd debugger-reload-metro -> not supported on vega vvd react-profiler-start -> not supported on vega vvd ``` Re-ran the whole sequence against the fixed target-selection to confirm the single-VVD path still connects. Also run locally: `tsc --build`, `tsc --noEmit -p tsconfig.test.json`, `eslint . --max-warnings 0`, `prettier --check`, `grade-skills`, and the tool-server unit tests (199 files / 1740 tests). Tests added: `vega-target-routing` (the mis-route, using a target payload captured verbatim from the live VVD), `source-resolver-empty-root` (both empty-root paths), and `vega-debugger-capability` — which now drives the real `ToolDefinition.capability` fields and `resolveDevice` instead of a hand-written name↔constant pairing, pinning all 6 enabled and all 14 gated tools. The old version passed even with `debugger-status` wired to the wrong capability constant. Scope note: VVD only — no tool declares `vega: { device: true }`, so physical Fire TV hardware is untouched. |
||
|
|
70660378bb |
feat(tool-server): type secrets via {{secret:NAME}} placeholders — plaintext never enters agent context (#506)
## Why
Feedback: unlike Stagehand's `act("type %pw% into password", {
variables: { pw } })`, argent had no way to type a credential without
the LLM seeing the plaintext — which disqualifies it as a CI agent. In
the MCP model the agent composes the `keyboard` call itself, so a
plaintext password lands in **six** places: the agent transcript,
`~/.argent/mcp-calls.log` (spyLog records full args + result), the
flag-gated tool-server event log, the `typed` result echo, recorded flow
YAMLs (committed to the repo), and the post-type auto-screenshot
(plaintext back into context as pixels for non-secure fields).
## What
`keyboard` and `paste` now accept **`{{secret:NAME}}`** placeholders in
`text`, resolved server-side from **`ARGENT_SECRET_NAME`** environment
variables:
```yaml
# GitHub Actions
env:
ARGENT_SECRET_APP_PASSWORD: ${{ secrets.APP_PASSWORD }}
```
```json
{ "udid": "<UDID>", "text": "{{secret:APP_PASSWORD}}", "key": "enter" }
```
**Resolution happens inside the tool's `execute`** — after every
existing logging boundary — so spyLog, the event log, the transcript,
and recorded flows all see only the placeholder, with near-zero extra
redaction work:
- `run-sequence` keyboard steps and flow `type` steps go through
`invokeSubTool` into the same `execute` → covered for free. A recorded
login flow stores the placeholder verbatim, so the committed YAML is
secret-free — and agent-less `argent flow run` (#429) resolves it in CI
from the job's env, no LLM in the loop at all.
- The mandatory `ARGENT_SECRET_` prefix is an allowlist by construction:
a prompt-injected agent can never exfiltrate `AWS_SECRET_ACCESS_KEY` or
`GITHUB_TOKEN` through this mechanism — only vars the user deliberately
exposed under the prefix resolve.
- `typed` echoes the placeholder, never the value.
- Backend errors are scrubbed at the resolution site (e.g. a failed
Android `input text` echoes the device-side command line containing the
secret) — message and stack, in place, preserving the error class and
with it the HTTP-status/telemetry mapping.
- Unknown placeholder → 400 listing the **names** of available secrets,
never values (`SECRET_PLACEHOLDER_UNKNOWN`).
- **Auto-screenshot skip:** the MCP layer deep-scans args for
`{{secret:` and replaces the after-action screenshot with a text note —
otherwise a non-secure-entry field hands the plaintext straight back to
the model as an image. Covers nested `run-sequence` steps too.
- Remote-routed setups resolve on the machine that types (the
tool-server's env) — documented in the skill.
- Inert unless a placeholder is used; no feature flag.
## Docs
- `keyboard` / `paste` tool descriptions
- `argent-device-interact` skill: "Typing secrets" section (incl.
*don't* `describe`/`screenshot` the field you just filled — submit
first)
- `argent-create-flow` skill: never record a real credential;
placeholder flows stay secret-free in the repo
## Known residual risks (shared with Stagehand-style designs)
- A prompt-injected agent could direct `{{secret:X}}` into an
attacker-visible field and screenshot it — inherent to any "agent types
secrets" design; the prefix allowlist and no-read-back keep the blast
radius to secrets deliberately exposed.
- `describe` can read back a non-secure field's value after typing; iOS
secure fields briefly show the last typed character; Android typing puts
the resolved string on the device-side `input` command line. Acceptable
for emulator CI; noted in the skill.
- `keys` in the result still reflects the real keystroke count (secret
length) — kept for debuggability.
## Tests
- `keyboard-secrets.test.ts`: resolution (embedded/repeated
placeholders, malformed shapes ignored), unknown-name error lists names
but never values, redaction preserves error class, full `execute`
round-trips on the chromium branch asserting the plaintext appears
nowhere in results or thrown errors, paste via mocked `sendCommand`.
- `auto-screenshot.test.ts`: `containsSecretPlaceholder` on
flat/nested/unserializable args (fails safe).
- Full suites green: tool-server 2550, argent-mcp 76, registry 87;
builds + `typecheck:tests` + eslint + prettier clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
fcf0ede624 |
feat(tool-server): settings-permissions tool to grant/deny/reset app permissions (#462)
## What Adds a `settings-permissions` MCP tool that edits an app's runtime permissions **out-of-band**, without navigating the system Settings UI. It's a test-setup / out-of-band tool — the default way to change a permission is still through the app (in-app toggle or the app-triggered dialog). Reach for this only when the app can't get there: - **pre-authorize** (`grant`) or **deny** a permission before the app asks, so no dialog interrupts the flow - **re-enable** one the user already denied (iOS never re-prompts once denied) - **reset** it so the first-run dialog reappears on next launch Supported permissions: camera, microphone, photos, contacts, notifications, calendar, location, location-always, media-library, motion, reminders. ## How **iOS simulator** — `xcrun simctl privacy grant|revoke|reset <service> [bundleId]`. - `notifications` has no simctl service → surfaced as an unsupported error. - An "invalid service" failure (e.g. `camera` on an Xcode that doesn't model it) is wrapped as unsupported with a hint to list supported services. - A shutdown simulator error carries a `boot-device` hint. **Android** — `pm grant` / `pm revoke` (plus `clear-permission-flags` on reset) over adb. - One abstract permission fans out to the concrete `android.permission.*` set (fine/coarse location, per-media reads); `location-always` grant also grants foreground location since background alone is unusable. - A `pm path` preflight rejects an uninstalled package (`pm grant`/`revoke` exit 0 for a missing package, which would otherwise report a false success). A transport-level adb failure is rethrown rather than mislabeled as "not installed" (`isTerminalAdbError` is now exported for this). - Partial results land in `skipped`; the action fails only when pm rejects **every** mapped permission. ## Supporting changes - New failure codes: `ANDROID_SETTINGS_PERMISSION_FAILED`, `IOS_SETTINGS_PERMISSION_FAILED`, `SETTINGS_PERMISSION_UNSUPPORTED`. - New `argent-settings-permissions` skill, plus cross-references from `argent-device-interact`, `argent-test-ui-flow`, and the argent rule (with a strong "prefer the app dialog over this tool" steer). - `bundleId` is regex-validated (same shell-injection guard as launch-app/restart-app) and schema-required for grant/deny. ## Testing `vitest` — **37/37 pass** (schema validation incl. shell-injection payloads, iOS branch, Android branch incl. partial/total pm failures and the preflight). `tsc --noEmit` clean for tool-server + registry + tests; prettier clean. --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> |
||
|
|
551c781fb0 |
feat(flow): support running end-to-end flows without agent (#429)
# Flow E2E
This PR turns flows from replayable tool-call recordings into a
declarative, portable e2e test system that runs headlessly via `argent
flow run <name>` — no LLM in the loop, non-zero exit on failure,
suitable for CI.
Agents can still replay flows via `flow-execute` and get the full output
from each step after it finishes.
## Flow format
Flows are YAML files in `.argent/flows/`, now built from **directives**
interpreted by the runner rather than raw tool calls:
- `launch` — terminate + relaunch the app under test (per-platform app
id map supported); its presence marks a flow as **e2e** (controls its
own start state) vs. a **fragment** (runs against current state,
declares an `executionPrerequisite`, composable via `run:` with
cycle/depth guards).
- `tap`, `type` (focus handshake + optional submit), `scroll-to`
(momentum-free, increments by half the container, `within:` for nested
scrollers), `await`/`assert` (`visible`/`exists`/`hidden`/`text`
conditions), `wait`, `snapshot`, `echo`, and raw `tool:` as escape hatch
— still fully supported, so previously recorded flows keep working
unchanged.
- **Selectors** are `{ text, identifier, role }`; a bare string is a
loose selector resolving identifier-first, then visible text. Any
directive failure hard-stops the flow; later steps report `skip`.
## Execution engine
- Flows store no device id — the runner binds a device at replay
(explicit `--device`/`--platform` or the single booted one).
- Selector resolution runs against **platform-unified UI trees**: iOS
native view hierarchy (full testID coverage, gated on native-devtools
connecting), Android full accessibility hierarchy (full resource-id
coverage), Chromium CDP DOM, and the Vega automation toolkit. A shared
flatten/text-hoisting pass lets `text` assertions read what an
identified container visibly shows. Vega is remote-driven, so touch
directives fail with guidance to use `tv-remote`/`keyboard`.
- Steps wait for the tree to **settle** (two consecutive identical
reads) before acting; `launch` additionally gates on platform
tree-source readiness instead of silently degrading.
## Visual snapshots
`snapshot` diffs a full-resolution screenshot against baselines in
`.argent/flows/__baselines__/<flow>/`, keyed by platform + resolution,
with configurable `maxMismatch` and `--update-baselines`. The status bar
is pinned for the whole run (iOS `simctl status_bar`, Android demo mode)
so clocks/battery don't drive diffs. Baseline/current/diff images are
returned as artifacts on failure (and the written baseline on first
capture/update); a clean pass carries none, keeping reports and agent
context lean.
## Recording
The record-live workflow (`flow-start-recording` → `flow-add-step` →
`flow-finish-recording`) now emits directives automatically: coordinate
taps are rewritten to portable selector `tap:` steps (selector captured
from the pre-tap tree), a leading `restart-app` becomes `launch:`,
`flow-execute` of a sibling fragment becomes `run:`, and device ids are
stripped. The skill instructs the agent to polish the saved YAML
afterwards (fold tap+keyboard into `type:`, swipes-to-reach into
`scroll-to:`, awaits into sugar form).
## Also included
- `argent flow run` / `argent flow list` CLI with per-step report
rendering, artifact materialization, and `--json` output.
- New structured failure codes for flow validation, device resolution,
composition, and devtools readiness.
- Extensive unit test coverage (tree adapters, selector matching,
scrolling, composition, type focus, snapshots, status bar).
|
||
|
|
a5c6730d05 |
feat(ios-profiler): attributable memory leaks via malloc_stack_logging (#351)
## What Opt-in `malloc_stack_logging` flag on `native-profiler-start` that makes iOS memory leaks **attributable** — they come back with a real responsible frame + library instead of `<Call stack limit reached>`. ## Why `native-profiler-start` attaches to the already-running app. iOS records malloc allocation backtraces only when `MallocStackLogging` is set **at process launch**, so an attached app has none: Instruments' Leaks scanner finds the leaked blocks but can't attribute them and emits the placeholder `<Call stack limit reached>` (that string is Instruments', not ours). ## How When `malloc_stack_logging: true`, the profiler cold-launches the app **under** xctrace instead of attaching: ``` xctrace record --template <Argent.tracetemplate> --device <udid> \ --env MallocStackLogging=1 --output <…> --no-prompt \ --launch -- <App.app> ``` - `.app` path resolved via `simctl get_app_container`; the running instance is terminated first for a clean cold start. - The existing Leaks export / parser / render path is reused **unchanged** — no parser changes. - Default behaviour is untouched: attach to the running app (no relaunch, no overhead). The flag is opt-in because it restarts the app (loses state) and adds allocator overhead — for leak-attribution passes, not CPU/hang work. - The report relabels unattributable leaks with a hint to re-run with `malloc_stack_logging: true`, instead of surfacing the raw placeholder. ## Evidence (real iPhone 16 sim, same scroll workload) Before (attach) — every leak row: ``` responsible-frame="<Call stack limit reached>" responsible-library="" ``` After (`--env MallocStackLogging=1 --launch`) — same Leaks export: | leaked-object | responsible-frame | library | |---|---|---| | Malloc 1008 B ×3 | `itanium_demangle::OutputBuffer::grow(...)` | libc++abi | | Malloc 48 B ×1 | `hermes::vm::JSTypedArrayBase::createBuffer(...)` | hermes | ## Tests - `test/ios-instruments/malloc-stack-logging.test.ts` — the malloc launch argv (`--env MallocStackLogging=1`, `--launch -- <app>` last, with and without `--notify-tracing-started`, `--no-prompt`) vs default `--attach`; terminate-first / get_app_container ordering; failed-start behavior; per-test `ARGENT_IOS_CAPTURE` scrubbing. - `test/ios-instruments/leak-attribution-render.test.ts` — unattributable vs attributed leak rendering, capture-mode-aware hints. - `test/ios-instruments/load-freshness.test.ts` — per-capture residue clearing on `profiler-load`, live/recovery-pending load refusals. - `test/ios-instruments/stop-recovery.test.ts` — capture-mode pairing at stop (clean and recovery paths). - `test/profiler-combined-leaks.test.ts`, `test/profiler-leak-stacks.test.ts`, `test/ios-leak-pipeline.test.ts` — shared caveat line, evidence-first hints, attribution-normalized grouping. - Full `test/ios-instruments/` suite green (9 files / 70 tests); full tool-server suite green; `tsc --noEmit` + `tsc -p tsconfig.test.json` clean. ## Notes E2E'd live twice during review (real iPhone 16 Pro / iPhone 17 Pro sims on Xcode 26.6): guard refusals fire before the app is touched, the `--device` cold launch's deadlock on degraded Xcode is confirmed (so the guard is warranted), and the malloc argv/terminate/relaunch behavior checked out on-device. Docs updated: `IOS_PROFILER_REFERENCE.md` and the `argent-native-profiler` skill. ## Review follow-ups (post-approval hardening) Later commits address the remaining review threads and several subsequent multi-lens review passes: - **Capture-mode is a real signal, end to end.** The session records the mode of the in-flight recording at start (`recordingMallocStackLogging`), pairs it with `exportedFiles` at stop (`mallocStackLogging`), and freezes it into `parsedData` at analyze/load — so the analyze report, the combined report, and the `leak_stacks` drill-down all name the capture mode of the data they actually render instead of inferring it (or mislabeling it after a newer capture starts). One shared `renderUnattributedLeaksNote()` keeps the wording identical across reports, and attribution evidence outranks the flag: a resolved frame proves the target ran under malloc stack logging however it was launched. - **`profiler-load` restores a clean slate.** Loading clears all per-capture residue (capture mode, `cpuFilterPid`, `wallClockStartMs`, `traceFile`) so a loaded trace is never analyzed with a previous live capture's PID filter, freshness anchor, or report path — and refuses to load while a recording is in flight or a crashed capture awaits `native-profiler-stop`'s partial-trace recovery, which the load would otherwise orphan. - **Failed starts are non-destructive.** Per-capture descriptors — and the recovery flags that gate `native-profiler-stop`'s partial-trace export — are stamped/reset only on a successful start, so a failed attempt leaves the previous capture's still-loaded exports fully described and a pending abnormal-end recovery intact. - **Guards keep session state coherent.** `profiler-load`, `native-profiler-analyze`, and `profiler-combined-report` all refuse while a recording is in flight or a crashed capture awaits recovery, via one shared `isCaptureInFlight`/`inFlightGuardMessage` helper whose message distinguishes recording vs 10-min-cap timeout vs unexpected exit (so the stated cause matches what `native-profiler-stop` reports next). Proceeding would otherwise wedge the session (load) or render one capture's frozen data under another's live fields (analyze/combined). - **The combined report anchors on a frozen clock.** The recording's start time is frozen into `parsedData` at analyze, and the iOS combined report anchors its hangs to that frozen value rather than the live session field — so a capture started after analyze can never shift a real hang↔commit correlation into "unmatched". Android keeps its live anchor (it re-derives hangs from the live trace). The combined-report guard's recovery advice routes through `native-profiler-analyze`, the only step that rebuilds `parsedData`. - **Leak grouping is attribution-normalized.** `aggregateLeaks` keys on `(objectType, frame-normalized-the-way-isLeakAttributed-matches)`, so mixed unattributed spellings (`Unknown`, the sentinel, whitespace variants) can't split one group per type. - **The malloc launch target is unambiguous.** Since the cold launch terminates the resolved app, `app_process` resolution prefers an exact `CFBundleExecutable` match and, when builds still tie (dev + prod sharing an executable/display name), refuses and points at the globally-unique `CFBundleIdentifier` — which it also accepts as an `app_process` — instead of killing whichever app the plist enumerates first. - **Report tables escape `|`.** Demangled C++ frames (`operator|` included) are real headline content under malloc mode; every free-form cell across the leak, CPU-hotspot, function caller/callee, and thread-breakdown tables — symbol *and* thread/queue-name columns — is escaped so a stray pipe can't shift the row's columns. - **Android start is non-destructive too.** `startNativeProfilerAndroid` now starts perfetto before mutating session state, mirroring iOS, so a failed start no longer burns a prior capped capture's pending partial-trace recovery. |
||
|
|
44b38d490e |
fix(tool-server): inject Android keyboard/button over adb input (#449) (#455)
## What & why
`keyboard` and `button` drive Android through the simulator-server's
USB-HID transport (`pressKey` / `cmd:"button"`). The guest only receives
those HID events when the AVD exposes a hardware keyboard (`hw.keyboard
= yes`) — the default, but CI / headless AVDs are routinely created with
`hw.keyboard = no` (and `hw.mainKeys = no`), where the guest **silently
drops** them. Because that transport is fire-and-forget, both tools
returned `{ typed }` / `{ pressed }` **success while injecting
nothing**.
This still affects current `main`: after the keyboard refactor,
`makeAndroidImpl` routes Android **TV** to `adb input` but Android
**phones/tablets** still to `typeSimulatorServer` (the HID path).
Fixes #449 (found during 0.13.0 RC E2E testing on a `hw.keyboard=no` AVD
— `argent-test-ci-repro`, the config CI uses).
## Change
Route Android **phone/tablet** key / text / button injection through
`adb shell input text` and `input keyevent` (new
`utils/android-input.ts`), the same on-device transport Android TV and
Vega already use:
- **`keyboard`** — `platforms/android.ts` now sends the non-TV branch to
a `typeAndroidPhone` helper (adb `input`); the Android-TV and iOS
branches are untouched.
- **`button`** — the Android branch presses via `injectAndroidKeycode`;
iOS keeps the simulator-server HID path.
`adb input` lands regardless of `hw.keyboard`, on emulators (any config)
and physical devices alike, and a non-zero exit surfaces as a **thrown
error** instead of a silent no-op — directly addressing the
fire-and-forget concern already called out in the `button` tool. Touch
gestures are unchanged.
## Verification
- **Unit tests — 11/11 pass** (rebased on current `main`): new
`keyboard-android.test.ts` (vocabulary parity vs `NAMED_KEYS`, `input
text` shell-quoting incl. spaces, `input keyevent` mapping, empty-text
no-op, newline rejection) + updated `button.test.ts` (Android `back` now
asserts `KEYCODE_BACK` over adb).
- **eslint** on the changed files: clean.
- **Live tool-server** on a `hw.keyboard=no` AVD: the `keyboard` tool
now issues `adb … shell input text '…'` (vs the RC's HID `pressKey`),
and a failed inject **raises an error** rather than a fake success. The
`adb input` path was independently observed typing/deleting on this
exact AVD during RC testing (where the HID path silently no-op'd).
- Note: a full `tsc --build` / whole-suite run couldn't be completed in
my environment (the `argent-private` submodule isn't checked out, which
breaks the npm workspace install, and `main` added upstream deps my base
lacked). `platforms/android.ts` mirrors the existing `platforms/ios.ts`
branch structure one-for-one, and the pre-rebase revision of this change
passed `tsc --build` + eslint. CI here is authoritative for the full
typecheck.
## Reviewer notes
- `input text` receives the string as one shell-quoted argv token, so
spaces are preserved verbatim; newlines are rejected (they'd truncate
the `input` line) — callers use `key: "enter"`.
- `keys` counts by codepoint, matching the tv/vega/simulator-server
backends.
|
||
|
|
b1cadd7cd6 |
fix(native-devtools): terminal state for non-injectable Apple system apps (#459)
Fixes #453 ## The bug For an app that can never be injected (an Apple built-in app such as `com.apple.Preferences`), `native-devtools-status` kept reporting `requiresRestart: true` / `nextLaunchWillBeInjected: true`, and the native-* feature tools (native-describe-screen, native-find-views, etc.) kept returning "restart-app then retry". Restarting never changes the state, so an autonomous agent loops `restart-app → retry` forever. There was no terminal "this app cannot be injected" signal. ## Why these apps can't inject Apple system/built-in apps (bundle ids under `com.apple.`) are platform binaries with library validation enabled; the simulator ignores `DYLD_INSERT_LIBRARIES` for them, so the Argent native devtools dylib can never load. This is a reliable, well-understood signal on the simulator. Third-party apps the user installs carry no such restriction and inject fine. So the fix treats bundle ids starting with `com.apple.` as non-injectable, via a general predicate rather than hardcoding the single bundle id from the issue. ## The fix - New pure predicate `isInjectableBundleId(bundleId)` in `blueprints/native-devtools.ts` returning `false` for `com.apple.*` bundle ids (case-sensitive prefix, matching how bundle ids are compared), with a doc comment explaining the library-validation rationale. - New failure code `NATIVE_DEVTOOLS_NOT_INJECTABLE` in `@argent/registry`. - `precheckNativeDevtools(api, udid, bundleId)` (the 3-arg overload) now throws a terminal `FailureError` with that code, *before* the `requiresAppRestart → restart_required` branch, when the bundle id is non-injectable. Because every native-* feature tool awaits this precheck and propagates the throw, all 7 tools now surface a hard, terminal error instead of an unbounded restart→retry instruction — no change needed in the individual tool files. The 2-arg overload (no bundleId), used by `native-devtools-status`, is unchanged and never throws (guarded by `bundleId !== undefined`). - `native-devtools-status` now *reports* the state instead of throwing: it adds an `injectable: boolean` field. For a non-injectable app it returns `injectable: false, requiresRestart: false, nextLaunchWillBeInjected: false` (with `envSetup`/`appRunning`/`connected` still measured), giving an agent that checks status first a terminal signal. The tool description documents the new field and that `injectable: false` is terminal (do not restart/retry; use the standard describe / screenshot / view-at-point tools). ## Verification - `npm run build` (root tsc): clean. - `vitest run` on `native-devtools-status.test.ts`: 13/13 passing. Added coverage for the predicate, the status terminal state for a `com.apple.*` app, and that the 3-arg precheck throws `NATIVE_DEVTOOLS_NOT_INJECTABLE` while the 2-arg overload does not throw for the same api. - Telemetry `sanitize.test.ts` (iterates every registered failure code): 49/49 passing — the new code is accepted. - Prettier + eslint (`--max-warnings 0`) clean on all touched files. Device E2E on an iOS simulator with `com.apple.Preferences` (confirming `native-devtools-status` returns `injectable: false` and the native-* tools return the terminal error rather than looping restart-app) is pending and will be run separately. --- ### Device verification (iOS 18.5 simulator, com.apple.Preferences) Reproduced before/after on a real simulator with a dev tool-server: **Before (origin/main):** `native-devtools-status` for `com.apple.Preferences` → `requiresRestart:true, nextLaunchWillBeInjected:true`; `native-describe-screen` → `restart_required`; after `restart-app` the status is identical (the infinite loop). **After (this branch):** - `native-devtools-status` → `{envSetup:true, appRunning:true, connected:false, requiresRestart:false, nextLaunchWillBeInjected:false, injectable:false}` — terminal. - `native-describe-screen` / `native-find-views` → terminal error: "com.apple.Preferences is an Apple system app ... can never be injected ... use the standard describe / screenshot / view-at-point tools instead." (no more restart→retry loop). **Regression (injectable third-party app `xyz.blueskyweb.app`):** `native-devtools-status` → `injectable:true` with the normal `requiresRestart:true`; `native-describe-screen` → the normal `restart_required` (not the terminal error). The predicate does not false-positive on normal apps. --- ### Review follow-ups (@hubgan on-device review) - **Recovery guidance no longer recommends `view-at-point`.** Both view-at-point tools run this same 3-arg precheck and re-throw `NATIVE_DEVTOOLS_NOT_INJECTABLE` for a `com.apple.*` bundle, so recommending them dead-ended the agent. All three surfaces (the precheck throw, the `describe` iOS fallback hint, and the `native-devtools-status` description) now share one verbatim "do not fall back to the native-* tools" warning (`NON_INJECTABLE_NATIVE_WARNING`) and recommend only `describe` and `screenshot`. ( |
||
|
|
10a78d0527 |
feat(file-inputs): add tar-upload kind for streaming remote file/directory uploads (#443)
## What A new `tar-upload` file-input kind so a tool that reads a local file or directory (e.g. `reinstall-app`'s app bundle) works when the tool-server runs on a different machine than the caller. ## Why `reinstall-app` assumes the bundle is on the tool-server's filesystem. That breaks with a cloud-hosted simulator — the bundle is on your machine, not the server's. The existing `file` kind inlines base64 and is capped at 32 MB, too small for a real `.app`/`.apk`; `tar-upload` streams instead. ## How When you're not linked to a remote server, an existing local path is used in place. When you are (e.g. `argent link`), the client tars the path, streams it to `POST /upload`, and the tool call carries `uploadId` plus a SHA-256 `contentHash`. The server always extracts from that upload — even if the same path already exists on disk — verifies the hash, and vets tar members before extracting. The client only uploads when the path is on its own machine (a line on stderr so you know it's working). Same command local or remote: ``` argent run reinstall-app --udid <udid> --bundleId <id> --appPath ./MyApp.app ``` ## Shared extraction Per review feedback, the tar pack/unpack logic is unified in a small `@argent/archive` package used by **both** directions — this upload path and the pre-existing artifact download (`streamDirectoryAsTarGz` / `downloadAndExtractArchive`). The shared extractor rejects path-traversal entries, symlinks whose target escapes the extract dir, and non-file/dir members (hardlinks, devices) — while still allowing a bundle's internal symlinks. This also hardens the artifact-download path, which previously extracted without those checks. ## Tests Client + server unit and HTTP round-trip tests: dir and file upload, skip-when-not-local, co-located no-op, hash required, path-traversal / escaping-symlink / hardlink rejection, sidecar handling, single-member resolution, `/upload` auth + size caps, and artifact download extraction. All suites pass. --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> |
||
|
|
a7775e4ba6 |
feat(tool-server): add tool-server event log (#367)
We want to display a nicely parsed and rendered log of events that happened over a span of an Argent session. Current log file is not designed to be machine-readable, so adding a JSONL-formatted log of events (gated by `tool-server-event-log`). I eventually want this 😋 https://x.com/j3nnings/status/2069072177584849192 When enabled, the tool server writes Bunyan-compatible records to `ARGENT_EVENT_LOG` or `~/.argent/tool-server-events.jsonl`. The log captures tool-server lifecycle events and registry lifecycle events, including tool invocations and others. To make messages nicer, added `interaction` object to tool definition. This allows tools to define a nicer message in log (print "Screenshot capture completed." instead of "Tool `screenshot` completed.") Adding these to the existing tools is a follow up PR matter I think. |
||
|
|
2a01e407b3 |
chore: release 0.15.0 (submodule + workspace bump) (#484)
## What - Bump the `packages/argent-private` submodule pointer from `v0.11.0-8-g2953e89` to **v0.15.0** (`41484e4`). The superproject pin had drifted ~4 releases behind the checked-out submodule. - Lockstep-bump all `packages/*` workspace packages from `0.14.0` → **0.15.0** (and the matching `package-lock.json` entries) to line up with the submodule. ## Why The submodule and the workspace were out of sync: the working tree had `argent-private` checked out at v0.15.0 while the superproject still pinned the v0.11 era, and the JS packages sat at 0.14.0. This adopts v0.15.0 as the single, consistent version across the repo. ## Verification - `node scripts/check-workspace-versions.mjs` → `All workspace packages are at 0.15.0.` - `git submodule status` → clean (no `+`), pinned at `v0.15.0`. - Lockfile diff is exactly 15 version-field swaps (no unrelated churn). |
||
|
|
7bd017a796 |
feat(installer): committable repo-local (--local) install mode (#428)
## Summary Adds an opt-in **committable / repo-local** install mode so a team can version Argent _with the repo_. `argent init --local` installs `@swmansion/argent` as a project **devDependency** and writes MCP configs that launch the project-local copy — so after `git clone && npm install`, every teammate has the same Argent version and MCP wiring with **no global install and no per-developer `argent init`**. **Global mode stays the default** and is unchanged; everything here is additive and backward-compatible. Motivation: the workflow some users already do by hand (#207). Supersedes the earlier, since-closed #232 — this version is Windows-safe (`node`, not the `.bin` shim), handles Yarn PnP, and closes the cross-install tool-server hazards rather than leaving them. ## What changed **`init`** - `--local` / `--global` flags + an interactive "How should argent be installed?" prompt. Mutually exclusive; non-interactive (`--yes`) defaults to **global**. - Local mode installs the devDependency with the package manager detected from the project's **lockfile** (run with `cwd = projectRoot`). - MCP entry points at the local copy: `node node_modules/@swmansion/argent/dist/cli.js mcp` (the bin path is derived from the installed `package.json` `bin` and existence-checked — forward slashes, committable, Windows-safe). Yarn PnP → `yarn argent mcp`; an `npx --no-install argent mcp` fallback is used only when the bin can't be resolved. - Records the mode in a committed `<root>/.argent/install.json`. - Drops global-only adapters (Windsurf/Hermes) in local mode; prints team-setup / commit guidance. **`update`** — detects the project's mode; in local mode reads the installed version from `node_modules` (not the global binary / npx cache) and bumps the **devDependency + lockfile**, then re-emits the local MCP command. **`uninstall`** — scoped to the project's mode: a local-mode uninstall removes the **local devDependency only and never the shared global install**, and prunes `.argent/install.json`. Global-mode behavior is unchanged. **Cross-install hardening (the gate that makes per-project pinning safe)** - `launcher`: a tool-server is reused only when its `bundlePath` matches the caller's — a repo-local pin never silently runs against (or kills) a different version's server. - `postinstall`: only stops a tool-server spawned from *this package's own* bundle, so a devDependency install can't tear down an unrelated editor session. **Telemetry** — adds an `install_mode` dimension to init/update/uninstall events. ## Testing - New unit tests: install-mode helpers + record (`utils`), mode-aware `getMcpEntry`/`resolveLocalCommandMode` incl. the opencode array form (`mcp-configs`), local-only uninstall guard (`uninstall`), and bundlePath-gated tool-server reuse (`launcher`). - All existing suites green (installer 346, tools-client 115, telemetry 191, registry 83, argent 13, tool-server 1621). Build + lint clean. - **Live end-to-end** in an isolated sandbox: `init --local` → real MCP stdio handshake serves **69 tools from `node_modules`**; `update --local` reads the local version; `uninstall --local` removes the local install and **leaves the global install intact**; `init --global` unchanged; flag conflict rejected. ## Notes / known limitations - Recommended on mainstream platforms (macOS, Linux x64, Windows x64) with prebuilt native binaries; Linux arm64 / Windows arm need a C/C++ toolchain to build `tree-sitter` on `npm install` (documented in the README). - Concurrent sessions of two *different* Argent versions on one machine each spawn their own tool-server; an orphaned one idle-times-out (no session is ever broken). 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
ef955e604d |
fix(telemetry): add missing failure codes (#420)
## Summary
Adds the missing **failure codes** across the tool-server and converts
raw
`throw new Error(...)` sites into classified `FailureError`s, so
operational
failures land in named telemetry buckets instead of falling through to
the
generic `REGISTRY_TOOL_FAILURE_UNCLASSIFIED` bucket.
- **39 new `FAILURE_CODES`** plus a new `"electron"` entry in
`FAILURE_COMMANDS`.
- Throw sites migrated to `FailureError`, each carrying
`error_code` + `failure_stage` + `failure_area` + `error_kind`, and
where
relevant `failure_command` / `network_failure` / subprocess metadata
(`failure_exit_code` / `failure_signal` / `failure_spawn_code`).
## Addressing review feedback
The review found that several proposed codes were thrown only on paths
that are
caught / reformatted before the telemetry boundary (a code only buckets
if its
`FailureError` propagates out of a registered tool's `execute`), so they
could
never bucket a real failure. Those are removed rather than left as dead
codes:
- **Dropped
`VEGA_PAGE_SOURCE_{FETCH_FAILED,HTTP_ERROR,RPC_ERROR,TIMEOUT,PARSE_FAILED}`**
- `describeVega` deliberately swallows every toolkit failure (empty tree
+
relaunch hint) except `MultipleVegaDevicesError`, so these never reach
`toolFailed`. The throw sites revert to plain `Error` (behaviour
unchanged).
- **Dropped `CHROMIUM_CLIPBOARD_FAILED`** (the chromium-server Express
route
reformats the error to a 500 body; the image-copy path has no production
caller) and **`CHROMIUM_BUTTON_UNSUPPORTED`** (the WS `button` handler
reformats it; the registered `button` tool is apple+android only).
- **Fixed the `CHROMIUM_CDP_INVALID_RESPONSE` gap** - a reachable
HTTP-200 with a
non-JSON body made `res.json()` reject with a raw `SyntaxError` that
escaped
`fetchJson` unclassified; it is now classified
`CHROMIUM_CDP_INVALID_RESPONSE`
(matching the non-2xx and missing-`webSocketDebuggerUrl` paths).
`AbortError`
still surfaces untouched.
- **Unified two codes that carried two `error_kind`s** - both
`REACT_PROFILER_NO_ACTIVE_SESSION` sites and both
`PROFILER_NATIVE_TRACE_MISSING`
sites now use `not_found` (a missing session / a trace file missing on
disk),
keeping telemetry keyed on `error_code` from splitting by platform.
- **Reverted an out-of-scope edit** - the in-flight Vega-boot coalescing
Map-key
separator is back to its original NUL byte.
- **Added `failure-classification.test.ts` coverage** - expanded from 7
to 17
pinned codes, reading each back exactly how telemetry does
(`getFailureSignal(err).error_code`) across the chromium CDP discovery
family
(incl. the malformed-200 regression guard), vega input, and
native-profiler
metadata / stack-query.
This branch also merges the latest `main`; the keyboard tool was
refactored on
`main` into per-platform modules + a `createKeyboardTool` factory, so
the
`KEYBOARD_KEY_UNSUPPORTED` / `KEYBOARD_CHARACTER_UNSUPPORTED`
classifications were
re-homed onto its new throw sites (`platforms/chromium.ts`,
`simulator-server-keys.ts`; the vega path keeps its classification in
`vega-input.ts`).
## What changed
By area (the 39 codes):
- **Chromium / CDP (16)** - discovery, input, viewport, storage, tabs,
describe,
and Electron boot: `CHROMIUM_CDP_UNREACHABLE`,
`CHROMIUM_CDP_NO_PAGE_TARGET`,
`CHROMIUM_CDP_INVALID_RESPONSE`, `CHROMIUM_PARAM_INVALID`,
`CHROMIUM_INPUT_INVALID`, `CHROMIUM_VIEWPORT_READ_FAILED`,
`CHROMIUM_SCREENSHOT_FAILED`, `CHROMIUM_STORAGE_EVAL_FAILED`,
`CHROMIUM_TAB_NOT_FOUND`, `CHROMIUM_TAB_OPEN_FAILED`,
`CHROMIUM_DESCRIBE_FAILED`,
`CHROMIUM_DEVICE_ID_INVALID`, `CHROMIUM_ELECTRON_APP_PATH_INVALID`,
`CHROMIUM_ELECTRON_SPAWN_FAILED`, `CHROMIUM_ELECTRON_CDP_TIMEOUT`,
`CHROMIUM_ELECTRON_EXITED_BEFORE_READY`.
- Unreachable debug ports are distinguished from reached-but-malformed
responses; the `undici`-wrapped `ECONN*` code is read off `err.cause`
and
mapped to a precise `network_failure` instead of always reporting
`connection_refused`.
- **Vega (8)** - device discovery/registration, boot timeouts, image
lookup,
input, and screenshot: `VEGA_DEVICE_NOT_FOUND`,
`VEGA_DEVICE_NOT_REGISTERED`,
`VEGA_MULTIPLE_DEVICES`, `VEGA_BOOT_TIMEOUT`, `VEGA_IMAGE_NOT_FOUND`,
`VEGA_ALREADY_RUNNING`, `VEGA_SCREENSHOT_FAILED`, `VEGA_TEXT_INVALID`.
- `MultipleVegaDevicesError` extends `FailureError` so callers that
re-throw it
(e.g. `describe`) surface a classified signal.
- **Native profiler (8)** - perfetto start failures (mirroring the iOS
xctrace
set), app-process detection, the metadata sidecar, and the shared
analyze
guard: `NATIVE_PROFILER_PERFETTO_PROCESS_ERROR`,
`NATIVE_PROFILER_PERFETTO_READY_TIMEOUT`,
`NATIVE_PROFILER_PERFETTO_READY_EXITED`,
`NATIVE_PROFILER_APP_PROCESS_NOT_FOUND`,
`NATIVE_PROFILER_NO_EXPORTED_TRACE`,
`NATIVE_PROFILER_TRACE_TEMPLATE_MISSING`,
`PROFILER_NATIVE_METADATA_MISSING`,
`PROFILER_NATIVE_METADATA_INVALID`.
- The iOS `analyze` guard moves off `PROFILER_NATIVE_TRACE_MISSING` onto
the
shared `NATIVE_PROFILER_NO_EXPORTED_TRACE` so the same logical failure
isn't
split by platform; `PROFILER_NATIVE_TRACE_MISSING` now means strictly a
trace
file missing on disk (and both of its sites use `error_kind:
not_found`).
- **Android / simulator / screenshot-diff (3)** -
`ANDROID_UIAUTOMATOR_CAPTURE_FAILED`, `SIMULATOR_SCREENSHOT_FAILED`,
`SCREENSHOT_DIFF_INPUT_INVALID`.
- **Flows / keyboard (4)** - `FLOW_PROJECT_ROOT_INVALID` (split from
`_REQUIRED`,
which now means strictly "no root set yet"), `FLOW_NAME_INVALID`,
`KEYBOARD_KEY_UNSUPPORTED`, `KEYBOARD_CHARACTER_UNSUPPORTED`.
`error_kind` is reserved consistently: `validation` for checks against
caller
input or a schema we own; `unknown` for malformed output from an
uncontrolled
source (renderer main world, toolkit RPC); `network` with a
`network_failure`
class for transport and reached-but-malformed responses; `not_found` for
a
missing resource / session / on-disk artifact.
## Testing
- `tsc --build` clean - `error_kind` / `failure_area` /
`failure_command` /
`network_failure` and every `FAILURE_CODES.X` are typed, so the compiler
enforces structural validity of every signal.
- Unit tests: registry **83/83**, tool-server green (the one red
`await-ui-element` case is a pre-existing local-only timing flake
unrelated to
this change - it fails identically on `main` and passes in CI).
- `failure-classification.test.ts` pins **17** codes, reading each back
via
`getFailureSignal(err).error_code`; the `CHROMIUM_CDP_INVALID_RESPONSE`
malformed-200 fix is reproduced against a local HTTP server.
- `prettier --check` and `eslint` clean on all changed files.
## Notes
- No new codes are orphaned - every added `FAILURE_CODES` entry has at
least one
throw site that can reach the telemetry boundary.
- A handful of internal-invariant throws (SQL-template/identifier
guards,
Tesseract-TSV parsing, unreachable `default:` branches) are
intentionally left
as plain `Error`s - they represent programmer/tooling errors, not
user-reachable failure modes, so they don't warrant telemetry codes.
|
||
|
|
a2ef316cf1 |
fix(registry): auto-recover a wedged simulator-server on connection-refused (#463)
## Problem A user reported that argent "worked once, then every call timed out" — an hour of `this is taking longer than expected` / `request timed out`. Their logs showed the tell: dozens of ``` Screenshot failed: cannot connect to simulator-server (connection refused at http://127.0.0.1:<port>) … ECONNREFUSED ``` against a single stale port, starting right after `Simulator state changed to 4 (not booted)`. **Root cause — liveness ≠ readiness.** When a simulator is un-booted (or the native server crashes), the `simulator-server` child process can stay **alive** while its API socket stops **listening**. The registry only tears a service down on the child's `exit` event, so the dead-but-cached handle is reused on every subsequent call → `ECONNREFUSED` forever — *even after the simulator is booted again*. The only escape was a manual `stop-simulator-server`, which no ordinary user discovers. ## Fix A generic, opt-in recovery hook on the service registry: **`ServiceBlueprint.recoverable(error)`**. After a tool fails, the registry asks each service that tool resolved whether the error means its instance is dead; any that say yes are disposed (killing the wedged process) and the tool is **retried once** against a freshly re-created instance. Bounded to a single retry so a genuinely broken service can't spin. The registry stays generic — it learns nothing about simulators. The **simulator-server blueprint** implements `recoverable` for **connection-refused only**: - `ECONNREFUSED` proves the request never reached the server, so respawning + retrying can't double-apply a gesture. - **Timeouts and resets are deliberately excluded** — the request there may have taken effect, and a hung-but-listening server is a different failure that respawning wouldn't fix. ## Verification Reproduced live on a real simulator (un-boot → re-boot mid-session), same harness for both arms: | | Result | |---|---| | **Fix enabled** | second screenshot succeeds automatically in ~2.8s (fresh sim-server respawned) — `RECOVERED` | | **Fix disabled** (1-line toggle) | `connection refused at :<port>` persists through the reboot — `WEDGED`, the reported symptom | Automated: - **Registry** (`registry-service-recovery.test.ts`) — retry-once success, no-retry on non-recoverable errors, single-retry cap (no infinite respawn), and targeted disposal (only the service that reports the error is torn down). - **Blueprint** (`simulator-server-blueprint.test.ts`) — recovers on ECONNREFUSED; does **not** on reset / timeout / unrelated errors. - Full suites green: registry 87, tool-server 1846. Lint + prettier clean. ## Files - `packages/registry/src/types.ts` — add optional `recoverable?(error)` to `ServiceBlueprint`. - `packages/registry/src/registry.ts` — `invokeTool` disposes recoverable services and retries once. - `packages/tool-server/src/blueprints/simulator-server.ts` — implement `recoverable` (connection-refused only). - Tests for both layers. |
||
|
|
c5b9857fa5 |
chore: bump all workspace packages to 0.14.0 (#454)
Bumps every package under `packages/*` from 0.13.0 → 0.14.0 in lockstep. - All 14 workspace packages bumped to `0.14.0`. - `package-lock.json` regenerated to match (also picks up npm's `"peer": true` normalization). - `npm run check:versions` passes: *All workspace packages are at 0.14.0.* Internal deps use `file:../` references, so no dependency version specifiers needed changing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
a92c859d07 |
feat(argent-lens): argent lens — bind a claude session to the preview window (#416)
## What Adds `argent lens` — open Argent Lens bound to a fresh `claude` session. ## Why The Lens preview window and the agent communicate through a shared, process-wide tool-server that carries **no per-agent identity** (only a coarse telemetry header). So when the user requests changes in the window, there's no reliable way to route them to the *specific* agent that opened it. `argent lens` sidesteps the problem by inverting ownership: the human launches Lens, and Lens spawns (and therefore owns) exactly one `claude` terminal. The binding is 1:1 by construction — no identity plumbing required. ## How it works 1. `argent lens` ensures a tool-server and **begins a CLI Lens session**, which opens the preview window up front (no `await_user_selection` needed) and keeps it open across rounds. 2. It spawns a tracked `claude` terminal in the current directory — a TypeScript port of the macOS applet's `osascript` spawn/track mechanism — seeded to propose variants without blocking. 3. A watcher polls `GET /preview/outcome`; on each submitted round it types a one-line feedback summary into that terminal, queuing it to the agent as its next prompt. Requesting changes therefore "talks back" to the bound agent instead of closing the window. macOS only (drives Terminal/iTerm via `osascript`); off-darwin it exits with a clear message. ## Changes - **CLI** `lens-terminal.ts` — `osascript` spawn/track + a new write-into-running-session path (the "outputting" channel; injects input into the running TUI, which a tty write cannot do). - **CLI** `lens.ts` — the command + feedback formatter. - **tool-server** — a `cliSession` flag in the variant store (opens the window, suppresses auto-close on submit); `POST /preview/cli-session` and `GET /preview/outcome` routes. - **UI** — relabels the submit action to "Request changes" while a CLI session is active. Behind the existing `argent-lens` feature flag. ## Verification - Build + typecheck across the workspace; ESLint (`--max-warnings 0`) + Prettier clean. - 34 new unit tests; full `@argent/cli` suite **118 passing**, tool-server variant/preview suites **46 passing**. - **E2E (observed on this machine):** - **tool-server HTTP** — `cli-session` toggles `cliSession` true/false; submitted feedback round-trips through `/preview/outcome`. - **terminal injection** — spawned a window running `cat`, confirmed `writeToSession` lands text in the running program's **stdin** (both iTerm and Terminal), with embedded newlines flattened. - **full relay** — the real `watchAndRelay` loop against a real tool-server typed two distinct submission rounds' feedback into a stand-in agent terminal (dedup on `completedAt` verified). - `argent lens --help` and the feature-flag gate (exit 1 with guidance, no spawn). Note: a real end-to-end with a live `claude` agent was not run (it would drive a paid interactive session); the agent-facing channel is proven by injecting into a stand-in process's stdin via the exact same code path. --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> |
||
|
|
20f5a1282c |
feat: initial sim-remote support (#291)
## Summary
Adds **initial support for remote iOS simulators** ("sim-remote") —
simulators that run in the cloud (radon-cloud orchestrator) and are
driven from the dev's machine over a QUIC/MoQ tunnel, alongside the
existing local iOS simulators. A remote sim is exposed as a new device
platform, `ios-remote`, and the existing
interaction/inspection/profiling tools work against it through the same
tool surface.
To get there without scattering `isRemote` branches across every tool,
the iOS code paths are refactored behind a small set of
host/transport/backend abstractions so each tool stays
transport-agnostic.
## How it works
- **Discovery** — `list-devices` enumerates remote sims via the
`sim-remote` CLI in addition to local `simctl`. They classify as
`platform: "ios-remote"`. Enumeration is best-effort: if `sim-remote`
isn't installed or the user isn't logged in, it silently returns none.
- **Transport** — remote interaction goes over a MoQ relay
(WebTransport/QUIC). `moq-client.ts` subscribes to the server-published
`simulator` broadcast (catalog/video/screenshot) and publishes a
`control` track carrying protobuf-encoded input commands (touch / key /
button / rotate / wheel / screenshot). WebTransport is provided in Node
by `@fails-components/webtransport`, polyfilled lazily on first use.
- **Native devtools over the tunnel** — unix sockets don't bridge the
QUIC tunnel, so on remote sims `ax-service` and `native-devtools` are
forced onto TCP with OS-assigned ephemeral ports, and a reverse proxy is
wired before the in-sim daemon dials back.
## Key components
New (`packages/tool-server/src/`):
- `utils/sim-remote.ts` — thin wrapper around the `sim-remote` CLI
(simctl verbs, `moq-info`, proxy), propagating CLI stderr verbatim.
- `utils/moq-client.ts` — pure-JS MoQ client for the remote
simulator-server relay.
- `utils/datachannel-proto.ts` + `proto/datachannel.proto` — vendored
protobuf schema + encoders for the control channel.
- `utils/ios-host.ts` — `IosHost` abstraction (`localIosHost` /
`remoteIosHost`, picked by `pickIosHost(device)`) capturing the
local-vs-remote differences (transport, env setup, proxying,
`requiresTcp`).
- `utils/simctl-backend.ts` — strategy for simctl verbs so one iOS
handler serves both `xcrun simctl` (local) and `sim-remote simctl`
(remote).
- `utils/ax-prefs.ts` — AX-pref helpers extracted from `ax-service`.
-
`tools/{launch-app,open-url,reinstall-app,restart-app}/platforms/ios-remote.ts`
+ shared handlers.
Refactors:
- **Strategy pattern** to limit `isRemote`/platform branching inside
tool and blueprint implementations.
- `simulator-client.ts` gains a transport abstraction
(`SimulatorServerTransport`) so call sites stay transport-agnostic;
local sims back it with WebSocket+HTTP, remote sims with the MoQ client.
- `ax-service` / `native-devtools` blueprints updated for ephemeral TCP
ports and the `ios-remote` platform.
- Dependency checks: tools that pick a platform path internally now gate
on the right CLI (`sim-remote` vs `xcrun`).
## New dependencies
`@moq/lite`, `@moq/net`, `@fails-components/webtransport`,
`@fails-components/webtransport-transport-http3-quiche`, `protobufjs`.
## Testing
- Existing unit suite passes (CI green); the native-devtools
status/describe tests now stub the platform dep-check so they exercise
their logic, not the host toolchain.
- E2E device-smoke jobs (local iOS sim, Android, Chromium, remote VVD)
pass.
---
<sub>Branch rebased onto latest `main` (linear history). `sim-remote` is
opt-in — only required when driving a remote iOS simulator.</sub>
|
||
|
|
b8afbed183 |
feat(tv): Apple TV (tvOS) and Android TV support (#374)
Adds end-to-end support for driving **Apple TV (tvOS)** and **Android TV (leanback)** targets through argent, using a focus-driven interaction model (TVs have no touchscreen — interaction is moving a focus highlight with the remote). This PR was reworked after review to **reuse the existing cross-platform tool surface and the `tv-remote` tool from the Vega/Fire TV work (#366)** rather than ship a separate family of `tv-*` tools. ## What this delivers TV targets are driven by the **same cross-platform tools as every other platform**, routed through the existing `dispatchByPlatform` fork mechanism — there are no dedicated `tv-describe`/`tv-navigate`/`tv-type`/`tv-set-focus` tools: - **`describe`** — on a `runtimeKind: "tv"` target, returns the focus-driven view (focused + focusable elements) instead of a tap tree. On Android TV it auto-falls-back to the full `uiautomator` tree when the RN focus engine exposes nothing. - **`tv-remote`** — the cross-platform remote/D-pad tool from #366, now extended with `ios` (Apple TV HID daemon) and `android` (Android TV adb keyevents) branches alongside the existing `vega` branch. Single button, a `repeat`, or a whole path (`["up","right","select"]`) in one call. - **`keyboard`** — types into the focused field on a TV target (named keys are rejected there — they're navigation, which belongs to `tv-remote`). - **`button`** stays hardware-only (phones/tablets); it is not a TV tool. **Apple TV** runs two native daemons (in-sim AX service + host-side HID daemon), shipped via the `argent-private` submodule. **Android TV** reuses the same tool surface, adb-backed (`input keyevent`, `uiautomator dump`, `input text`). ## Remote vocabulary parity `tv-remote` exposes the full 16-button vocabulary (`up`/`down`/`left`/`right`/`select`/`back`/`home`/`menu`/`playPause`/`rewind`/`fastForward`/`next`/`previous`/`volumeUp`/`volumeDown`/`mute`): | | Apple TV | Android TV | Vega | |---|---|---|---| | D-pad / select / back / menu / home / playPause | ✅ | ✅ | ✅ | | media-transport + volume/mute | ❌ rejected | ✅ | ✅ | Media-transport/volume keys **genuinely work on Android TV** (real keycodes — verified live: `volumeUp` moved `STREAM_MUSIC`, `mute` toggled per `dumpsys audio`). On the **Apple TV simulator** they are **rejected with a clear error**: on-device testing confirmed the tvOS sim's HID stack silently drops Consumer-Control events, so returning success would be a lie. ## Key design points - **tvOS UDIDs are UUID-shaped and indistinguishable from iOS by shape.** Handled with `runtimeKind` detection (tvOS runtime string for Apple TV; `pm list features` leanback/television for Android TV — *not* `ro.build.characteristics`, which lies on TV emulators). - Converted launch-app / restart-app / screenshot / screenshot-diff / run-sequence from **eager** to **lazy** service resolution, so a tvOS target never spins up (and hangs on) the iOS-only simulator-server / native-devtools blueprints. - Native injection selects the platform-matched (TVOSSIMULATOR) dylib slice; daemons recycle across sim reboots and app relaunches so injection and focus survive. - SKILL.md footprint minimized per review: the two TV skills were collapsed into a single lean **`argent-tv-interact`** (~40 lines). ## Verification - `npm run build`, ESLint, Prettier, and the full tool-server suite (**1565 tests**) all pass; CI green (incl. the Apple TV / Android TV / Vega e2e jobs). - Core flows verified live against real apps on both an Apple TV 4K simulator and a Google ATV emulator. ## Dependency / merge order Pins the `argent-private` submodule to the tip of its `feat/tv-support` branch (software-mansion/argent-private#20). **Merge that PR to `main` first, then bump the submodule pointer here to the merged SHA before merging this PR.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fa5af59931 |
refactor: replace hand-rolled parsers with standard packages (#373)
## What & why
Swept the repo for hand-rolled regex/string parsers that reimplement a
solved problem, and replaced the genuinely fragile ones with
battle-tested packages (or deps already in the tree). Each swap is
paired with a regression test for an input the old code got wrong.
| Site | Was | Now |
|------|-----|-----|
| `adb.ts` `resolveAvdPath`, `linux-preflight.ts` `readMb` | per-key
anchored regex over AVD `.ini` / `config.ini` | **`ini`** |
| `workspace-reader.ts` `extractEnvKeys` | split-on-first-`=` line loop
| **`dotenv`** (`parse`) |
| `uninstall.ts` `readBundledSkillName` | `^name:(.+)$` capture +
outer-quote strip | **`yaml`** (already a dep) |
| `mcp-configs.ts` `escapeRegExp` | hand-rolled char-class escape |
**`escape-string-regexp`** |
| `export.ts` xctrace version, `session-context.ts` RN minor |
`match(/(\d+)\./)` / `split(".")[1]` | **`semver`** `coerce` (already a
dep) |
| `source-resolver.ts` `parseDebugStack` | `at fn (file:l:c)` regex
(required both line+col) | **`stacktrace-parser`** |
### Bugs these fix (covered by new tests)
- **ini**: a quoted `path = "/Users/My Name/.../X.avd"` kept its quotes,
failed the `startsWith("/")` guard → AVD unresolved. Inline comments /
trailing junk after a sizing value made the anchored regex return `null`
→ sizing warning silently lost.
- **dotenv**: `export FOO=bar` produced the key `"export FOO"` (dropped
by the identifier filter); a multi-line quoted value leaked its
continuation lines (`FAKE=...`) as phantom keys.
- **yaml**: a `name:` value with a trailing `# comment` was taken
verbatim (comment included); odd quoting only had one outer pair
stripped.
- **stacktrace-parser**: a `:line`-only frame (no column, as Hermes/JSC
can emit) became an unusable `unknown` frame.
## Deliberately left alone (not "flaky", or no safe swap)
To avoid churn and regressions, I did **not** touch:
- **`uiautomator-parser.ts`** hand-rolled XML parser — a deliberate,
documented choice with a dedicated hardening suite, including a
*leniency* test for malformed dumps (stray `</node>`). A spec XML parser
would reject inputs this is built to recover from.
- **`ios-profiler/pipeline/xml-parser.ts`** (xctrace XML) — the most
tangled parser, but it has no real `.trace` fixtures and only thin
entity-decode tests. A rewrite can't be proven equivalent without
device-generated traces, so swapping it blind risks silently breaking
iOS CPU/hang/leak reports. Best done separately with a captured fixture.
- **`http.ts` `extractHostname`** — DNS-rebinding guard; `URL.hostname`
keeps the IPv6 brackets the loopback check compares without, so the
"fix" isn't simpler and the path is security-sensitive.
- **`adb.ts` `shellQuote`** — the POSIX single-quote escape is the
standard correct form.
- **`parseAdbDevices`, `android-screen.ts` `wm size`, `detect-app.ts`
dumpsys** — tool-specific CLI-output scraping with no clean package
equivalent (and documented edge-case handling).
- **`workspace-reader.ts` `extractMetroPort`** — a best-effort heuristic
over JS config; a correct replacement needs an AST parse, out of scope
here.
- **`config-parse.ts` Yarn/npm age-gate parsing** — a precise,
unit-tested domain parser; `ms` would introduce a bare-number ambiguity.
## Verification
- `tsc --build` clean across the workspace graph.
- Tests: tool-server **1205**, installer **264**, update-core **31** —
all pass (incl. the new regression tests).
- `prettier --check` clean on all changed files.
- Production esbuild bundle (`bundle-tools.cjs`) emits a valid
`tool-server.cjs`/`mcp-server.mjs` with
`ini`/`dotenv`/`stacktrace-parser` inlined (`node --check` OK). *(The
script's final step errored only on the auth-gated
`trace-processor.wasm` download, unrelated to bundling.)*
New runtime deps: `ini`, `dotenv`, `stacktrace-parser` (tool-server),
`escape-string-regexp` (installer) — all small, pure-JS. Lockfile change
is purely additive.
|
||
|
|
270c9a158a |
chore: bump to 0.13.0 (#412)
as per title |
||
|
|
0edefebe06 |
fix(telemetry): orchestator sub-tools invocations was missing ai_client and platform attributes (#400)
## Summary Fixes telemetry attribution for tools dispatched by orchestrators. `run-sequence`, `flow-execute`, and `flow-add-step` run their steps via `registry.invokeTool()` directly, which minted a fresh, unregistered invocation id per nested call — so the `ai_client` / `platform` the HTTP layer captured for the outer request never reached the nested gestures, and they were recorded as **anonymous** in telemetry. Remove `opt_out` event that is a leftover. ## Changes - Add an optional `recordChildInvocation` recorder to `InvokeToolOptions` / `ToolContext`, bound by the HTTP layer to the outer request's attribution. - New `invokeSubTool` helper: registers each sub-tool under a minted id inheriting the request's AI client, with the platform re-derived from the sub-tool's **own** device arg (`udid` / `device_id` / `avdName`); forwards the recorder so propagation survives arbitrary nesting (e.g. `flow-execute → run-sequence → gesture-tap`). - Route the three orchestrators' sub-dispatch through - Refactor platform inference in the HTTP layer into a shared `platformFromArgs` helper (also hardened to not throw on a malformed device arg). - Remove opt_out event leftover Why re-derive platform per child: an orchestrator like `flow-execute` has no platform of its own and a single flow can target several devices, so the child's device arg is the only correct platform source; the parent's platform is the fallback. |
||
|
|
0bed3624a0 |
feat(vega): Amazon Vega (Fire TV) platform support (#366)
## Summary
Adds **Vega (Amazon Fire TV)** as a third Argent platform alongside iOS
and Android. Vega apps are React Native 0.72 + Hermes running on a QEMU
**Virtual Device (VVD)**, driven by a **D-pad tv-remote rather than
touch**. This PR extends Argent's platform abstraction so the existing
tool surface gates correctly on `vega`, and adds the Vega-specific tools
needed to actually drive a Fire TV app end to end.
### Platform abstraction
- `Platform` gains `vega`; `DeviceKind` gains `vvd` (the QEMU VVD);
`ToolCapability` gains a `vega { vvd, device }` block; `ToolDependency`
gains `vega`.
- `assertSupported()` selects the `vega` capability matrix for Vega
devices; `dispatchByPlatform()` gains an optional `vega` branch, and a
Vega dispatch with no branch throws `NotImplementedOnPlatformError`
(501).
- New `vega-cli` util (modeled on `adb.ts`) resolves `vega`/`kepler`
from `PATH` or `~/vega/bin` with the same hardened exec/quoting;
`check-deps` resolves and hints the `vega` dependency.
- The `simulator-server` blueprint is never wired for `vega` (Vega uses
`adb` + the `vega`/`kepler` CLI, not the proprietary binary).
### How Vega is wired
- **Input / screenshot / describe** go over `adb` (input via
`inputd-cli`, screenshots host-side via the Android emulator console,
element tree via the on-device automation toolkit).
- **App lifecycle** goes over the `vega`/`kepler` CLI.
- v1 is **Virtual-Device-only**: every Vega capability is declared
`vvd`. Physical Fire TV is out of scope and untested — the tool-server
does not connect to or detect hardware, so `resolveDevice` classifies
every `amazon-` serial as a VVD by shape. A physical device is therefore
not classified as `device` and not rejected at the capability gate;
supporting and gating real hardware is deferred to a version where it
can be tested.
- Vega runs **React Native 0.72**, which predates the new React Native
DevTools. The CDP/Hermes debugger surface (`debugger-*`, React/native
profilers) depends on integration points the new DevTools provides, so
**debugging and profiling are out of scope for this PR** — Vega
profiling/crash analysis is handled by the
`amazon-devices-buildertools-mcp` server instead.
### Also includes
- `argent-vega` skill documenting the full Vega workflow (target
discovery, the describe → compute-path → `tv-remote` navigation loop,
text injection).
- Vega VVD end-to-end CI workflow.
## Feature support by platform
Legend: ✅ supported · ❌ not supported · ⚠️ degraded / conditional · —
not applicable
| Feature (tool) | iOS | Android | Chromium | **Vega (VVD)** | Vega
notes |
|---|:---:|:---:|:---:|:---:|---|
| **Device discovery** (`list-devices`) | ✅ | ✅ | ✅ | **✅** | Tagged
`platform:"vega"`, `kind:"vvd"`; pass `serial` as `udid` |
| **Boot / start device** | ✅ | ✅ | — | ✅ | |
| **Launch app** (`launch-app`) | ✅ | ✅ | ✅ | **✅** | `bundleId` =
interactive-component app id from `manifest.toml` |
| **Restart app** (`restart-app`) | ✅ | ✅ | ✅ | **✅** | Terminate +
relaunch |
| **Reinstall app** (`reinstall-app`) | ✅ | ✅ | ❌ | **✅** | Uninstall +
install a `.vpkg` |
| **Screenshot** (`screenshot`) | ✅ | ✅ | ✅ | **✅** | Host-side via
emulator console (needs `adb`); rotation ignored (fixed landscape) |
| **Describe / element tree** (`describe`) | ✅ | ✅ | ✅ | **✅** |
On-device automation toolkit; surfaces `[focused]`/`[selected]` +
normalized frames |
| **Touch gestures** (`gesture-tap`/`swipe`/`scroll`/`drag`/`custom`) |
✅ | ✅ | ✅ | **❌** | Vega is tv-remote-driven; gestures error — use
`tv-remote` |
| **Pinch / rotate gesture** (`gesture-pinch`/`gesture-rotate`) | ✅ | ❌
| ⚠️ | **❌** | N/A on a TV |
| **Remote / D-pad** (`tv-remote`) | — | — | — | **✅** | Vega-only tool;
single key, repeat, or a whole path in one call |
| **Hardware buttons** (`button`) | ✅ | ✅ | — | **❌** | Remote keys live
in `remote` instead |
| **Text input** (`keyboard`) | ✅ | ✅ | ✅ | **✅** | Whole-string
injection via `inputd-cli send_text` |
| **Rotate orientation** (`rotate`) | ✅ | ✅ | — | **❌** | TV framebuffer
is fixed landscape |
| **Open URL / deep link** (`open-url`) | ✅ | ✅ | ✅ | **❌** | Not wired
on Vega |
| **JS debugger** (`debugger-*`) | ✅ | ✅ | ✅ | **❌** | RN 0.72 predates
the new RN DevTools — out of scope for this PR |
| **Native devtools / AX** (`native-describe-screen`, etc.) | ✅ | ❌ | —
| **❌** | iOS-only |
| **Native profiler** (`native-profiler-*`) | ✅ | ✅ | — | **❌** | Use
`amazon-devices-buildertools-mcp` (Perfetto) instead |
| **React profiler** (`react-profiler-*`) | ✅ | ✅ | — | **❌** | Out of
scope; RN 0.72 / new RN DevTools not supported |
| **Screenshot diff** (`screenshot-diff`) | ✅ | ✅ | ✅ | **✅** |
Host-side image comparison; platform-agnostic |
| **Flows record/replay** (`flow-*`) | ✅ | ✅ | ✅ | **✅** | Records any
tool-call sequence |
## Test plan
- [x] `list-devices` surfaces connected Vega devices (`platform:"vega"`,
`kind:"vvd"`)
- [x] `launch-app`, `restart-app`, `reinstall-app`,
`list-installed-apps` work on Vega
- [x] `tv-remote` drives focus (single key, `repeat`, and multi-key
path) and `keyboard` injects whole strings
- [x] `screenshot` returns a frame via the host-side VVD path
- [x] `describe` returns the on-screen element tree with `[focused]`
state and normalized coordinates
- [x] Vega VVD e2e CI workflow passes
- [ ] Test multiple platforms being driven at the same time
- [x] Android + Vega
- [ ] iOS + Vega
- [ ] chrome + Vega
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
31bb59fb60 |
chore: bump all workspace packages to 0.12.1 (#381)
Bumps every workspace package under `packages/*` from `0.12.0` → `0.12.1`, keeping the monorepo's lockstep versioning intact. ## Changes - `version` bumped to `0.12.1` in all 15 workspace `package.json` files - `package-lock.json` workspace entries synced to match ## Verification - `npm run check:versions` → `All workspace packages are at 0.12.1.` 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
8483fe23f9 |
feat: add telemetry (#274)
## Summary Adds anonymous, opt-out telemetry for Argent installer, update/uninstall flows, CLI telemetry controls, tool-server lifecycle, and tool invocation health signals. ## What Changed - Added a new private `@argent/telemetry` package - Introduced typed telemetry events with runtime property allowlisting/sanitization. - Added lazy anonymous identity creation in `~/.argent/telemetry-id`. - Added consent handling via: `argent telemetry enable`, `argent telemetry disable`, `argent telemetry status` - Added local debug logging with `ARGENT_TELEMETRY_DEBUG=1`. - Tracks installer funnel events for init, global install/update decisions, editor selection, allowlist decisions, skill installation, rules/agents copy, update, and uninstall. - Tracks tool-server start/stop and tool invocation/complete/fail events. - Adds per-tool invocation IDs so request metadata can be correlated without relying on ordering. - Records only coarse platform metadata (`ios` / `android`) for device tools - Ensures short-lived commands drain telemetry with bounded shutdown before exit. - Wires telemetry package into bundling and injects full Argent CLI version metadata at build time. - Added central `FAILURE_CODES` and `FailureSignal` helpers in `@argent/registry`. - Attached structured, sanitized failure metadata across registry errors, tool-server tools/services, HTTP validation/dependency gates, installer flows, and `argent run`. - Added telemetry allowlisting for safe failure fields only: static error codes/stages, coarse area/kind/command, bounded exit codes, allowlisted signals/spawn codes, and coarse network failure classes. - Split CLI-local `argent run` failures into `cli:run_fail` so they are not double-counted as tool failures. - Added installer telemetry for failed package actions, update/uninstall failures, rules/agents copy results, and skill refresh results. - Made telemetry consent writes atomic and kept telemetry status side-effect free via `peekAnonId()`. - Adds a coarse, anonymous signal to tool telemetry: which AI coding tool is driving the MCP server (Claude Code, Codex, Cursor, Gemini, VS Code, Windsurf, Zed, opencode, Copilot, or other). ## Tests Added / Updated - Telemetry unit coverage for consent precedence, identity creation, base props, sanitization, PostHog config, debug logging, shutdown, opt-out, and forget behavior. - Registry/tool-server tests for invocation IDs, platform metadata boundaries, startup failure flushing, and tool lifecycle telemetry. - Installer tests for final telemetry drain, update action classification, and uninstall identity cleanup ordering. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
02e500884f |
chore(deps): npm-minor-patch group excluding tree-sitter (fixes #348) (#355)
## What Applies the safe updates from dependabot #348 (`npm-minor-patch` group) and **excludes the one bump that breaks the build**, `tree-sitter`. | Package | From | To | | --- | --- | --- | | prettier | 3.8.3 | 3.8.4 | | @clack/prompts | 1.1.0 / 1.4.0 | 1.5.1 | | @types/node | 25.9.0 | 25.9.3 | | esbuild | 0.28.0 | 0.28.1 | | semver | 7.7.4 / 7.8.0 | 7.8.4 | | vitest | 4.1.6 | 4.1.9 | | tree-sitter | 0.21.1 | **held at 0.21.1** | (`ws` was part of #348's group but is already at 8.21.0 on `main`, so it's untouched here.) ## Why tree-sitter is excluded `tree-sitter@0.25.0` ships **no Node 24 prebuilds**, and its `binding.gyp` forces `-std=c++17` while Node 22+ v8 (cppgc) headers require C++20, so the native build fails `npm ci`/`npm install` on current Node. That's exactly why #348's `ESLint` job (Node 24) fails at install: ``` .../node_modules/tree-sitter ... node-gyp-build .../include/node/v8config.h:13:2: error: #error "C++20 or later required." ``` (#348's `Unit tests` job is pinned to Node 20, where it still compiles — which is why that PR looks half-green.) There is no tree-sitter release between 0.22.4 and 0.25.0, so there's no newer working version. `^0.21.1` ships N-API prebuilds (no compile) and installs cleanly on Node 20 / 22 / 24. This PR also adds a Dependabot `ignore` for tree-sitter minor/major so the group stops re-proposing the broken bump. Patch updates stay allowed; drop the ignore once upstream restores prebuilt binaries. ## Verification (local) - Lockfile regenerated on **Node 20 / npm 10.8.2** (`npm install --package-lock-only --ignore-scripts`), idempotent under a re-run — won't trip the strict lockfile check. tree-sitter resolves to 0.21.1 (prebuilt, no native compile). - Full CI-equivalent on **Node 20**: `tsc --build`, `eslint --max-warnings 0`, `prettier --check`, `typecheck:tests`/`typecheck:scripts`, full unit suite incl. **1250 tool-server tests** — all green. - `npm ci` from the committed lockfile succeeds. - **Live device E2E**: ran the built tool-server with the bumped deps, captured a real screenshot of a booted iPhone 16 Pro Max (`simulator-client` uses `ws`), then downloaded the PNG artifact over HTTP — `200`, 331 KB PNG. Supersedes #348 (Dependabot auto-closes it once the safe bumps land and tree-sitter is ignored). |
||
|
|
c832109573 |
chore: bump versions to 0.12.0 (#358)
Bumps the package suite from `0.11.0` to **`0.12.0`**. ## Changed - All release packages `0.11.0 → 0.12.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`, `@argent/configuration-core`. - `@argent/preview-window` `0.9.0 → 0.12.0` — aligned to the suite (was lagging a release behind; `private`/unpublished). - Regenerated the matching `package-lock.json` workspace version entries. ## Not changed - `@argent/ui` stays at `0.0.1` (separate pre-release track). - The private workspace root has no version. - Internal deps use `file:../` specifiers, so no dependency ranges needed updating. No source/runtime changes — `argent --version` reads from `package.json` at runtime. |
||
|
|
f3d108a5a1 |
feat: Argent Lens (#271)
Adds a UI for proposing and choosing between multiple agent-generated variants on a running app. **MCP tools** (`packages/tool-server/src/tools/variants/`) - `propose_variant` — non-blocking; agent stages a variant for an on-screen element (text/label/identifier/role matcher). - `await_user_selection` — one blocking call; parks until the human submits, then returns the picks + free-form annotations + a global comment. **Preview UI** (`packages/ui`) - Floating cards anchored to live element frames via the existing `describe` tree, with connector lines. - Comment-mode inspector: hover spotlight (blur+dim everything else), right-click any element to pin a comment. - Sticky padded hitbox + trajectory-aware dismissal so focus doesn't drop on cursor jitter. - Footbar [Complete] [Clear], confirm modal for destructive actions, green toast on submit. - Single source of feel constants in `theme.css`. **Electron host** (`packages/preview-window`) - New workspace package. Frameless transparent BrowserWindow at fixed size; the squeeze open/close is a CSS `transform: scaleY` on `<html>`, GPU-composited — no OS-level resize. - Tool-server spawns it on `await_user_selection` park, reuses it across re-awaits, and asks it to animate-close on submit (1s grace so the toast is visible). - Stdio-line IPC (no extra ports). Ephemeral throughout — the renderer hits the tool-server's already-ephemeral `/preview/*` HTTP. --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
acfe80ab6b |
chore: add ESLint with typescript-eslint (#349)
## What Adds type-aware linting to the workspace. Until now the only static gate on JS/TS was Prettier (formatting) and `tsc` — there was no linter, so the whole class of bugs `tsc` doesn't catch (floating promises, misused promises, throwing non-Errors, unsafe stringification, dead code) went unguarded across ~450 TS files. - **`eslint.config.mjs`** — flat config. `recommendedTypeChecked` runs on the TS sources via an explicit `project` list (each package keeps tests in a separate `tsconfig.test.json` that `projectService` can't auto-discover, so it's listed explicitly). Type-checked rules are disabled for the loosely-typed plain-JS build/dev scripts. The `argent-private` submodule and build artifacts are ignored (mirrors `.prettierignore`). - **`lint` / `lint:fix`** npm scripts. - **`.github/workflows/lint.yml`** — builds the workspace (type-aware rules need the referenced projects' declarations) then runs `eslint .`. Node 24 (ESLint 10 requires `^20.19 || ^22.13 || >=24`). ## Rule calibration This is an adoption PR, so it's deliberately calibrated rather than maximal: - **Kept on (bug-catchers):** `no-floating-promises`, `no-misused-promises` (with `checksVoidReturn: false` to drop the noisy callback variant), `only-throw-error`, `no-base-to-string`, `prefer-promise-reject-errors`, `no-unused-vars` (`_`-prefix escape hatch), plus the ESLint recommended set. - **Parked off as documented debt:** the high-volume style/`any`-family rules — `no-unsafe-*`, `require-await`, `no-unnecessary-type-assertion`, `restrict-template-expressions`, `unbound-method`, `no-explicit-any`, etc. These flag broad classes of pre-existing code and are out of scope here; each is commented as a ratchet target for follow-up passes. All findings from the kept-on rules are fixed in this PR (dead code/imports, error-cause chaining, **3 genuine floating promises**, non-Error throws, unsafe stringification). Stale unused `eslint-disable` directives left over in the tree were removed by `--fix`. ## Verification Locally, all green: `eslint .` (0 errors), `tsc --build`, `typecheck:tests`/`typecheck:scripts`, the full `vitest` suite (no behavior change from the fixes), and `prettier --check`. ## Not included (intentionally) Other static-analysis gates discussed (CodeQL, dependency-audit, ShellCheck, actionlint) are out of scope for this PR. |
||
|
|
6e4d8babec |
feat: GET /artifacts/ endpoint (#347)
EAS is going to poll this endpoint for new artifacts and upload as session artifacts for later user inspection. I'm open to better ways to integrate, this seems the easiest to merge and move forward. |
||
|
|
f288a461b0 |
refactor: rename the Electron device platform to "chromium" (CDP) (#332)
## Summary The Electron support added in #247 is **generic Chromium/CDP under the hood**. The discovery layer probes the Chromium-default CDP port (`9222`) and any `ARGENT_*_PORTS` entries; the connection picks the first non-devtools page target; describe is a plain in-page DOM walker; input/screenshot/screencast/navigation/JS-debugger are all straight CDP. Nothing in that path is Electron-specific — the only Electron-specific piece is the *launcher* (`boot-electron.ts`, which resolves and spawns an Electron binary). So any Chromium-family runtime exposing a CDP endpoint — an Electron app, **or** Chrome/Brave/Edge/WebView2 launched with `--remote-debugging-port` — is discoverable and drivable through the same tool surface. This PR renames the platform discriminator (and everything keyed off it) from `electron` to `chromium` so the naming matches reality. ## What changed **Public/contract surface (clean hard rename, no back-compat aliases):** - Platform discriminator `"electron"` → `"chromium"`; device id `electron-cdp-<port>` → `chromium-cdp-<port>`. - `ToolCapability.electron` → `.chromium`; the capability-gate rejection now reads `Tool 'X' is not supported on chromium app`. - Env `ARGENT_ELECTRON_PORTS`(`_FILE`) → `ARGENT_CHROMIUM_PORTS`(`_FILE`); ports cache `~/.argent/electron-cdp-ports.json` → `chromium-cdp-ports.json`; HTTP surface `/electron-server/` → `/chromium-server/`. **Files / dirs / symbols:** - `src/electron-server/` → `src/chromium-server/`; blueprints `electron-cdp` / `electron-js-runtime-debugger` → `chromium-*`; `describe`/`open-url`/`launch-app` `platforms/electron.ts` → `chromium.ts`; `keyboard/electron-keys` → `chromium-keys`; `utils/electron-discovery` → `chromium-discovery`. - `ElectronServer`/`ElectronCdpApi`/`ElectronJsRuntimeDebugger`/`describeElectron`/`discoverElectronDevices`/`ELECTRON_ID_PREFIX`/… → `Chromium*` / `CHROMIUM_*`. Tests renamed to match. **Stays "electron" (the Electron-specific launcher):** - `boot-device` keeps the `electronAppPath` / `electronPort` / `electronArgs` inputs, and `boot-electron.ts` still resolves & spawns an Electron binary. It just yields a `chromium` device now (an Electron app *is* a Chromium/CDP runtime). **Skills:** `argent-device-interact`, `argent-metro-debugger`, `rules/argent.md`, and the RN-workflow reference were reframed to describe **Chromium (CDP)** availability — Electron apps *plus* any Chromium browser over CDP (auto-discovered on `9222` / `ARGENT_CHROMIUM_PORTS`) — instead of Electron only. ## Test plan - [x] `tsc --build` clean - [x] `typecheck:tests` clean - [x] **1116 vitest cases pass (126 files)** - [x] prettier clean on all changed/renamed files - [x] Full-repo sweep: every remaining "electron" mention is intentional (the launcher path + skill prose explaining Electron as one kind of Chromium/CDP app); `electronic` in LICENSE/COC untouched. |
||
|
|
9b027096d7 |
feat: Electron support (#247)
## Summary Adds Electron (Chromium + CDP) as a third device platform alongside iOS and Android. Argent can boot an Electron app, list it next to simulators/emulators, drive it with the same tool surface where it makes sense — taps, swipes, keyboard, screenshots, describe, navigation — and debug it via the four ported `debugger-*` tools. Mirrors the sim-server architecture in TypeScript: a per-device server with HTTP API, MJPEG screencast, refcounted CDP screencast, FPS tracker, clipboard, and a WebSocket command bus. ## What works on Electron | group | tools | status | |---|---|---| | Lifecycle | `boot-device` (with `electronAppPath`), `launch-app`, `open-url` | ✅ works | | Vision | `screenshot`, `describe` (DOM walker w/ shadow-DOM + same-origin iframes) | ✅ works | | Input | `gesture-tap`, `gesture-swipe`, `keyboard` (text + named keys) | ✅ works | | Sequencing | `run-sequence` (per-step capability check) | ✅ works | | **JS debugger** | `debugger-connect`, `debugger-status`, `debugger-evaluate`, `debugger-log-registry` | ✅ **direct CDP** | | Discovery | `list-devices` (Electron entries shown alongside sims/emulators) | ✅ works | | Cleanup | `stop-simulator-server`, `stop-all-simulator-servers`, `stop-metro` | ✅ works | | Misc | `update-argent`, `dismiss-update`, `gather-workspace-data`, all `flow-*` recording tools | ✅ works | ### Debugger port — how it routes Dispatch lives in `tools/debugger/debugger-service-ref.ts`: an Electron device id (`electron-cdp-<port>`) routes to a new `ElectronJsRuntimeDebugger` blueprint that wraps the page CDP session already opened by `boot-device`. Everything else stays on the Metro-driven `JsRuntimeDebugger` blueprint. The new blueprint exposes the same `JsRuntimeDebuggerApi` shape so tool code is unchanged. `Runtime.consoleAPICalled` events feed into the existing `LogFileWriter` + cluster pipeline — `debugger-log-registry` returns the same shape on Electron as it does on Metro. ## What is cleanly rejected with `Tool 'X' is not supported on electron app` HTTP capability gate declines these up-front (HTTP 400): | group | tools | |---|---| | Mobile gestures | `gesture-pinch`, `gesture-rotate`, `gesture-custom` | | Mobile controls | `button`, `rotate` | | App lifecycle | `restart-app`, `reinstall-app` | | iOS-native | `paste`, all `native-*`, `native-profiler-*` | | RN-only debugger | `debugger-component-tree`, `debugger-reload-metro`, `debugger-inspect-element` | | RN-only network | `view-network-logs`, `view-network-request-details` | | RN-only profiler | all `react-profiler-*` (incl. `component-source`), all `profiler-*` query / combined tools | ## Verification history Three commits resolved findings from two parallel verification swarms: - `507b295` — swarm v1 fixes: early-disconnect race in new blueprint, NaN timestamp crash, disconnect listener leak, missing test coverage for 13 of 15 lockouts, missing disconnect→terminated propagation test, stale skill + tool descriptions. - `dd0f185` — pre-existing crash: `spawn()` in boot-electron lacked an `'error'` event handler; ENOENT escalated to `uncaughtException`. + 4 regression test cases. - `95161ab` — swarm v2 fixes: orphan promise rejection when pid-less spawn fires deferred `'error'`, misleading rationale comment, stale cross-skill references calling this "Metro debugging only". Branch was also cherry-picked onto v0.8.0 (`e1327b4`) to pick up #245. ## Follow-ups left for separate PRs - **CPU profiling on Electron** — Chromium's `Profiler.start`/`Profiler.stop` work fine; only the V8 sample format differs from Hermes'. A normalizer in the query layer would unlock `react-profiler-cpu-summary` + `profiler-cpu-query` for arbitrary Electron renderers (without the React-DevTools commit half). - **Source-mapped element inspection on Electron** — `debugger-inspect-element` could be rebuilt on top of `DOM.getNodeForLocation` + CSS source maps, but it'd be a from-scratch implementation rather than a port. ## New sim-server-equivalent HTTP surface Mounted at `/electron-server/:deviceId/*` — not advertised to MCP, consumed by preview UIs and integration tests: - `GET /viewport`, `GET /stream.mjpeg` (multipart-JPEG screencast) - `POST /api/screenshot`, `/api/clipboard/text`, `/api/fps`, `/api/navigate`, `/api/reload`, `/api/history/back`, `/api/history/forward` - `WS /ws` (input commands + event bus) Sharp is loaded as an optional dependency — screenshots still work without it (no rotate/resize, just raw PNG). ## Test plan - [x] 786 vitest cases pass; new coverage for the dispatch helper, blueprint factory edge cases (early disconnect, NaN timestamp, dispose symmetry), 15-tool table-driven lockout assertion, and 5 boot-electron spawn-error scenarios. - [x] Build + prettier clean. - [x] E2E swept the original 27 surfaces (11 MCP tools, 8 HTTP endpoints, MJPEG, WebSocket, 6 capability rejections, session cleanup). - [x] E2E swept the debugger surface against a live Electron app — connect / status / evaluate (`1+1`, JSON-stringified globals, `document.title`) / log-registry (clusters + log file). - [x] E2E swept the 17 lockouts — every one returns HTTP 400 with `Tool 'X' is not supported on electron app`. - [x] Two rounds of parallel verification swarm; every real finding addressed. - [ ] Reviewer drives an Electron app end-to-end via MCP and confirms describe / tap / swipe / keyboard / debugger-evaluate land correctly. --------- Co-authored-by: Filip131311 <contact@filipkaminski.com> |
||
|
|
c01f41cd5f |
feat(remote): input-side file boundary — flows, screenshot-diff, profiler reports, workspace tools (#319)
## Why PR #278 fixed the **output** half of the remote file boundary: files a tool *produces* travel to the client as `ArtifactHandle`s. But seven tools still break when the tool-server runs on a different machine than the agent, in one (or both) of two ways: - **Output-side leftovers** — `native-profiler-analyze` and `screenshot-diff` still returned raw host paths (and embedded them in report/summary prose), so the agent can't open them remotely. - **Input-side gap** — nothing moved a file the *agent* owns onto the tool-server. Any tool taking a client-local path as a parameter (`screenshot-diff` baselines, the three flow tools' `project_root`, `react-profiler-component-source`, `gather-workspace-data`) silently read/wrote the wrong machine's disk. Flows were fully broken in remote mode: the YAML landed on the server's disk under the agent's path. ## What ### Output side (apply the existing pattern) - **native-profiler-analyze** — `reportFile` is now an `ArtifactHandle` (mirrors `react-profiler-analyze`'s `fileArtifact()`); the inline report now says *"use the Read tool on the `reportFile` path in this result"* instead of embedding the server path. - **screenshot-diff** — `diffPath` / `contextDiffPath` are now `ArtifactHandle`s; the MCP adapter renders the context-diff image from materialized bytes (legacy string-path results from older servers still render co-located). The summary references the result fields instead of embedding host paths. ### Input side (new, symmetric mechanism) A declarative **file-input boundary**, mirroring the artifact gate in the opposite direction: - A tool declares `fileInputs: [{ target, path: "${param}…", kind }]` on its `ToolDefinition`; the declaration ships to the client via `GET /tools`. - Before sending a call, the client (`@argent/tools-client`, shared by CLI and MCP) interpolates each template from the args and replaces the target arg with a wrapper: `{ __argentFileInput, path, size, mtimeMs, content? }`. File bytes are base64-inlined **only when routed to an external tool-server** (`argent link` / `ARGENT_TOOLS_URL`) — unlinked local calls never pay for encoding. - The server resolves wrappers **before zod validation**, so tools always execute against a plain local path: - path matches on the server's own filesystem (existence for dirs, size+mtime for files) → used **in place**, zero copies — the exact dual of the artifact materializer's co-located gate; - otherwise `kind: "file"` content is materialized into a temp file (with size-integrity check, 32 MB cap); - `kind: "directory"` (trees that can't ride in a call) → 422 with actionable remote-mode guidance; - `kind: "probe"` → never fails; the tool learns host-presence via `ctx.fileInputs` and adapts. - The reverse channel: a tool whose output belongs in the **agent's project** returns a `__argentClientFile` directive (path + content); the client writes it and rewrites the directive to the path. Writes are constrained to `**/.argent/flows/<safe-name>.yaml` so a tool-server can't direct writes anywhere else on the client machine. ### Tool migrations | Tool | Boundary use | |---|---| | `flow-start-recording` | `probe` on `project_root`: present → host persistence exactly as before; absent (remote) → recording kept in server memory, YAML returned as a client-write directive | | `flow-add-step` / `flow-add-echo` / `flow-finish-recording` | follow the session's persist mode; every mutation returns `savedTo` (path locally, directive remotely) so the agent-side file is always current | | `flow-execute` / `flow-read-prerequisite` | `file` input derived from `${project_root}/.argent/flows/${name}.yaml` into an internal `flow_file` param — read in place locally, uploaded when remote | | `screenshot-diff` | `baselinePath`/`currentPath` as `file` inputs; `outputDir` now **optional** (temp default) and probe-ignored when absent on the host | | `react-profiler-component-source`, `gather-workspace-data` | `directory` gate — unchanged locally; clear error remotely instead of a silent empty AST index / all-nulls snapshot | ### Version skew Both halves degrade to today's behavior: an old client sends plain strings (the resolver passes them through, tools take their legacy paths), and an old server doesn't advertise `fileInputs` (the new client never wraps). Wrappers on undeclared params fail the tool's own schema validation, so nothing can be smuggled through free-form args. ## Tests **Unit** (all green: registry 68, tool-server 992, tools-client 107, argent-mcp 66, argent-cli 117): - `tool-server/test/file-inputs.test.ts` — in-place gate, stat-mismatch → upload, truncated-upload rejection, directory gate, probe semantics, undeclared-target smuggling. - `argent-tools-client/test/file-inputs.test.ts` — wrapping (stat-only vs inlined content), multi-param template derivation, explicit-override respect, directive writes incl. path-validation refusals (relative, traversal, outside `.argent/flows`, bad charset/extension). - `tool-server/test/flows/flow-remote-recording.test.ts` — remote recording stays in memory + returns directives, nothing written on the host, replay/read from a boundary-resolved `flow_file`. - Updated existing screenshot-diff / native-profiler tests to the artifact-handle contract. **Local / co-located (unlink scenario)** — isolated tool-server + CLI on an iPhone 17 Pro simulator: full flow record→finish→read-prerequisite→execute loop (YAML written via the same path as before, in-place gate hit, no byte copies); screenshot-diff with and without `outputDir`; wire-level checks of the unlinked client shape (wrapper without content → in-place read) and the legacy plain-string shape; missing-file → 422 with a precise message. **Remote (Lima Linux VM as the agent machine, tool-server on the host Mac, `--host` bind + bearer auth)** — via both the CLI and the real MCP stdio server: - flow recording from the VM: YAML lands **in the VM's project**, `savedTo` rewritten to the VM path, *no* agent-path directories created on the host; replay from the VM uploads the YAML and drives the host simulator. - `screenshot` materializes into the VM temp cache; `screenshot-diff` round-trips both directions (VM baseline uploaded, live capture on host, diff artifacts downloaded back; MCP returns `image,text` content with the context diff inlined). - `gather-workspace-data` / `react-profiler-component-source` with VM-only paths → the new actionable 422. ## Notes - `native-profiler-analyze` was not exercised live (xctrace export is unusable on this machine's iOS 26.4 simulators — known, unrelated); its change is the same one-line `fileArtifact` pattern `react-profiler-analyze` already ships, covered by unit tests. - `react-profiler-analyze`'s own `project_root` (AST resolve stage) is left as-is deliberately: it degrades gracefully (report still renders) and gating it would break currently-working remote analysis. Tracked as follow-up alongside nested invocations (`run-sequence`/`flow-execute` steps bypass the HTTP boundary by design). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fa86905fbd |
chore: bump versions to 0.11.0 (#322)
Bumps the package suite from `0.10.0` to **`0.11.0`**. ## Changed - All release packages `0.10.0 → 0.11.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`. - `@argent/configuration-core` `0.9.0 → 0.11.0` — was lagging a release behind (extracted in #295); aligned to the suite. (It's `private`/unpublished, so this is internal-only.) - Regenerated `package-lock.json` to reflect the new workspace versions. ## Not changed - `@argent/ui` stays at `0.0.1` (separate pre-release track). - The private workspace root has no version. - Internal deps use `file:../` specifiers, so no dependency ranges needed updating. No source/runtime changes — `argent --version` reads from `package.json` at runtime. |
||
|
|
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>
|
||
|
|
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) |
||
|
|
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>
|
||
|
|
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` |
||
|
|
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` |
||
|
|
18cf0f12cd | chore(deps): bump zod to v4 (#265) | ||
|
|
09a18a65ef | bump to v0.8.0 (#245) |