Skip to content

feat(ingest): let a scoped ingest key authenticate /v1/* and /api/v1/*, so an agent off-box can be observed #5679

Description

@vivekchand

Problem

An agent is observable by ClawMetry only if the daemon runs on the same machine as the agent. That excludes CI runners, containers, serverless, hosted agent products, and a teammate's laptop — i.e. most agents that cost real money.

Verified in tree:

  • dashboard.py::_check_auth gates /v1/* on OPENCLAW_GATEWAY_TOKEN; loopback is trusted, and the only escape hatch is CLAWMETRY_OTLP_ALLOW_UNAUTH=1 (a LAN-wide open door, not a credential).
  • docs/CUSTOM_RUNTIME_INGEST.md: the write API authenticates on one static CLAWMETRY_INGEST_TOKEN shared by the whole install — no rotation, no revocation, no attribution.
  • clawmetry/endpoints.py: ingest.clawmetry.com accepts E2E-sealed snapshots from an authenticated daemon and nothing else, so there is no key a remote agent could hold.

Competitive context: Bronto's entire agent story is one public OTLP endpoint plus x-bronto-api-key. That is why they can publish an OpenClaw integration page and we cannot publish a CI one.

What already works — do not rebuild it

  • dashboard.py::_otlp_decode already handles protobuf, OTLP/JSON and gzip, keyed off Content-Type / Content-Encoding. Wire coverage is at parity with Bronto today; it is simply undocumented and loopback-scoped.
  • _process_otlp_traces already recognises gen_ai.operation.name and slugs a foreign service.name into its own runtime rather than mis-bucketing it as openclaw.
  • PR feat: build your own UI, a keyed and scoped read API over the q/1 contract #5676 ships clawmetry/apikeys.py: cmk_<id>_<secret>, sha256 at rest in ~/.clawmetry/api_keys.json (0600), create / verify / revoke / list_keys / scope_catalogue, and the clawmetry key CLI.

This issue is the write half of #5676, not a parallel key system.

Scope

  1. A write scope. Add write:ingest to apikeys.SCOPES. Careful: scope_catalogue() derives methods from query_contract.live_methods_by_scope, which is read-only by construction. A write scope maps to no q/1 shape, so the catalogue needs a branch that says so rather than rendering an empty method list that reads like a bug.
  2. Key auth as an additional path. Accept x-clawmetry-key on /v1/logs, /v1/metrics, /v1/traces and /api/v1/runs*. Loopback and the gateway token keep working unchanged — this adds a door, it does not move the existing one.
  3. No CORS, POST only. Ingest is server-to-server. Do not extend routes/public_api.py::_add_cors; it is deliberately pinned to /api/q/. A browser-resident ingest key is not a use case, and making it one re-opens the hole feat: build your own UI, a keyed and scoped read API over the q/1 contract #5676 closed.
  4. Routing headers, resolved once per request and stamped on every event in it:
    • x-clawmetry-runtime — validated against entitlements.ALL_RUNTIMES, with an explicit free-form fallback for custom engines (the write API already accepts arbitrary runtime strings).
    • x-clawmetry-env — environment/project string, [a-z0-9_-]{1,64}. This is the one grouping axis above runtime; resist growing it into Bronto's dataset/collection/tags taxonomy.
  5. Documented limits and honest failures. Return, don't 500: 200 accepted, 400 malformed, 401 bad/absent key, 403 key lacks write:ingest, 413 over size, 429 over rate. Caps to publish: 10 MB body, 1000 events per batch (already the custom-runtime cap).
  6. CLI: clawmetry key create --scope write:ingest prints a curl that actually works against the local dashboard.

Non-goals

Regional endpoint split (Bronto's own troubleshooting page shows region-scoped keys are their most common support failure). Per-key rate limiting — that belongs on the hosted side. A base "any format" endpoint. CORS.

Traps

  • _check_auth returns early for /api/q/ precisely so the stricter gate in routes/public_api.py is the only gate. Do the same here — one gate, in the ingest module — rather than layering a second check on top of the gateway-token check.
  • Pushed events must go through local_store.ingest, so secret redaction and dedupe apply to foreign data exactly as they do to detected data. Do not add a bypass for speed.
  • A new OSS route with no cloud_route_policy entry is a SystemExit in apply_policy and cloud will not boot. Land the cloud-side policy classification in the same window (see the paired cloud issue).

Acceptance

  • An agent running in a GitHub Actions container, with no ClawMetry daemon anywhere on the machine, pushes OTLP with a key and appears as a live session with correct token cost.
  • Revoking the key stops ingest within one request.
  • Loopback with no key behaves exactly as it does today — regression test, not manual check.
  • A read:metrics key gets 403 on ingest; a write:ingest key gets 403 on /api/q/1/*. Both pinned by test.
  • x-clawmetry-env is filterable in at least one tab, or the header is not shipped.

Depends on #5676.

Part of the ingest-gap plan (phase 1, item 1 of 5).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions