Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ jobs:
run: npx --yes @anthropic-ai/claude-code@2.1.236 plugin validate .
- name: Test Codex Logfire exporter hooks
run: python3 -m unittest discover -s plugins/logfire-exporter/tests -v
- name: Test Claude connection runtime
- name: Test shared runtime and Claude connection
run: |
python3 -m pip install uv==0.13.0
python3 plugins/logfire/scripts/claude_runtime.py --data-root "${RUNNER_TEMP}/logfire-connection-runtime" --install
LOGFIRE_CLAUDE_TEST_CLI="${RUNNER_TEMP}/logfire-connection-runtime/bin/logfire-cli" npx --yes --package @anthropic-ai/claude-code@2.1.236 -c 'python3 -m unittest discover -s plugins/logfire/tests -v'
python3 plugins/logfire/scripts/agent_runtime.py --data-root "${RUNNER_TEMP}/logfire-connection-runtime" --install
LOGFIRE_CONNECTION_TEST_CLI="${RUNNER_TEMP}/logfire-connection-runtime/bin/logfire-cli" npx --yes --package @anthropic-ai/claude-code@2.1.236 -c 'python3 -m unittest discover -s plugins/logfire/tests -v'
- name: Test installed Codex plugin and native CLI capture lifecycle
run: LOGFIRE_CONNECTION_TEST_CLI="${RUNNER_TEMP}/logfire-connection-runtime/bin/logfire-cli" npx --yes --package @openai/codex@0.159.3 -c 'python3 -m unittest discover -s plugins/logfire/tests -p "test_codex*.py" -v'

publish-agent-skills:
name: Publish Agent Skills
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ A daily workflow (`.github/workflows/sync-from-upstream.yml`) clones each upstre

**Anything you add directly inside a synced skill directory will be wiped on the next sync.** To change synced skill content, send a PR upstream. Everything else in this repo (plugin metadata, repo-root files, `.github/`, `scripts/`) is fine to edit here.

`plugins/logfire/skills/logfire-codex-connection` is plugin-owned onboarding,
not a mirrored library specialist. Its runtime and native-host instructions are
maintained here and are not part of the standalone skill catalog or upstream sync.

## Adding a new skill

1. Ensure the skill exists in the library at `<package-dir>/.agents/skills/<skill-name>/`.
Expand Down
8 changes: 7 additions & 1 deletion plugins/logfire/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "logfire",
"version": "0.1.5",
"version": "0.1.8",
"description": "Instrument applications and infrastructure, investigate telemetry, and manage Pydantic Logfire workflows across Python, TypeScript, Rust, or any language.",
"author": {
"name": "Pydantic",
Expand All @@ -23,6 +23,11 @@
],
"skills": "./skills/",
"mcpServers": "./.codex-plugin/mcp.json",
"extensions": {
"com.openai": {
"onboardingSkill": "./skills/logfire-codex-connection/SKILL.md"
}
},
"interface": {
"displayName": "Logfire",
"shortDescription": "Investigate telemetry and add Logfire instrumentation.",
Expand All @@ -37,6 +42,7 @@
"privacyPolicyURL": "https://pydantic.dev/privacy/",
"termsOfServiceURL": "https://pydantic.dev/terms/",
"defaultPrompt": [
"Connect Codex to Logfire.",
"Help me add Logfire instrumentation to this app.",
"Investigate recent Logfire errors for this service.",
"Find slow endpoints in Logfire and suggest fixes."
Expand Down
32 changes: 22 additions & 10 deletions plugins/logfire/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ OAuth; those are separate actions. This flow installs no transcript exporter.

## Install In Codex

After installing, run plugin setup or ask "Connect Codex to Logfire."
The `logfire-codex-connection` skill guides MCP access and optional recording,
and handles status or disconnect requests. Query-only needs no Logfire CLI.
Recording offers full, messages-without-tools, or metadata-only capture and
uses the supported native CLI to configure native telemetry plus pinned
conversation enrichment. Python 3 and uv are needed for the managed CLI install.
Restart Codex after changing recording; uninstalling the main plugin does not
stop native recording. Ask "Stop recording my Codex sessions" to remove it while
keeping MCP and skills. Setup leaves model-provider and Gateway settings alone.

Add the published Pydantic marketplace to Codex:

```bash
Expand Down Expand Up @@ -141,16 +151,18 @@ If you installed from Anthropic's `claude-plugins-official` marketplace instead,
name. That marketplace pins a specific commit of this repository, so a release can take longer to
appear there than on `pydantic-skills`, which tracks `main`.

**Codex** users can switch by replacing the MCP entry and re-authenticating:

```bash
codex mcp remove logfire
codex mcp add logfire --url https://logfire-eu.pydantic.dev/mcp
codex mcp login logfire
codex mcp get logfire
```

Start a new Codex conversation after switching so the MCP tools reload.
**Codex** users can ask "Connect Logfire to my EU account" or provide their
self-hosted MCP URL. The plugin's setup skill disables only the bundled US MCP
server, keeps the plugin and skills enabled, and configures one native regional
connection. Existing matching connections are reused; replacing another connection
requires confirmation. The URL of a bundled server cannot be overridden through
Codex's plugin policy or `LOGFIRE_MCP_URL`.

See the [regional MCP workflow](skills/logfire-codex-connection/references/mcp.md)
for the native commands and scoped policy helper. Restart/reload Codex after
switching, authenticate the selected connection, and verify access. Returning to
US removes only the agreed regional connection before re-enabling the bundled
server. MCP changes do not change session recording or Gateway routing.

**Cursor** installs are a local copy, so edit the `url` in
`~/.cursor/plugins/local/logfire/mcp.json` and reload the window.
Expand Down
2 changes: 1 addition & 1 deletion plugins/logfire/commands/connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ turn native capture on or off. Never infer consent from MCP authentication.
After consent, locate the plugin's persistent runtime:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/claude_runtime.py"
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/agent_runtime.py"
```

If it reports `runtime_required`, explain that the plugin needs a persistent
Expand Down
2 changes: 1 addition & 1 deletion plugins/logfire/commands/disconnect.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ capture, disconnect MCP, or both. If MCP-only, use Claude's native controls and
skip the telemetry runtime entirely. Do not make users stop capture just to
disconnect MCP, or clear their MCP login when they only asked to stop capture.

Run `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/claude_runtime.py"` without
Run `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/agent_runtime.py"` without
`--install`. Using its returned executable and data directory:

1. Preview `--beta --no-input --output json agent claude --data-dir <data_dir> remove --dry-run`.
Expand Down
2 changes: 1 addition & 1 deletion plugins/logfire/commands/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Check the two connections separately. This command is for Claude Code.
without changing the server or opening a new login flow.
MCP configuration commands may exit zero even when connection failed; inspect
their reported status, not just the process exit code.
2. Run `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/claude_runtime.py"` without
2. Run `python3 "${CLAUDE_PLUGIN_ROOT}/scripts/agent_runtime.py"` without
`--install`. This must not install dependencies during a status check.
3. If ready, use its returned executable to run
`--beta --no-input --output json agent claude --data-dir <data_dir> status`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Locate or explicitly install a persistent CLI for Claude's native telemetry helper."""
"""Locate or explicitly install a persistent CLI for native agent telemetry."""

import argparse
import hashlib
Expand Down Expand Up @@ -38,15 +38,16 @@ def compatible(executable):
return False


def prepare(root, install=False):
"""Do not install, authenticate, or modify Claude settings during a status check."""
config = Path(os.environ.get("CLAUDE_CONFIG_DIR") or Path.home() / ".claude")
def prepare(root, install=False, *, agent="claude"):
"""Do not install, authenticate, or modify agent settings during a status check."""
variable = {"claude": "CLAUDE_CONFIG_DIR", "codex": "CODEX_HOME"}[agent]
config = Path(os.environ.get(variable) or Path.home() / f".{agent}")
if not config.is_absolute():
return {
"status": "invalid_config_dir",
"message": "CLAUDE_CONFIG_DIR must be absolute.",
"message": f"{variable} must be absolute.",
}
# Separate SDK destinations for independent Claude installations. Retargeting
# Separate SDK destinations for independent agent installations. Retargeting
# one configuration must not invalidate another installation's bound helper.
identity = hashlib.sha256(os.fsencode(config)).hexdigest()
data_dir = str(root / "projects" / identity)
Expand All @@ -67,7 +68,7 @@ def prepare(root, install=False):
"message": "Install uv, then retry connection setup.",
}
# uv tool environments survive cache cleanup; uvx environments do not. Keep this
# outside the plugin cache/data, which Claude can delete during updates/uninstall.
# outside the plugin cache/data, which the host can delete during updates/uninstall.
environment = {
**os.environ,
"UV_TOOL_DIR": str(root / "tools"),
Expand Down Expand Up @@ -106,13 +107,14 @@ def main():
action="store_true",
help="Allow installing a compatible CLI after user consent",
)
parser.add_argument(
"--data-root", type=Path, default=Path.home() / ".logfire" / "claude-plugin"
)
parser.add_argument("--agent", choices=("claude", "codex"), default="claude")
parser.add_argument("--data-root", type=Path)
args = parser.parse_args()
if args.data_root is None:
args.data_root = Path.home() / ".logfire" / f"{args.agent}-plugin"
if not args.data_root.is_absolute():
parser.error("--data-root must be absolute")
report = prepare(args.data_root, args.install)
report = prepare(args.data_root, args.install, agent=args.agent)
print(json.dumps(report))
return 0 if report["status"] == "ready" else 1

Expand Down
154 changes: 154 additions & 0 deletions plugins/logfire/scripts/codex_mcp_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Toggle only the installed Logfire plugin's bundled MCP server via Codex's config API."""

import argparse
import json
import os
import queue
import shutil
import subprocess
import sys
import threading
import time


def set_policy(plugin_id, enabled):
codex = shutil.which("codex")
if codex is None:
raise RuntimeError("Install the Codex CLI before changing MCP settings.")
environment = {
key: value
for key, value in os.environ.items()
if key not in {"LOGFIRE_API_KEY", "LOGFIRE_TOKEN", "LOGFIRE_MCP_TOKEN"}
}
installed = subprocess.run(
[codex, "plugin", "list", "--json"],
env=environment,
capture_output=True,
text=True,
timeout=30,
check=False,
)
if installed.returncode:
raise RuntimeError(
"Could not inspect installed Codex plugins; settings unchanged."
)
if not plugin_id.startswith("logfire@") or not any(
item["pluginId"] == plugin_id
for item in json.loads(installed.stdout)["installed"]
):
raise RuntimeError(
"Select the exact installed Logfire plugin ID; settings unchanged."
)
process = subprocess.Popen(
[codex, "app-server"],
env=environment,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
responses = queue.Queue()

def read_responses():
try:
for line in process.stdout:
responses.put(json.loads(line))
except ValueError:
pass
finally:
responses.put(None)

reader = threading.Thread(target=read_responses, daemon=True)
reader.start()

def request(identifier, method, params):
process.stdin.write(
json.dumps({"id": identifier, "method": method, "params": params}) + "\n"
)
process.stdin.flush()
deadline = time.monotonic() + 30
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise RuntimeError(
"Codex did not confirm the configuration operation in time."
)
response = responses.get(timeout=remaining)
if response is None:
raise RuntimeError("Codex closed the configuration connection.")
if response.get("id") == identifier:
if "error" in response:
raise RuntimeError("Codex rejected the configuration operation.")
return

try:
request(
1,
"initialize",
{"clientInfo": {"name": "logfire-plugin-setup", "version": "1"}},
)
request(
2,
"config/value/write",
{
"keyPath": f"plugins.{json.dumps(plugin_id)}.mcp_servers.logfire.enabled",
"value": enabled,
"mergeStrategy": "replace",
},
)
finally:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
reader.join(timeout=5)
process.stdin.close()
process.stdout.close()


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("enable", "disable"))
parser.add_argument("--plugin-id", required=True)
parser.add_argument(
"--yes", action="store_true", help="Confirm this user-wide MCP policy change"
)
args = parser.parse_args()
if not args.yes:
parser.error("Confirm the MCP change with the user, then pass --yes.")
try:
set_policy(args.plugin_id, args.action == "enable")
except (
RuntimeError,
OSError,
ValueError,
KeyError,
TypeError,
queue.Empty,
subprocess.TimeoutExpired,
) as error:
# Do not expose child output or configuration. A write can have succeeded
# before a lost response; callers must inspect state before retrying.
message = (
str(error)
if isinstance(error, RuntimeError)
else "Could not complete the Codex configuration operation."
)
print(json.dumps({"status": "check_required", "message": message}))
return 1
print(
json.dumps(
{
"status": "configured",
"plugin_id": args.plugin_id,
"bundled_mcp_enabled": args.action == "enable",
}
)
)
return 0


if __name__ == "__main__":
sys.exit(main())
33 changes: 33 additions & 0 deletions plugins/logfire/skills/logfire-codex-connection/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
name: logfire-codex-connection
description: Connect Codex itself to Logfire, check connection status, or stop session recording. Use for this plugin's MCP access and optional native Codex telemetry, not application instrumentation or AI Gateway routing.
---

# Connect Codex to Logfire

Help the user connect, check status, or disconnect without making them run a
setup checklist. Honor the requested operation; a status request never grants
permission to install, authenticate, or repair anything.

For connection, ask only if missing: "Would you like to query Logfire, record
your Codex sessions, or both?" MCP access and recording are independent. Do not
require CLI login for working MCP, or MCP read access to record sessions.

- **Connect:** follow [connection setup](references/connect.md).
- **Status:** follow [status and removal](references/status.md#status).
- **Disconnect:** follow [status and removal](references/status.md#disconnect).

Resolve bundled scripts relative to this installed skill directory. The runtime
helper is `../../scripts/agent_runtime.py --agent codex`; the explicit agent
selects Codex's separate runtime and project credential directory.
Quote paths as single arguments. Do not assume
`CLAUDE_PLUGIN_ROOT` exists in Codex.

Leave telemetry configuration and exporter installation to the native Logfire
CLI. Do not add custom transcript hooks, copy credentials, open secret-bearing
Codex/exporter configuration files, or start a nested coding session for setup.
Keep existing application instrumentation, model providers, and Gateway routing
unchanged. Plugin installation is not consent to session capture.

Finish with MCP access, capture mode/destination, and delivery evidence as
separate facts. Give one next action. "Configured" does not mean data arrived.
Loading
Loading