f13fd1157b
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Local agents run through the `claude_local` adapter, which spawns Claude Code and routes its Anthropic API traffic through a shared proxy (e.g. better-ccflare). > - That proxy's request log feeds observability dashboards that break down cost and token usage. > - But requests from local agents arrive without an agent identifier, so all traffic collapses into a single unattributed bucket and per-agent telemetry is impossible. > - Anthropic's CLI forwards `ANTHROPIC_CUSTOM_HEADERS` onto each API request, and better-ccflare now reads an `X-Anthropic-Agent-Id` header to attribute requests per agent. > - This pull request stamps the agent's id into `ANTHROPIC_CUSTOM_HEADERS` as `X-Anthropic-Agent-Id`, injected into the env the adapter actually forwards to the spawned process. > - The benefit is per-agent cost/token attribution in proxy-backed dashboards, with zero impact on agents that don't run behind such a proxy. ## Linked Issues or Issue Description No public GitHub issue exists; describing inline per CONTRIBUTING.md (feature template). **Problem or motivation** When several `claude_local` agents share a proxy (better-ccflare) in front of the Anthropic API, the proxy cannot tell which agent issued a given request. Per-agent cost and token dashboards therefore can't be built — every request is attributed to one undifferentiated bucket. **Proposed solution** Have the `claude_local` adapter stamp each spawned Claude Code process with `ANTHROPIC_CUSTOM_HEADERS: X-Anthropic-Agent-Id: <agentId>`. The Anthropic CLI forwards that header on every API call, so the proxy can attribute requests to the specific agent. **Alternatives considered** Parsing per-agent identity from workspace paths or session ids in the proxy — brittle and proxy-specific. A first-class request header is the stable contract; it pairs with better-ccflare's `X-Anthropic-Agent-Id` support ([tombii/better-ccflare#260](https://github.com/tombii/better-ccflare/pull/260)) and dashboards like [danieltamas/axon](https://github.com/danieltamas/axon). **Roadmap alignment** Additive observability glue for an existing adapter; no overlap with planned core work. ## What Changed - Added `server/src/adapters/claude-agent-id-header.ts` exporting `stampClaudeAgentIdHeader`, which wraps the `claude_local` execute and merges `X-Anthropic-Agent-Id: <agentId>` into the run's `config.env.ANTHROPIC_CUSTOM_HEADERS`. - `server/src/adapters/registry.ts`: the `claude_local` adapter now uses the wrapped execute (`stampClaudeAgentIdHeader(claudeExecute)`). - The header is merged into **`config.env`** — the env the Claude adapter actually forwards into the spawned process — rather than `agent.adapterConfig.env`, which is resolved upstream before `execute` runs and is never read by the Claude adapter. - `agentId` is sanitized (CR/LF stripped, capped at 256 chars) before interpolation to prevent HTTP header injection. - Passthrough when no agent id is available; a pre-existing `X-Anthropic-Agent-Id` (manual override) is respected and not duplicated. - Added `server/src/adapters/claude-agent-id-header.test.ts` covering injection into `config.env`, append-to-existing `ANTHROPIC_CUSTOM_HEADERS`, CR/LF sanitization, length bounding, no-agent-id passthrough, and the duplicate-header guard. ## Verification - Unit tests: `cd server && npx vitest run src/adapters/claude-agent-id-header.test.ts` → 6 passing. - CI: typecheck, build, server suites, and Greptile gates run on this PR. - Manual: configure a `claude_local` agent behind better-ccflare, run a heartbeat, and confirm the proxy log shows `X-Anthropic-Agent-Id: <agentId>` on that agent's requests. An agent that already sets `X-Anthropic-Agent-Id` in its adapter config keeps its override (no duplicate line). ## Risks Low risk. The change is additive and non-breaking: - When no agent id is present, or when an `X-Anthropic-Agent-Id` is already configured, execution is byte-for-byte unchanged (the original `ctx` is passed straight through). - The header only has an effect for deployments whose proxy reads it; agents without such a proxy are unaffected. - The agent id is sanitized (CR/LF removed, length-bounded) before it is interpolated into a header value, preventing header injection. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. This is additive observability glue for an existing adapter and does not overlap planned core work. ## Model Used Claude (Anthropic) — Claude Opus 4.8 (`claude-opus-4-8`), via the Claude Code CLI with extended reasoning and tool use. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots (N/A — no UI changes) - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: zenprocess <zenprocess@users.noreply.github.com> Co-authored-by: Devin Foley <devin@paperclip.ing> Co-authored-by: Paperclip <noreply@paperclip.ing>