From f80d21fa0653a19c13deed476dbd9bd3291f3f3f Mon Sep 17 00:00:00 2001 From: Saket Aryan Date: Wed, 16 Sep 2026 02:49:11 +0530 Subject: [PATCH] fix(plugins): do not claim a host application the portable bundle cannot know Review point. The portable bundle is built with host "coding-agent", and the build wrote that straight into PLATFORM_APPLICATION, so every portable install sent X-Application: coding-agent. That value is not in the platform's allowlist, so it was already being dropped server-side. The effect was the worst of both: the wire claimed we knew the editor, the stored event recorded that we did not, and nothing said which was right. An absent header says the same thing honestly and costs a lookup. HARNESS_ID stays "coding-agent". It is the PostHog-side label, it is true, and grouping portable installs together there is useful. Native bundles are unchanged apart from the regenerated comment. Covered by two new build tests: portable declares no application, and each native names the host it was generated for. Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb --- integrations/AGENTS.md | 9 ++++++ integrations/agent-plugin-core/build/build.py | 16 ++++++++-- .../agent-plugin-core/tests/test_build.py | 32 +++++++++++++++++++ .../antigravity-plugin/core/_harness_id.py | 2 ++ .../claude-code-plugin/core/_harness_id.py | 2 ++ integrations/codex-plugin/core/_harness_id.py | 2 ++ .../cursor-plugin/core/_harness_id.py | 2 ++ integrations/kimi-plugin/core/_harness_id.py | 2 ++ .../mem0-agent-plugin/core/_harness_id.py | 4 ++- 9 files changed, 67 insertions(+), 4 deletions(-) diff --git a/integrations/AGENTS.md b/integrations/AGENTS.md index 9b85cddb1..221c8206f 100644 --- a/integrations/AGENTS.md +++ b/integrations/AGENTS.md @@ -82,6 +82,15 @@ The backend recognizes a fixed list of source values and buckets everything else into `OTHERS`. A new value has to land in the platform's `EventSource` enum, so do not invent one without that change going in too. +`X-Application` is allowlisted the same way, and this one has a rule of its own: +**omit the header when you do not know the host.** A value outside the allowlist +is discarded server-side, so guessing produces an event that claims an +attribution we do not actually have. The portable bundle is the case that +matters. It runs in whatever editor a user drops it into, so its build leaves +`PLATFORM_APPLICATION` empty and `memory_core` sends no header at all, while the +native bundles each name the host they were generated for. If you add a build +target, decide which of those two it is. + ## Adding an integration 1. For a native coding-agent host, add `integrations/-plugin/` with `plugin-build.json`, its manifest, and a thin adapter, then generate its shared runtime. Portable clients use the single `mem0-agent-plugin/` package. Independent TypeScript integrations stay self-contained and import shared lifecycle behavior from `agent-plugin-core/typescript/`. diff --git a/integrations/agent-plugin-core/build/build.py b/integrations/agent-plugin-core/build/build.py index e675efb02..3059b9b1f 100644 --- a/integrations/agent-plugin-core/build/build.py +++ b/integrations/agent-plugin-core/build/build.py @@ -81,15 +81,23 @@ def replace_output(staged: Path, output: Path) -> Path: return output -def _render_harness_id(host: str) -> str: +def _render_harness_id(host: str, *, portable: bool = False) -> str: """Emit core/_harness_id.py for one host. Carries both vocabularies from a single definition: the PostHog `source` tag and the platform's X-Mem0-Source / X-Application pair. Keeping them together is what stops the two from drifting into separate vocabularies for the same thing. + + The portable bundle runs in whatever editor a user drops it into, so it does + not know its host and must not guess one. HARNESS_ID stays "coding-agent", + which is true and useful for grouping in PostHog, but PLATFORM_APPLICATION is + left empty: X-Application names a real host app, is checked against an + allowlist server-side, and a value that is always discarded is worse than no + value -- it reads like an attribution we have and do not. """ tag = host.upper().replace("-", "_") + "_PLUGIN" + application = "" if portable else host return ( '"""Generated by integrations/agent-plugin-core/build/build.py. Do not edit."""\n' "\n" @@ -98,8 +106,10 @@ def _render_harness_id(host: str) -> str: "\n" "# Platform-side vocabulary (mem0_event.source + X-Application). The whole\n" "# plugin family is one source; which editor it runs in is the application.\n" + "# An empty application means the host is unknown, and memory_core omits\n" + "# the header entirely rather than sending a placeholder.\n" 'PLATFORM_SOURCE = "MEM0_PLUGIN"\n' - f'PLATFORM_APPLICATION = "{host}"\n' + f'PLATFORM_APPLICATION = "{application}"\n' ) @@ -122,7 +132,7 @@ def _bundle_python( # to call telemetry.init(). mcp_server.py and the detached telemetry.py sender # never did, which is how MCP searches reported harness=generic and every # batch they drained was labelled MEM0_PLUGIN regardless of the real host. - (core / "_harness_id.py").write_text(_render_harness_id(host), encoding="utf-8") + (core / "_harness_id.py").write_text(_render_harness_id(host, portable=portable), encoding="utf-8") values = { "PLUGIN_ROOT": plugin_root, diff --git a/integrations/agent-plugin-core/tests/test_build.py b/integrations/agent-plugin-core/tests/test_build.py index 54b5e5c6a..23295cc38 100644 --- a/integrations/agent-plugin-core/tests/test_build.py +++ b/integrations/agent-plugin-core/tests/test_build.py @@ -70,6 +70,38 @@ def test_portable_bundle_is_conformant_and_self_contained(tmp_path: Path) -> Non assert not any(path.is_symlink() for path in root.rglob("*")) +def _harness_identity(root: Path) -> dict[str, str]: + """Read the generated core/_harness_id.py without importing it.""" + values: dict[str, str] = {} + for line in (root / "core" / "_harness_id.py").read_text(encoding="utf-8").splitlines(): + if "=" in line and not line.lstrip().startswith("#"): + name, _, raw = line.partition("=") + values[name.strip()] = raw.strip().strip('"') + return values + + +def test_the_portable_bundle_declares_no_host_application(tmp_path: Path) -> None: + """It runs in whatever editor a user drops it into, so it cannot know the host. + + X-Application is allowlisted server-side. A guessed value is silently dropped + there, which is the worst outcome: the wire says we know the host and the + stored event says we do not. + """ + identity = _harness_identity(build("mem0-agent-plugin", "portable", tmp_path / "portable")) + + assert identity["PLATFORM_APPLICATION"] == "" + # The PostHog-side label is still useful for grouping and stays populated. + assert identity["HARNESS_ID"] == "coding-agent" + assert identity["PLATFORM_SOURCE"] == "MEM0_PLUGIN" + + +@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"]) +def test_a_native_bundle_names_the_host_it_was_built_for(host: str, tmp_path: Path) -> None: + identity = _harness_identity(build(host, "native", tmp_path / host)) + + assert identity["PLATFORM_APPLICATION"] == host + + @pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"]) def test_native_bundle_is_self_contained(host: str, tmp_path: Path) -> None: root = build(host, "native", tmp_path / host) diff --git a/integrations/antigravity-plugin/core/_harness_id.py b/integrations/antigravity-plugin/core/_harness_id.py index 2c8a515f4..18b7f68ec 100644 --- a/integrations/antigravity-plugin/core/_harness_id.py +++ b/integrations/antigravity-plugin/core/_harness_id.py @@ -5,5 +5,7 @@ SOURCE_TAG = "ANTIGRAVITY_PLUGIN" # Platform-side vocabulary (mem0_event.source + X-Application). The whole # plugin family is one source; which editor it runs in is the application. +# An empty application means the host is unknown, and memory_core omits +# the header entirely rather than sending a placeholder. PLATFORM_SOURCE = "MEM0_PLUGIN" PLATFORM_APPLICATION = "antigravity" diff --git a/integrations/claude-code-plugin/core/_harness_id.py b/integrations/claude-code-plugin/core/_harness_id.py index 6c3e1ce15..9c4949408 100644 --- a/integrations/claude-code-plugin/core/_harness_id.py +++ b/integrations/claude-code-plugin/core/_harness_id.py @@ -5,5 +5,7 @@ SOURCE_TAG = "CLAUDE_CODE_PLUGIN" # Platform-side vocabulary (mem0_event.source + X-Application). The whole # plugin family is one source; which editor it runs in is the application. +# An empty application means the host is unknown, and memory_core omits +# the header entirely rather than sending a placeholder. PLATFORM_SOURCE = "MEM0_PLUGIN" PLATFORM_APPLICATION = "claude-code" diff --git a/integrations/codex-plugin/core/_harness_id.py b/integrations/codex-plugin/core/_harness_id.py index 152313a21..47938ee08 100644 --- a/integrations/codex-plugin/core/_harness_id.py +++ b/integrations/codex-plugin/core/_harness_id.py @@ -5,5 +5,7 @@ SOURCE_TAG = "CODEX_PLUGIN" # Platform-side vocabulary (mem0_event.source + X-Application). The whole # plugin family is one source; which editor it runs in is the application. +# An empty application means the host is unknown, and memory_core omits +# the header entirely rather than sending a placeholder. PLATFORM_SOURCE = "MEM0_PLUGIN" PLATFORM_APPLICATION = "codex" diff --git a/integrations/cursor-plugin/core/_harness_id.py b/integrations/cursor-plugin/core/_harness_id.py index 40241a7f4..0e20bf1ff 100644 --- a/integrations/cursor-plugin/core/_harness_id.py +++ b/integrations/cursor-plugin/core/_harness_id.py @@ -5,5 +5,7 @@ SOURCE_TAG = "CURSOR_PLUGIN" # Platform-side vocabulary (mem0_event.source + X-Application). The whole # plugin family is one source; which editor it runs in is the application. +# An empty application means the host is unknown, and memory_core omits +# the header entirely rather than sending a placeholder. PLATFORM_SOURCE = "MEM0_PLUGIN" PLATFORM_APPLICATION = "cursor" diff --git a/integrations/kimi-plugin/core/_harness_id.py b/integrations/kimi-plugin/core/_harness_id.py index aa17750b1..83ab061ef 100644 --- a/integrations/kimi-plugin/core/_harness_id.py +++ b/integrations/kimi-plugin/core/_harness_id.py @@ -5,5 +5,7 @@ SOURCE_TAG = "KIMI_PLUGIN" # Platform-side vocabulary (mem0_event.source + X-Application). The whole # plugin family is one source; which editor it runs in is the application. +# An empty application means the host is unknown, and memory_core omits +# the header entirely rather than sending a placeholder. PLATFORM_SOURCE = "MEM0_PLUGIN" PLATFORM_APPLICATION = "kimi" diff --git a/integrations/mem0-agent-plugin/core/_harness_id.py b/integrations/mem0-agent-plugin/core/_harness_id.py index b0836d6de..3efd000f8 100644 --- a/integrations/mem0-agent-plugin/core/_harness_id.py +++ b/integrations/mem0-agent-plugin/core/_harness_id.py @@ -5,5 +5,7 @@ SOURCE_TAG = "CODING_AGENT_PLUGIN" # Platform-side vocabulary (mem0_event.source + X-Application). The whole # plugin family is one source; which editor it runs in is the application. +# An empty application means the host is unknown, and memory_core omits +# the header entirely rather than sending a placeholder. PLATFORM_SOURCE = "MEM0_PLUGIN" -PLATFORM_APPLICATION = "coding-agent" +PLATFORM_APPLICATION = ""