You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
This issue is the write half of #5676, not a parallel key system.
Scope
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.
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.
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.
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).
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.
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_authgates/v1/*onOPENCLAW_GATEWAY_TOKEN; loopback is trusted, and the only escape hatch isCLAWMETRY_OTLP_ALLOW_UNAUTH=1(a LAN-wide open door, not a credential).docs/CUSTOM_RUNTIME_INGEST.md: the write API authenticates on one staticCLAWMETRY_INGEST_TOKENshared by the whole install — no rotation, no revocation, no attribution.clawmetry/endpoints.py:ingest.clawmetry.comaccepts 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_decodealready handles protobuf, OTLP/JSON and gzip, keyed offContent-Type/Content-Encoding. Wire coverage is at parity with Bronto today; it is simply undocumented and loopback-scoped._process_otlp_tracesalready recognisesgen_ai.operation.nameand slugs a foreignservice.nameinto its own runtime rather than mis-bucketing it asopenclaw.clawmetry/apikeys.py:cmk_<id>_<secret>, sha256 at rest in~/.clawmetry/api_keys.json(0600),create/verify/revoke/list_keys/scope_catalogue, and theclawmetry keyCLI.This issue is the write half of #5676, not a parallel key system.
Scope
write:ingesttoapikeys.SCOPES. Careful:scope_catalogue()derivesmethodsfromquery_contract.live_methods_by_scope, which is read-only by construction. A write scope maps to noq/1shape, so the catalogue needs a branch that says so rather than rendering an empty method list that reads like a bug.x-clawmetry-keyon/v1/logs,/v1/metrics,/v1/tracesand/api/v1/runs*. Loopback and the gateway token keep working unchanged — this adds a door, it does not move the existing one.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.x-clawmetry-runtime— validated againstentitlements.ALL_RUNTIMES, with an explicit free-form fallback for custom engines (the write API already accepts arbitraryruntimestrings).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.200accepted,400malformed,401bad/absent key,403key lackswrite:ingest,413over size,429over rate. Caps to publish: 10 MB body, 1000 events per batch (already the custom-runtime cap).clawmetry key create --scope write:ingestprints 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_authreturns early for/api/q/precisely so the stricter gate inroutes/public_api.pyis 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.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.cloud_route_policyentry is aSystemExitinapply_policyand cloud will not boot. Land the cloud-side policy classification in the same window (see the paired cloud issue).Acceptance
read:metricskey gets403on ingest; awrite:ingestkey gets403on/api/q/1/*. Both pinned by test.x-clawmetry-envis 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).