agent-joule/PRIVACY_HARDENING.md
David Charlot 88a41bedd3 Rebrand Zed -> Joule across UI, docs, embedded assets, and legal
The fork shipped renamed binary/bundle but the product name "Zed" was still
shown everywhere in the UI. Rename the display name to "Joule" (tagline
"Energy-metered agentic coding") while preserving every load-bearing reference.

Display renames (105 .rs files): release_channel display_name, welcome +
onboarding + ai_onboarding, macOS app menu (About/Hide/Quit), window titles,
About panel, settings UI, notifications, update/collab messages, system specs.
Bundle display name (crates/zed/Cargo.toml) -> "Joule" so CFBundleName / the
menu-bar name and the .app are "Joule" (identifier science.transaction.AgentJoule
and the agent-joule binary are unchanged).

Docs (223 .md): product prose Zed -> Joule, fence-aware (code blocks untouched).
Embedded assets (12 JSONC): // comments only.
Legal (4 files): product -> Joule, entity "Zed Industries, Inc." -> "Transaction
Science, Inc.", domain zed.dev -> openie.dev. NOTE: these are structurally Zed's
inherited terms and need a real legal review, not just a rename.

Deliberately PRESERVED (renaming would break the app): font aliases ("Zed Plex
Sans"/".ZedMono" in text_system + assets), HTTP header names (X-Zed-Predict-*),
zed:: action ids, app/bundle identifiers (dev.zed.Zed*, Zed-Editor-* product ids),
zed.dev URLs, ZED_* env vars, crate/package names, "Zed Industries" in non-legal
attribution, and the {zed,Zed} config-dir glob.

Signed + notarized + stapled as Joule.app, installed to /Applications, verified
(Gatekeeper: Notarized Developer ID, FH93XC2VRR). Caveat: test-code string
literals were renamed too, so some `cargo test` assertions may need updating.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-03 02:23:35 -04:00

5.7 KiB

agent-joule — Privacy Hardening

agent-joule is a private, no-phone-home fork of Joule. It drives vendors' own logged-in agent CLIs (Claude Code, Gemini) over ACP plus a local vLLM backend. It must never establish a Joule identity, send telemetry, upload crashes, check for updates, or make any unprompted network call to Joule-operated (or Joule-adjacent) infrastructure.

This document records what was removed/neutralized and how it was verified.

Threat model / goal

  • No automatic sign-in; no Joule account is ever established.
  • No telemetry, analytics, or usage events leave the machine.
  • No crash/minidump upload.
  • No auto-update or contact with release servers.
  • No unprompted startup fetches to Joule-operated endpoints.
  • Expected, allowed traffic: the federated agent processes the user explicitly drives (e.g. npx ACP agents) and the local vLLM backend on localhost. These are the vendor's processes, not agent-joule phoning home.

Changes

A — Sign-in removed

  • crates/zed/src/main.rs: authenticate() is a no-op — no automatic sign-in/connect at startup, so no Joule identity is established (this was the real phone-home).
  • crates/title_bar: removed the Sign In button + now-unused bindings.
  • crates/onboarding: removed the Joule-cloud "Joule Agent" promo + SignIn action. The dangling onboarding::SignIn keybindings were also removed from the three default keymaps (a removed action left bound = startup keymap-validation panic).
  • The client crate stays compiled but signed-out; collab/CRDT is retained for later self-hosting against a non-Joule server.

B — Auto-update removed entirely

Updates are a wholesale uninstall + reinstall; there is no in-app updater.

  • Deleted the auto_update, auto_update_ui, and auto_update_helper crates and all usages (menus, title-bar affordance, activity indicator, init).
  • This closed a residual egress that the earlier poll-neuter missed: auto_update still contained live calls to Joule release servers (download_remote_server_release / get_release_asset), reachable via "Check for Updates" and the SSH remote-server download path.
  • remote_connection: remote-server auto-download is stubbed to a clear "disabled in agent-joule" error instead of silently fetching from Joule.
  • Windows packaging was updated to match: script/bundle-windows.ps1 no longer builds/copies/signs auto_update_helper, and zed.iss drops the {app}\tools and {app}\updates directories (auto-update installer artifacts).

C — Telemetry neutralized

  • crates/client/src/telemetry.rs: send_event() is a no-op, so every telemetry::event! call site across the tree is inert with a single edit. Nothing is queued, recorded, or sent.

D — Crash upload disabled

  • MINIDUMP_ENDPOINT is forced to None (ignores build/runtime env), so the upload paths in reliability.rs are unreachable. Local minidump capture is unaffected. (Runtime log confirms: "Minidump endpoint not set".)
  • RefreshLlmTokenListener is inert (no sign-in event to trigger a Joule cloud token refresh).

E — ACP agent-registry auto-fetch disabled

The federation feature auto-fetched the ACP agent registry from cdn.agentclientprotocol.com at startup (registry index + icons). This is not Joule telemetry, but it is an unprompted launch-time request to Joule-adjacent infrastructure — exactly the egress this build avoids.

  • crates/project/src/agent_registry_store.rs: removed the startup auto-refresh in init_global, and made refresh_if_stale() a no-op (this was the path that actually fired, via agent-settings load and registry-agent connect).
  • The registry still loads from the on-disk cache, and the explicit, user-initiated refresh() (e.g. opening the Agent Registry page) still works. Only the automatic, staleness-driven fetch is gone.

F — Egress backstop

Belt-and-suspenders: even if some code path survived the removals above, it cannot reach a Joule host.

  • crates/http_client/src/http_client.rs: the four Joule-host URL builders (build_zed_api_url, build_zed_cloud_url, build_zed_cloud_url_with_query, build_zed_llm_url) now bail! instead of returning a URL. Any surviving caller fails loudly rather than contacting api.zed.dev / cloud.zed.dev / llm.zed.dev.

Extensions

  • assets/settings/default.json: auto_install_extensions is empty by default (was {"html": true}), so the extension registry/CDN is not contacted at startup. Users opt in to extensions explicitly.

Verification

Method: launch the built agent-joule against a fresh --user-data-dir, then observe outbound connections with lsof -nP -i -p <pid> and capture TLS SNI hostnames with tshark -i en0 -Y 'tls.handshake.type == 1' (Wireshark's ChmodBPF grants capture access without sudo). IP-based inference is unreliable here because Joule and the ACP CDN both sit behind Cloudflare anycast; SNI is authoritative.

Findings across the hardening passes:

  • Zero connections to *.zed.dev (Zed's own infra) at any point.
  • The one persistent startup egress observed (~36 connections) was conclusively identified by SNI as cdn.agentclientprotocol.com — the ACP registry fetch, fixed in E above.
  • After E: agent-joule makes zero outbound connections at idle startup (0 established across repeated samples; no cdn.agentclientprotocol.com, no *.zed.dev in the SNI capture). Startup is network-silent.

Re-run the check after any change touching networking:

cargo build --bin agent-joule
tshark -i en0 -f 'tcp port 443' -Y 'tls.handshake.type == 1' \
  -T fields -e tls.handshake.extensions_server_name -a duration:26 | sort | uniq -c &
./target/debug/agent-joule --user-data-dir /tmp/azed-check .
# expect: no agent-joule connections to *.zed.dev or *.agentclientprotocol.com