Skip to content

feat(runtime): bundle mitmdump with packaged Agent Code - #121

Merged
Juliusolsson05 merged 3 commits into
mainfrom
feat/runtime-tools-scaffolding
May 14, 2026
Merged

Juliusolsson05 merged 3 commits into
mainfrom
feat/runtime-tools-scaffolding

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Closes #119. Part of the bundled runtime-tools effort; tmux follows in a separate PR (#120).

Summary

Packaged Agent Code now ships its own `mitmdump` so Claude proxy streaming works without a Homebrew-installed mitmproxy. The bundled archive (`mitmproxy 12.2.3`, ~52 MiB compressed) extracts lazily on first proxy session into `userData/runtime/mitmproxy-/` and the resolver returns the absolute path of the inner Mach-O. The reusable `claude-code-headless` package stays Electron-agnostic: Agent Code main resolves the path and passes it through `mitmDumpPath`.

What's in the diff

  • `third_party/mitmproxy/` — committed: `manifest.json` (pins `12.2.3` with per-arch sha256/bytes), `README.md`, `LICENSE.md`, `.gitignore`. Cache is ignored; binaries are never in git.
  • `third_party/tmux/` — scaffolding only (issue Bundle tmux with packaged app #120). Manifest has `TBD-PR3` placeholders; the verify script handles them gracefully.
  • `scripts/runtime-tools/` — `fetch-mitmproxy.mjs` (download + sha256 verify, idempotent), `verify-mitmproxy.mjs` (re-verify cache, with `--strict` for CI), `build-tmux.mjs` (stub for PR 3), `verify-tmux.mjs` (placeholder-aware).
  • `src/main/setup/runtimeTools.ts` — final resolver API: `getPlatformKey`, `isBundledArchiveAvailable` (cheap setup-gate check), `resolveBundledTool` (lazy extraction with per-version mutex + best-effort old-version cleanup). Reads manifest from `out/main/runtime/mitmproxy/manifest.json`, finds archive via the same path, swaps `.asar/` → `.asar.unpacked/` for packaged builds.
  • `scripts/copy-packaged-resources.mjs` — copies manifests + cache archives into `out/main/runtime/` during `npm run build:app`. Missing cache logs a warning rather than failing (dev forgiving; release CI runs strict prepare first).
  • `electron-builder.yml` — adds `out/main/runtime/**/*` to `asarUnpack`.
  • `src/providers/claude/runtime/claudeSession.ts` — resolves bundled mitmdump first, then setup-cached, then lets `createProxyServer` fall through to its own PATH/Homebrew chain.
  • Setup gate (`prerequisites.ts` + `SetupGate.tsx` + `shared/types/setup.ts`):
    • new `source: 'bundled' | 'system'` field on `SetupToolStatus`
    • `brew` flipped from `required` to optional (packaged users without Homebrew shouldn't be blocked)
    • bundled tools show a "Bundled" badge with "Shipped with Agent Code; no install required.", and the install button is suppressed for them
    • bundled tools no longer trigger the "missing optional tool" gate prompt
  • `CLAUDE.md` — documents the `vendor/` (read-only source references) vs `third_party/` (runtime artifacts we ship) distinction so the two folders don't blur over time.

New npm scripts

```
runtime:fetch:mitmproxy download pinned archive (current platform; --all for both)
runtime:verify:mitmproxy re-verify cache (non-strict by default)
runtime:verify:tmux placeholder-aware until #120
runtime:verify both, default non-strict
runtime:build:tmux stub; full impl in #120
runtime:prepare:mac CI entry: fetch --all + verify --strict (mitmproxy)
```
`postinstall` is NOT wired to fetch — dev `npm i` stays cheap. Release CI calls `runtime:prepare:mac` explicitly.

Verification

  • `npm run runtime:prepare:mac` — passes; both `darwin-arm64` and `darwin-x86_64` archives downloaded, sha256-verified, idempotent on re-run.
  • `npm run build:app` — passes; `out/main/runtime/mitmproxy/{manifest.json, darwin-arm64/.tar.gz, darwin-x86_64/.tar.gz}` are present (105 MiB total). tmux is correctly skipped with a warning.
  • Extracted one bundled archive manually and ran the inner binary:
    ```
    Mitmproxy: 12.2.3 binary
    Python: 3.14.4
    OpenSSL: OpenSSL 3.5.5 27 Jan 2026
    Platform: macOS-26.2-arm64-arm-64bit-Mach-O
    ```
  • `git check-ignore` confirms `third_party//cache/` is excluded. Repo size impact: 0.

Size impact

DMG grows by ~52 MiB (compressed mitmproxy archive). Installed-on-disk after first proxy session: ~87 MB in `userData/runtime/mitmproxy-/`. Old versions get cleaned up on subsequent launches.

Test plan

  • Run the unpackaged app, enable proxy streaming, spawn a Claude session — confirm proxy startup uses the bundled mitmdump (look for the `runtime/mitmproxy-12.2.3/` directory under userData and the absence of "Unable to find mitmdump" errors).
  • Re-run with the directory pre-existing — confirm the `.installed` marker short-circuits extraction.
  • Package via `npm run dist:mac`, install the DMG on a machine without Homebrew mitmproxy, verify proxy streaming works end-to-end.
  • SetupGate: confirm "Bundled" badge shows for mitmdump and no Install button appears.

🤖 Generated with Claude Code

Juliusolsson05 and others added 3 commits May 14, 2026 18:23
PR 1 of the bundled-runtime-tools effort (#119, #120). Pure scaffolding,
no behaviour change yet — establishes the conventions PR 2/PR 3 build on:

- `third_party/<tool>/{manifest.json,README.md,LICENSE.md,.gitignore}`
  pinning each tool's version, per-arch sha256, URL/source layout.
  Binaries are never committed; the manifest plus fetch/build scripts
  are the single source of truth.
- `scripts/runtime-tools/` with working fetch + verify for mitmproxy
  (real upstream archive, real hashes for mitmproxy 12.2.3 darwin
  arm64/x86_64) and stubs for tmux that will be implemented in PR 3.
- npm scripts `runtime:fetch:mitmproxy`, `runtime:verify`,
  `runtime:prepare:mac`. Deliberately NOT wired into `postinstall` so
  dev `npm i` stays cheap (mitmproxy is ~52 MiB per arch).
- `src/main/setup/runtimeTools.ts` defines the resolver API surface
  (`resolveBundledTool`, `getPlatformKey`) returning null for now;
  PR 2 wires mitmdump extraction, PR 3 wires tmux.
- `CLAUDE.md` documents the `vendor/` (source references, never shipped)
  vs `third_party/` (runtime artifacts, shipped with releases)
  distinction so the two folders do not blur together over time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the runtime resolver scaffolded in the previous commit so
packaged Agent Code no longer requires a user-installed mitmproxy for
Claude proxy streaming. Closes #119.

What changes:

- `scripts/copy-packaged-resources.mjs` copies each
  `third_party/<tool>/cache/<platform>/*` plus the manifest into
  `out/main/runtime/<tool>/<platform>/` so electron-builder's
  asarUnpack glob can land them at a real filesystem path. Missing
  caches log a warning (dev) rather than fail; release builds run
  `runtime:prepare:mac` first and that step is strict.
- `electron-builder.yml` adds `out/main/runtime/**/*` to `asarUnpack`.
- `src/main/setup/runtimeTools.ts` implements the resolver:
  reads the packaged manifest, locates the platform archive, extracts
  on first call into `app.getPath('userData')/runtime/mitmproxy-<ver>/`,
  writes the `.installed` marker last so a crash mid-extract is
  recoverable, and best-effort prunes older versions to stop unbounded
  growth across upgrades. Per-version in-process mutex prevents
  concurrent ClaudeSession spawns from racing tar extraction.
- `src/providers/claude/runtime/claudeSession.ts` resolves the bundled
  path before each proxy startup and threads it through `mitmDumpPath`
  on `createProxyServer`. Falls back to the setup-cached path and
  finally to the headless package's own PATH/Homebrew chain when
  bundled is unavailable. The headless package stays Electron-agnostic.
- Setup gate: `brew` is no longer required (packaged users without
  Homebrew can now reach the app). `mitmdump` shows as "Bundled" with
  the new `SetupToolSource = 'bundled' | 'system'` field; the install
  button is suppressed for bundled tools and the SetupGate UI labels
  them clearly. Bundled tools also skip the "missing optional"
  prompt the gate uses to nag users about Homebrew installs.
- `src/shared/types/setup.ts` gains the `source` field on
  `SetupToolStatus`.

Verification (run in worktree):
  - `npm run runtime:prepare:mac` — fetch all + verify --strict (pass)
  - `npm run build:app` — clean (warns about empty tmux cache as
    intended; tmux ships in #120)
  - Extracted bundled archive into /tmp and ran
    `mitmproxy.app/Contents/MacOS/mitmdump --version` -> "Mitmproxy:
    12.2.3 binary" — confirms the resolver's archive layout
    assumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…spaces, atomic extract

Three fixes from the PR #121 review, all must-fix:

1. `copy-packaged-resources.mjs` was using `new URL(import.meta.url)
   .pathname`, which returns the percent-encoded URL path. A
   contributor whose checkout lives under a path with a space would
   compute a `repoRoot` that does not exist on disk; every
   `fileExists()` would return false; the runtime cache copy would
   silently no-op; the packaged DMG would ship without the bundled
   mitmproxy archive — with no build warning. Switched to
   `fileURLToPath(import.meta.url)`, matching the pattern already used
   in the sibling fetch/verify scripts and in
   `claude-code-headless`' addon resolver.

2. `claudeSession.ts` was resolving the bundled mitmdump path before
   ever consulting `CLAUDE_HEADLESS_MITMDUMP`. Since
   `createProxyServer` short-circuits on `mitmDumpPath` before
   reading the env var, the documented "env override wins" contract
   in `runtimeTools.ts` was being silently broken — a contributor
   pointing the env var at a debug/instrumented mitmdump would
   simply not get it. Now we early-return on env override and let
   `claude-code-headless` consume it through its own existing chain.

3. `runtimeTools.ts` was extracting on top of any pre-existing
   `mitmproxy-<ver>/` directory. mitmproxy.app's PyInstaller layout
   includes symlinks (Python framework Versions/X/Current and
   similar); BSD `/usr/bin/tar` overwriting a half-extracted leftover
   from a previous interrupted install can leave mixed file-vs-
   symlink state that the marker check cannot detect. Now we extract
   into a sibling tmp directory, verify the inner executable is
   present, rm-rf the canonical dir, and rename the tmp dir into
   place — atomic on macOS because both paths live on the same
   filesystem (userData).

Also added an explicit KNOWN LIMITATION comment on
`cleanupOldMitmproxyVersions` documenting the same-userData multi-
install race (item #4 in the review). Decision: ship v1 without a
cross-process advisory lock; revisit if it surfaces in practice.

Verified:
- `npm run build:app` clean.
- The three other concerns flagged in the review (resolver path
  swap, hardened runtime entitlements, setup-gate brew-flip
  regressions) were independently confirmed safe by the reviewer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Pushed 8f1b6d5 addressing the three must-fix items from the review:

  1. copy-packaged-resources.mjs — percent-encoded path bug (scripts/copy-packaged-resources.mjs:21). Switched new URL(...).pathname → fileURLToPath(import.meta.url). Reproduced the encoding issue with a Node one-liner before fixing. Contributors whose checkouts live under a path with a space (e.g. /Users/some user/...) would otherwise produce a DMG with no bundled archive and no warning.

  2. Env override precedence (src/providers/claude/runtime/claudeSession.ts:195-219). Now early-returns on CLAUDE_HEADLESS_MITMDUMP (and the legacy CC_PROXY_TEST_MITMDUMP alias) before resolving bundled, so debug/instrumented mitmdump builds work again. The headless package's existing env-var handling is the single consumer; we just stop shadowing it. This also brings the actual order in line with the documented contract in runtimeTools.ts.

  3. Atomic temp+rename extraction (src/main/setup/runtimeTools.ts, installMitmproxy). Tar into a sibling .tmp-<pid>-<ts> directory, verify the inner executable, rm-rf the canonical install dir, then rename. The rename is atomic on macOS because both paths share the userData volume. This eliminates the half-extract failure mode the reviewer flagged for mitmproxy.app's PyInstaller symlinks.

Also: explicit KNOWN LIMITATION comment on cleanupOldMitmproxyVersions documenting the cross-process race (#4 in the review). Calling that out as an accepted v1 limitation rather than engineering an advisory lock before the scenario surfaces.

Build: npm run build:app clean on the new commit. Manual archive validation (mitmdump --version → 12.2.3) still passes from the original verification.

Test plan items 1, 2, 4 from the PR body still need to be exercised against the live Electron app before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bundle mitmdump with packaged app

1 participant