From d9b251b1f25deadd329b54b0aa97185bb32344af Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:08:04 -0700 Subject: [PATCH 1/2] remove local user agent The headless apps/local server is always launched by the executor CLI (executor mcp / web / daemon run / installed service) unless the desktop app spawns it with EXECUTOR_CLIENT=desktop. Report it as cli instead of local and narrow SurfaceClient to cli | desktop so the surface is only ever one of the two. --- apps/local/src/installation.ts | 8 +++++--- packages/core/integrations-registry/src/registry.test.ts | 4 ++-- packages/core/integrations-registry/src/registry.ts | 5 ++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/local/src/installation.ts b/apps/local/src/installation.ts index 90f7ea50c..301f3ac23 100644 --- a/apps/local/src/installation.ts +++ b/apps/local/src/installation.ts @@ -16,10 +16,12 @@ const resolveChannel = (version: string): InstallationChannel => { }; // The desktop main process sets `EXECUTOR_CLIENT=desktop` when it spawns the -// sidecar so PostHog can slice desktop installs from headless apps/local -// (CLI `executor web`, `daemon run --foreground`, etc.). +// sidecar. Otherwise this headless apps/local server was launched by the +// `executor` CLI (`executor mcp`, `web`, `daemon run --foreground`, or the +// installed background service), so it reports as `cli`. Surface is only ever +// `cli` or `desktop`; mirrors the owner.client resolution in apps/cli/src/main.ts. const resolveClient = (): SurfaceClient => - process.env.EXECUTOR_CLIENT === "desktop" ? "desktop" : "local"; + process.env.EXECUTOR_CLIENT === "desktop" ? "desktop" : "cli"; export const CHANNEL: InstallationChannel = resolveChannel(LOCAL_VERSION); export const VERSION: string = LOCAL_VERSION; diff --git a/packages/core/integrations-registry/src/registry.test.ts b/packages/core/integrations-registry/src/registry.test.ts index c4ec2a420..20da742fb 100644 --- a/packages/core/integrations-registry/src/registry.test.ts +++ b/packages/core/integrations-registry/src/registry.test.ts @@ -49,8 +49,8 @@ describe("buildUserAgent", () => { expect(buildUserAgent({ channel: "stable", version: "1.2.3", client: "cli" })).toBe( "executor/stable/1.2.3/cli", ); - expect(buildUserAgent({ channel: "beta", version: "1.2.3-beta.0", client: "local" })).toBe( - "executor/beta/1.2.3-beta.0/local", + expect(buildUserAgent({ channel: "beta", version: "1.2.3-beta.0", client: "desktop" })).toBe( + "executor/beta/1.2.3-beta.0/desktop", ); }); diff --git a/packages/core/integrations-registry/src/registry.ts b/packages/core/integrations-registry/src/registry.ts index b203f5444..86957d483 100644 --- a/packages/core/integrations-registry/src/registry.ts +++ b/packages/core/integrations-registry/src/registry.ts @@ -11,7 +11,10 @@ import { NodeFileSystem } from "@effect/platform-node"; // --------------------------------------------------------------------------- export type InstallationChannel = "stable" | "beta" | "dev"; -export type SurfaceClient = "cli" | "local" | "desktop"; +// Only two surfaces: the desktop app (sets EXECUTOR_CLIENT=desktop) or the +// `executor` CLI (everything else, including the headless apps/local daemon, +// `executor mcp`, and the installed background service). +export type SurfaceClient = "cli" | "desktop"; export const DEFAULT_INTEGRATIONS_URL = "https://integrations.sh/api.json"; From 1c839459e3d81c26766bd2ad4716c83618e32cab Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:16:11 -0700 Subject: [PATCH 2/2] always set EXECUTOR_CLIENT so the surface is never guessed The CLI now stamps EXECUTOR_CLIENT=cli at startup (preserving desktop's own EXECUTOR_CLIENT=desktop), so the in-process apps/local server always sees an explicit surface. apps/local passes the value through, validated, instead of defaulting not-desktop to a guess. --- apps/cli/src/client-env.ts | 10 ++++++++++ apps/cli/src/main.ts | 4 ++++ apps/local/src/installation.ts | 20 +++++++++++++------- 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 apps/cli/src/client-env.ts diff --git a/apps/cli/src/client-env.ts b/apps/cli/src/client-env.ts new file mode 100644 index 000000000..73dc4b0f3 --- /dev/null +++ b/apps/cli/src/client-env.ts @@ -0,0 +1,10 @@ +// Stamp the surface client for this process so the integrations User-Agent is +// always `cli` or `desktop`, never guessed. The desktop app sets +// EXECUTOR_CLIENT=desktop before spawning the CLI as its sidecar/daemon, so +// `??=` preserves that; every other invocation is the CLI itself. +// +// This MUST run before the `@executor-js/local` import graph loads: apps/local +// builds its User-Agent from EXECUTOR_CLIENT at module init (installation.ts), +// so setting the env in main.ts's body would run too late. Imported as an early +// side effect in main.ts, right after native-bindings. +process.env.EXECUTOR_CLIENT ??= "cli"; diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index c6e4df0e0..1ab045bf7 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -2,6 +2,10 @@ // before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them. import "./native-bindings"; +// Stamp EXECUTOR_CLIENT=cli (unless the desktop app already set `desktop`) +// before `@executor-js/local` builds its integrations User-Agent from it. +import "./client-env"; + import { randomUUID } from "node:crypto"; import { existsSync, realpathSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; diff --git a/apps/local/src/installation.ts b/apps/local/src/installation.ts index 301f3ac23..67fde2658 100644 --- a/apps/local/src/installation.ts +++ b/apps/local/src/installation.ts @@ -15,13 +15,19 @@ const resolveChannel = (version: string): InstallationChannel => { return "stable"; }; -// The desktop main process sets `EXECUTOR_CLIENT=desktop` when it spawns the -// sidecar. Otherwise this headless apps/local server was launched by the -// `executor` CLI (`executor mcp`, `web`, `daemon run --foreground`, or the -// installed background service), so it reports as `cli`. Surface is only ever -// `cli` or `desktop`; mirrors the owner.client resolution in apps/cli/src/main.ts. -const resolveClient = (): SurfaceClient => - process.env.EXECUTOR_CLIENT === "desktop" ? "desktop" : "cli"; +// EXECUTOR_CLIENT identifies which product launched this headless apps/local +// server: the desktop app sets `desktop` before spawning it, and the executor +// CLI sets `cli` (apps/cli stamps it at startup, see src/client-env.ts). It is +// therefore always set to a valid surface in a real launch, so we pass it +// through rather than guess. The `cli` floor only covers direct library/test +// imports that boot the server without going through a launcher. +const SURFACE_CLIENTS = ["cli", "desktop"] as const; +const resolveClient = (): SurfaceClient => { + const client = process.env.EXECUTOR_CLIENT; + return (SURFACE_CLIENTS as readonly string[]).includes(client ?? "") + ? (client as SurfaceClient) + : "cli"; +}; export const CHANNEL: InstallationChannel = resolveChannel(LOCAL_VERSION); export const VERSION: string = LOCAL_VERSION;