# Contribute telemetry to Prowl

The contract: any HTTP-client wrapper, MCP shim, or runtime hook fires
`POST /v1/telemetry/observe` after every outbound agent call. The agent
itself never sees the mechanism. We ship four reference wrappers — pick
the one that fits your stack.

| Wrapper | Best for | Where |
|---|---|---|
| **Claude Code hook** | Anyone using `claude` CLI day-to-day | `scripts/prowl_telemetry_hook.py` (this repo) |
| **Python SDK middleware** | Python agent runtimes using httpx | `pip install prowl-client` → `prowl.observe(httpx_client)` |
| **LangChain callback** | Any LangChain-based agent | `examples/langchain_telemetry.py` (this repo) |
| **mcp-prowl shim** *(coming)* | Cursor / Continue / Cline / any MCP client | `npm i mcp-prowl` |

## What gets sent

For each successful `WebFetch`:

```json
{
  "host": "api.stripe.com",
  "method": "GET",
  "status": null,
  "latency_ms": 312,
  "response_bytes": 2048,
  "source": "claude-code-hook"
}
```

What never leaves your machine:
- The URL path or query string (only the **host** is reported).
- The response body.
- Any header values (no auth, no cookies, no client state).
- Hosts on the default denylist: localhost, RFC 1918 ranges, `.local`,
  `.internal`, cloud metadata endpoints.

## Install

1. Make the script available somewhere stable. Easiest if you already have
   the `agentdir` repo cloned:

   ```bash
   chmod +x /path/to/agentdir/scripts/prowl_telemetry_hook.py
   ```

   Otherwise drop the file at e.g. `~/.claude/hooks/prowl_telemetry_hook.py`.

2. Add to `~/.claude/settings.json`:

   ```json
   {
     "hooks": {
       "PreToolUse": [
         {
           "matcher": "WebFetch",
           "hooks": [{
             "type": "command",
             "command": "/abs/path/to/prowl_telemetry_hook.py"
           }]
         }
       ],
       "PostToolUse": [
         {
           "matcher": "WebFetch",
           "hooks": [{
             "type": "command",
             "command": "/abs/path/to/prowl_telemetry_hook.py"
           }]
         }
       ]
     }
   }
   ```

3. (Optional, recommended) Add your agent key so reports carry weight
   1.0 instead of 0.1–0.3:

   ```bash
   # in ~/.bashrc or ~/.zshrc
   export PROWL_AGENT_KEY="ak_..."
   ```

   Get one via `POST https://prowl.world/v1/auth/agents/register`
   (free, no PII).

## Env-var reference

| Var | Default | Effect |
|---|---|---|
| `PROWL_TELEMETRY_URL` | `https://prowl.world/v1/telemetry/observe` | Where to POST. Override for self-hosted Prowl. |
| `PROWL_AGENT_KEY` | _(unset)_ | If set, attaches your `ak_` key. Anonymous reports still work, weighted lower. |
| `PROWL_TELEMETRY_ALLOWLIST` | _(unset)_ | Comma-separated host substrings. When set, **only** matching hosts are reported. |
| `PROWL_TELEMETRY_DENYLIST` | _(see script)_ | Extra hosts to never report on. Augments the built-in denylist. |
| `PROWL_TELEMETRY_DEBUG` | _(unset)_ | If set to a path, the raw stdin event is appended there for inspection. |
| `PROWL_TELEMETRY_DRY_RUN` | _(unset)_ | If set, prints the outgoing payload to stderr instead of POSTing. For local testing. |
| `PROWL_TELEMETRY_HASH_ONLY` | _(unset)_ | If set, the host is hashed server-side (`hash_host=true`). Trades per-host evidence for stronger privacy. Aggregate counts still work. |

## Python SDK middleware

Drop-in for any agent runtime using `httpx`:

```python
import httpx, prowl

# Wrap once. The client is unchanged from your perspective.
http = prowl.observe(
    httpx.AsyncClient(),
    agent_key="ak_...",       # optional — anonymous reports also accepted
    source="my-agent",        # any label
    allowlist=["stripe.com"], # optional — restrict to specific hosts
    hash_host=False,          # set True for hash-only mode
)

# Use exactly like before. Every successful or failed call
# fires a fire-and-forget telemetry report.
resp = await http.get("https://api.stripe.com/v1/charges")
```

Works for `httpx.Client` (sync) and `httpx.AsyncClient` (async). Multiple
calls to `observe()` on the same client are a no-op past the first.

## LangChain callback

```python
from examples.langchain_telemetry import ProwlTelemetryCallback
from langchain.agents import AgentExecutor

cb = ProwlTelemetryCallback(agent_key="ak_...")
agent_executor.invoke({"input": "..."}, config={"callbacks": [cb]})
```

The callback hooks `on_llm_start/end` and `on_tool_start/end`. LLM calls
are attributed by model name (claude → api.anthropic.com, gpt → openai,
gemini → google, etc.). Tool calls are attributed when the tool output
contains a URL.

## Batch endpoint

For high-volume agents, send up to 500 observations in one POST:

```http
POST /v1/telemetry/observe_batch
Content-Type: application/json
X-Agent-Key: ak_...

{
  "observations": [
    {"host": "api.stripe.com", "status": 200, "latency_ms": 312},
    {"host": "resend.com",     "status": 200, "latency_ms": 88},
    ...
  ]
}
```

Each item is processed and rate-limited individually. Invalid entries
are silently skipped (the response carries `accepted`, `skipped`, and
per-item `weights`). Hits the same 600/hr/agent ceiling as single-shot.

## Privacy posture

Telemetry from this hook is **opt-in by install**. We chose conservative
defaults: anonymous reports are accepted, hosts on internal ranges are
auto-denied, and only the registered domain (not paths, not query
strings, not headers, not bodies) leaves the machine. If you want
tighter control, set `PROWL_TELEMETRY_ALLOWLIST` to the few public APIs
you actually want measured.

The script's source is at
[`scripts/prowl_telemetry_hook.py`](../scripts/prowl_telemetry_hook.py) —
read it before installing.

## Why this exists

Continuous score signal for the agent ecosystem is the gateway thesis,
but the gateway needs the vendor to opt in. The telemetry hook is the
mirror image: the **agent's** side opts in, and the same kind of data
flows to Prowl without the vendor needing to do anything.

See [`prowl-gateway-reputation-plan.md`](./prowl-gateway-reputation-plan.md)
for the broader picture.
