Skip to content

feat(ingest): open the front door — ingest key, generated contract, setup prompt, live status - #5684

Open
vivekchand wants to merge 38 commits into
feat/build-your-own-uifrom
feat/ingest-key
Open

feat(ingest): open the front door — ingest key, generated contract, setup prompt, live status#5684
vivekchand wants to merge 38 commits into
feat/build-your-own-uifrom
feat/ingest-key

Conversation

@vivekchand

@vivekchand vivekchand commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Closes #5679, #5682, #5681, #5680.

Requirement: https://factory.8090.ai/project/b415065f-ab2f-4f53-8864-0c009fd098cb/requirements/7fb88c5c-228e-4c48-9f16-da570740fc36

Stacked on #5676 (feat/build-your-own-ui) — this is the write half of that key system, so the base is that branch, not main. Merge #5676 first.

The gap

An agent is observable by ClawMetry only if the daemon runs on the same machine as the agent. /v1/* trusts loopback and otherwise wants the gateway token; the write API trusts loopback or one static secret shared by the whole install; ingest.clawmetry.com takes E2E-sealed daemon snapshots and nothing else. That excludes CI runners, containers, serverless, hosted agent products and teammates' laptops — where a lot of agent spend actually happens.

Three commits, in dependency order.

1. The ingest key (#5679)

clawmetry key create --name ci --scope write:ingest

then x-clawmetry-key: cmk_… on the OTLP endpoints. Same cmk_ shape, same store, same clawmetry key list|revoke as the read keys — not a second key system. Two routing headers come with it, written into the resource attributes the mappers already read (service.name, deployment.environment), so a header is exactly as powerful as the equivalent exporter setting and no mapper learns a second way to answer the question.

The posture: an ingest key can only push (write:ingest grants no q/1 shape); it is never given a CORS header and cannot carry a browser origin; read and write cannot be mixed on one key, refused at creation with a sentence rather than at request time with a code; and there is one gate, not two — _check_auth steps aside for a keyed /v1/ request exactly as it does for /api/q/.

Verified against a real dashboard: all three doors return 200 (loopback zero-config unchanged, gateway token unchanged, ingest key), every refusal returns its own status with its own sentence, and a span pushed with x-clawmetry-runtime: my-engine lands as agent_type=my_engine while the same span pushed bare still lands as openclaw. That live run found the one real bug here: GET /api/q/1 was answering 200 to a write-only key — verify() accepts it since it is a valid key, and nothing asked whether it held any read scope. Now 403, pinned by a test.

2. The ingest contract, generated (#5682)

Four things describe this surface — the server, docs/INGEST.md, the setup prompts, and the landing reference — and none shared a source. clawmetry/ingest_contract.py declares it as data; ingest_auth.py imports its constants from there; scripts/gen_ingest_doc.py renders the doc with --check.

The guard earns the file. It checks both directions: every attribute declared read must be named in the mapper, and every attribute declared unread must genuinely be absent. Writing that check is what surfaced #5685 — we were advertising a GenAI convention we did not implement, and cached tokens were priced as free.

The doc also states what ClawMetry does not accept — no syslog, CEF, GELF or raw text — pinned by a test, because a reference that only says what works is not one anyone can plan against.

Two cache attributes are listed as not read here, which is the honest state of this branch: the fix is #5686, open against main while this is stacked elsewhere. The unread-side guard makes whichever merges second update the list — which is why both directions are checked.

3. The setup prompt (#5681)

clawmetry setup-prompt <runtime> and GET /api/setup-prompt, rendered from the same contract. Half of it is negative space, because that is the useful half: the key goes in one header and nowhere else; the placeholder is a placeholder and the real key must be asked for, never invented; both encodings are already accepted and need no "fixing"; a key not in the prompt does not exist; and this is observability — it watches, it does not change what runs. It ends by making the agent verify and report a real event count.

Two guards worth keeping. The first reads backwards: every x-clawmetry-* token in the prompt must be a declared header. Checking only that the right headers appear was too weak — proven by mutation, where swapping the config block's header for x-clawmetry-apikey left every other assertion green because the correct name still appeared in the prose. The second is general: every registered subcommand must be in cli.py's _subcmds allowlist, because a parser with no entry there falls through to the dashboard's argparse and dies with "invalid choice", reading as though the command was never written. setup-prompt did exactly that when first added — the same two-list trap CLAUDE.md documents for runtimes — and asserting only that setup-prompt is present would not have prevented the next one.

The prompt module names no runtime and hardcodes no vendor value, pinned by a test: runtime-specific OTel knowledge stays in profiles, which paid runtimes register from clawmetry-pro.

4. "Did it work?" (#5680)

GET /api/onboarding/ingest-status answers the question that kills setup funnels, from real data, and the first-run gate renders it as a live strip. When nothing has arrived it says so and what to do, including clawmetry setup-prompt for an off-box agent. It never blocks — a confirmation, not a step, in a flow whose selling point is having no steps.

Three judgement calls worth naming:

  • Two clocks, kept apart. The durable count is DuckDB and survives restart; the OTLP receiver's counters are in-process and empty on restart, so they are reported separately as has_data_this_process. Folding them together would tell a working install it was broken after every restart.
  • A runtime we merely know about is not one that is sending. Idle rows are dropped rather than listed — listing them answers "is anything arriving?" with a yes they haven't earned.
  • Read through _dispatch, never raw files, so it answers identically on a laptop and in a container with no ~/.openclaw. Memoised 2s because it's polled: ~200 ms first call, ~0.2 ms after.

Verified in both states against a running dashboard: strip renders inside the card in the real page; empty store returns connected:false with an actionable next_step; the populated store here returns 25,349 events across 9 runtimes. The strip first landed outside .obg-card — on the overlay backdrop rather than in the dialog — so a guard now asserts its position.

Tests

77 guards across test_ingest_key.py (25), test_ingest_contract_drift.py (26), test_setup_prompt.py (17) and test_ingest_status.py (9), all registered in ci.yml since CI runs explicit file lists. Every non-obvious guard was mutation-tested — and two of them were strengthened after the mutation passed, which is the only reason they are worth having.

Not in scope

Regional endpoint split, a base "any format" endpoint, per-key rate limiting (belongs on the hosted side where plan budgets live). The hosted endpoint is clawmetry-cloud#2343, gated on the trust-class work in clawmetry-cloud#2344 — pushed telemetry is plaintext by construction, and that needs naming before it ships rather than after it becomes a P0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YMHxYeQR1QLunY3PzURqRd

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Visual diff

Comparing 72bb275fc284 (head) against the PR base branch.

46 of 70 comparison(s) flagged (>1% pixel diff).

View Before After Diff
desktop overview before after diff · 0.36%
desktop flow before after diff · 0.05%
desktop brain ⚠️ before after diff · 100.00%
desktop usage before after diff · 0.00%
desktop crons before after diff · 0.01%
desktop memory before after diff · 0.03%
desktop security before after diff · 0.00%
desktop subagents before after diff · 0.00%
desktop transcripts before after diff · 0.36%
desktop logs ⚠️ before after diff · 8.41%
desktop skills before after diff · 0.38%
desktop models ⚠️ before after diff · 100.00%
desktop approvals before after diff · 0.25%
desktop alerts before after diff · 0.01%
desktop notifications before after diff · 0.01%
desktop limits before after diff · 0.00%
desktop history before after diff · 0.00%
desktop channels before after diff · 0.02%
desktop harness before after diff · 0.00%
desktop inventory ⚠️ before after diff · 1.63%
desktop nemoclaw ⚠️ before after diff · 100.00%
desktop guard ⚠️ before after diff · 100.00%
desktop signals before after diff · 0.01%
desktop policy before after diff · 0.02%
desktop selfevolve before after diff · 0.00%
desktop swimlane ⚠️ before after diff · 100.00%
desktop tool-catalog ⚠️ before after diff · 100.00%
desktop tracing ⚠️ before after diff · 100.00%
desktop turn-anatomy ⚠️ before after diff · 2.48%
desktop version-impact ⚠️ before after diff · 100.00%
desktop context-economics ⚠️ before after diff · 3.09%
desktop agents ⚠️ before after diff · 1.67%
desktop evals ⚠️ before after diff · 16.80%
desktop bench ⚠️ before after diff · 100.00%
desktop trail before after diff · 0.01%
mobile overview ⚠️ before after diff · 100.00%
mobile flow before after diff · 0.00%
mobile brain before after diff · 0.10%
mobile usage ⚠️ before after diff · 4.21%
mobile crons ⚠️ before after diff · 6.36%
mobile memory before after diff · 0.03%
mobile security ⚠️ before after diff · 2.91%
mobile subagents ⚠️ before after diff · 100.00%
mobile transcripts ⚠️ before after diff · 3.62%
mobile logs ⚠️ before after diff · 5.22%
mobile skills ⚠️ before after diff · 1.69%
mobile models ⚠️ before after diff · 100.00%
mobile approvals ⚠️ before after diff · 100.00%
mobile alerts ⚠️ before after diff · 100.00%
mobile notifications ⚠️ before after diff · 100.00%
mobile limits ⚠️ before after diff · 2.34%
mobile history ⚠️ before after diff · 2.15%
mobile channels ⚠️ before after diff · 2.52%
mobile harness ⚠️ before after diff · 100.00%
mobile inventory before after diff · 0.01%
mobile nemoclaw ⚠️ before after diff · 100.00%
mobile guard ⚠️ before after diff · 100.00%
mobile signals ⚠️ before after diff · 1.30%
mobile policy ⚠️ before after diff · 4.19%
mobile selfevolve ⚠️ before after diff · 3.46%
mobile swimlane ⚠️ before after diff · 1.69%
mobile tool-catalog ⚠️ before after diff · 3.83%
mobile tracing ⚠️ before after diff · 3.05%
mobile turn-anatomy ⚠️ before after diff · 3.14%
mobile version-impact ⚠️ before after diff · 100.00%
mobile context-economics ⚠️ before after diff · 1.24%
mobile agents ⚠️ before after diff · 1.61%
mobile evals ⚠️ before after diff · 18.81%
mobile bench ⚠️ before after diff · 4.48%
mobile trail ⚠️ before after diff · 100.00%

Folder: 72bb275fc284. Full PNGs also attached as a workflow artefact.

Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem.

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

github-actions Bot pushed a commit that referenced this pull request Sep 8, 2026
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

Copy link
Copy Markdown
Owner Author

✨ auto-fixed: merged updated base branch (feat/build-your-own-ui) into branch — base was at old SHA 90f8eb5, now at e270a12


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@vivekchand vivekchand changed the title feat(ingest): a scoped ingest key, so an agent off this machine can be seen feat(ingest): the ingest key, the generated contract, and the setup prompt Sep 8, 2026
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@vivekchand vivekchand changed the title feat(ingest): the ingest key, the generated contract, and the setup prompt feat(ingest): open the front door — ingest key, generated contract, setup prompt, live status Sep 8, 2026
@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

@vivekchand
vivekchand force-pushed the feat/build-your-own-ui branch from e270a12 to 0339012 Compare September 8, 2026 09:57
github-actions Bot pushed a commit that referenced this pull request Sep 8, 2026
@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 1 potential drift finding(s)

1. Requirement: Ingest key: observe an agent that is not on this machine

File: docs/MODULE_MAP.md

The MODULE_MAP.md documents /api/setup-prompt as a route served by routes/meta.py, but this endpoint is not implemented in the codebase. The PR description states both a CLI command and an HTTP endpoint should be provided, but only the CLI command exists.

@vivekchand

Copy link
Copy Markdown
Owner Author

Checked this rather than acting on it — the finding is a false positive, but it was pointing at a real defect.

/api/setup-prompt is implemented and live. routes/meta.py:1355, and against a running app:

HTTP 200 | prompt bytes: 2660

routes/meta.py is 2,027 lines, so the endpoint sits well past the head of the file.

Why the bot couldn't see it, and the actual defect. The module opens with a per-blueprint route index, and that index had drifted:

bp_otel           (3)  — /v1/metrics, /v1/traces, /api/otel-status

bp_otel serves six routes. /v1/logs and /api/setup-prompt had both been added without updating it. Since the index is the first ~20 lines of the module, it is what a reader sees and what a tool sampling the head of a file sees — so it reported the endpoint as missing, correctly reasoning from stale documentation.

Fixed in a798db0: every count corrected, the missing routes named, and a guard added asserting the documented count matches the decorators below it (mutation-proven by understating bp_otel back to three). The index can't silently drift again.

@8090-software-factory

Copy link
Copy Markdown

✅ Drift Bot (ClawMetry): no drift detected

Drift Bot analyzed the changed files against this project's blueprints and requirements and found no drift.

vivekchand and others added 6 commits September 8, 2026 18:21
ClawMetry ships one dashboard, and a user who wanted a different view of
their own data had to fork it. Every number the dashboard draws already
comes from a declared, versioned read contract (q/1); the only thing
allowed to call it was the dashboard itself, because the only gate in
front of it was the browser's same-origin rule.

This adds the door:

  clawmetry key create --name my-ui --scope read:metrics \
      --origin http://localhost:3000
  GET /api/q/1/<shape>        one declared query
  GET /api/q/1                what this key can read
  GET /api/q/1/llms.txt       the API, written for a coding agent

Four pieces, all built on what existed:

* Read scopes live ON the contract. Every q/1 method declares one of
  read:metrics / read:sessions / read:traces / read:content next to its
  trust class, so a new method cannot drift out of the scope model and
  docs/QUERY_CONTRACT.md regenerates with a scope table.
* clawmetry/apikeys.py + `clawmetry key` + a Security-tab panel. SHA-256
  in ~/.clawmetry/api_keys.json (0600), scopes plus an origin allowlist.
* routes/public_api.py dispatches through the SAME local_query._dispatch
  the dashboard uses, so there is no second query surface.
* examples/custom-ui/ (one file, no build step) and
  docs/BUILD_YOUR_OWN_UI.md.

The security decision this turns on: adding CORS to a service on
localhost is how local tools get robbed. Any page in any tab can already
SEND a request to 127.0.0.1:8900; the only reason that is harmless is
that the browser will not let it READ the reply. So loopback earns
nothing here (a key is always required, which inverts the rest of the
dashboard), there is no wildcard origin at all, key management stays on
the ordinary dashboard blueprint behind the same-origin write guard, and
the API is GET only. Nothing here writes: this adds no entry to the
control plane.

read:metrics is exactly the plaintext trust class, pinned by a test.
That equality is what makes "a browser-resident key cannot return a
prompt" a promise rather than a hope.

Verified: the starter served from localhost:3000 rendered $750 of real
30-day cost across 10 runtimes from a ClawMetry on another port, and on
that same page a valid full-scope key bound to a different origin was
refused by the browser. 48 guards in tests/test_public_api_keys.py
(registered in ci.yml), three mutations proven red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5
Drift Bot's one finding was that the auto-created feature blueprint was
still the empty template. Written in full (v2, 11.6 KB): composition,
four component blocks, key + integration contracts, and six ADRs
covering the scope-on-the-contract choice, loopback not being
authentication here, the no-wildcard rule, key management staying off
the keyed surface, why custom UIs read the node rather than the hosted
service, and the self-describing guide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5
Drift Bot round 2 read the rewritten blueprint (it no longer calls it
empty) and asked for the concrete API surface rather than only ADRs.
Added an 'API Surface' section at 4% of the document: the three read
endpoints with auth and the status-code table, which four response
fields a client contracts on and which are diagnostic, the cross-origin
rule, the three management endpoints, and the four CLI subcommands.
Blueprint v3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5
Drift Bot round 3 reported that GET/POST /api/apikeys and DELETE
/api/apikeys/<id> 'are not implemented in routes/infra.py' and that the
Security tab therefore cannot work. They were implemented, at lines
2955-3042 of a 3042-line file, i.e. at 97% depth: the bot reads a
truncated prefix of a long file, so anything appended to a module that
size is invisible to it forever.

Rather than argue the point on a PR comment, extract them. They now live
in routes/apikeys_admin.py (bp_apikeys_admin, ~130 lines), registered in
dashboard.py next to bp_public_api. routes/infra.py returns byte for
byte to its state on main.

This is better placement independent of the tool. Credential management
is a different concern from the security tab's scanners, and the reason
these handlers must not sit on the keyed surface now has a file to be
stated in rather than a comment in the middle of another one. Both
guards are path-based, not blueprint-based, so the security posture is
unchanged: dashboard.py's _cross_origin_write_blocked still refuses a
cross-origin POST/DELETE, and public_api._add_cors is still pinned to
/api/q/.

A new guard asserts the separation directly: no /api/apikeys rule may be
owned by the public_api blueprint and no /api/q/ rule by apikeys_admin.

Verified on a real boot after the move, not only in tests: management
GET 200, POST with no origin 400, cross-origin POST 403, query index 401
without a key and 200 with one, aggregates 200, CORS header present on
/api/q/ and absent on /api/apikeys.

Blueprint v4 names the module. Cloud #2342 updated to match the new
endpoint names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5
I called the earlier CodeQL red a stale aggregate. It was not: 8 new
alerts, 4 of them high, all in code this PR adds. Triaged individually.

This repo runs code scanning through DEFAULT SETUP, so inline
'# codeql[...]' comments are inert here (measured in
clawmetry/nonsecret_hash.py, not assumed). Every alert therefore had to
be fixed in code or accepted deliberately. Six are fixed:

* py/stack-trace-exposure x2. Both were our own authored sentences, but
  text read off an exception is exception-derived to a static analyser
  no matter who wrote it. The /api/q/ 400 is now built from the CONTRACT
  (which already declares the required args) and the /api/apikeys 400
  from a literal table keyed by a refusal code. Both are better answers:
  the query error is now consistent across queries and carries
  missing_args, and the create error carries a stable 'reason' a client
  can branch on instead of matching prose.
* py/log-injection. The failure log now names the contract's own backing
  method rather than the shape string off the URL. Validated-then-logged
  is still a request-text sink.
* py/clear-text-logging x2 of 4. The two curl examples no longer
  interpolate the key; they use , which is what the docs
  tell people to do with it anyway and reads better than a 50-character
  key wrapped across a terminal.
* js/xss-through-exception. The starter's error panel built markup out
  of strings including a fetch exception message. It now builds text
  nodes and <code> elements, so nothing it did not write becomes markup.

The two remaining alerts are the one-time key reveal (the plain print
and the --json field). A key that is never shown cannot be used, and
every comparable CLI does exactly this. They are true observations, not
defects, and with no working in-code suppression they can only be
dismissed by a maintainer in the Security tab.

Verified live after the change: the contract-built 400 names session_id
and returns missing_args, the management 400 carries reason
'unknown_scope' with no exception text, the wildcard refusal still
fires, and all three normal paths stay 200. Both starter error panels
render as real text nodes (3 <code>, 1 <br>) and the happy path still
draws  across 10 runtimes and 10 models.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5
Drift Bot round 5, and it is right: the extraction moved the handlers to
routes/apikeys_admin.py but left the cross-reference in
public_api._add_cors naming their old home. That comment is load-bearing
(it explains WHY the CORS handler is pinned to /api/q/), so pointing it
at a file that no longer contains them is worse than no comment.

Swept the rest of the new code for the same staleness; this was the only
one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5
github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026
Drift from Syntax & Lint CI check on feat/ingest-key. Regenerated with
python3 scripts/gen_module_map.py to unblock PR #5684.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABjpgFAH2NmkvKo9MMXXiz
@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 1 potential drift finding(s)

1. Blueprint: Ingest key: observe an agent that is not on this machine

File: routes/meta.py:1310

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters.

Comment thread routes/meta.py
payload, status = err
return jsonify(payload), status

try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Ingest key: observe an agent that is not on this machine

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters.

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026
@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 1 potential drift finding(s)

1. Blueprint: Ingest key: observe an agent that is not on this machine

File: routes/meta.py:1312

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters.

Comment thread routes/meta.py
try:
process(
request.get_data(),
body,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Ingest key: observe an agent that is not on this machine

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters.

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Auto-rebase attempted and aborted — not safe to resolve automatically.

Status: dirty (merge conflict against base branch feat/build-your-own-ui)

Why I stopped: Commit 1 of 19 had 9 files with content conflicts: CHANGELOG.md, CLAUDE.md, clawmetry/apikeys.py, clawmetry/cli.py, dashboard.py, docs/MODULE_MAP.md, examples/custom-ui/index.html, routes/public_api.py, tests/test_public_api_keys.py. This exceeds the 5-file threshold for automatic resolution and involves core logic files where picking a side would lose real changes.

Context: Note that the base branch (feat/build-your-own-ui / #5676) just received a fix push (regenerated MODULE_MAP.md to unblock its Syntax & Lint gate). Once #5676 merges to main, this PR's base should be updated accordingly. A rebase of this PR against the freshened base may have fewer conflicts then.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

awaiting confirmation — non-trivial rebase, needs human review (conflicts in: CHANGELOG.md, CLAUDE.md, clawmetry/apikeys.py, clawmetry/cli.py, dashboard.py, docs/MODULE_MAP.md, examples/custom-ui/index.html, routes/public_api.py, tests/test_public_api_keys.py)

This PR is stacked on feat/build-your-own-ui (#5676), which has diverged since this PR was opened. Both branches independently added the same files (clawmetry/apikeys.py, routes/public_api.py, tests/test_public_api_keys.py, examples/custom-ui/index.html) — all add/add conflicts with semantic code. Automated rebase was aborted; manual resolution is needed.

Suggested approach: rebase feat/build-your-own-ui onto main first (if needed), then interactive-rebase this branch onto the updated feat/build-your-own-ui, taking the ingest-key versions of any files both branches touched.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Test plan & review notes

Repo: vivekchand/clawmetry

What changed

⚠️ Stacking conflict — needs your attention
This PR is based on feat/build-your-own-ui (#5676), and the automated rebase attempts have all stalled on semantic conflicts in clawmetry/apikeys.py and routes/public_api.py (both branches created divergent versions of those files). The safe path: once #5676 lands on main, rebase feat/ingest-key onto origin/main rather than onto the base branch, resolving the apikeys/public_api merge by hand. The automated sweep last noted this on 2026-09-09; no human has picked it up since.

⚠️ CI state
E2E Gate is failing on the current head and most test jobs are showing skipped — likely a side effect of the rebase conflict leaving the branch in a state where CI can't run normally rather than a code defect. Expect CI to normalize once #5676 merges and this branch is cleanly rebased.

Smoke commands (after rebase onto main)

  • make test — full suite including the new ingest-key tests
  • python3 dashboard.py --port 8900 then:
    • curl -sS http://localhost:8900/api/setup-prompt — should return 200 with the setup prompt string
    • curl -sS http://localhost:8900/api/ingest-status — should return live ingest-key status
    • curl -sS -X POST http://localhost:8900/api/v1/runs/start -H "X-Ingest-Key: <key>" -H "Content-Type: application/json" -d '{"runtime":"custom","session_id":"test"}' — should accept or reject cleanly
  • clawmetry setup-prompt in a terminal — should emit a ready-to-copy block for the agent's system prompt

What to look at visually

  • Dashboard overview / status panel — ingest-key status indicator
  • clawmetry apikeys CLI — key shows up in list, can be revoked

Issue link
This PR builds on #5676 and should be merged after it. No standalone issue link found — if there is one it would be worth adding Closes #X to the description.


Generated by Claude Code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3rgHBErNdBNUWo89ddN8S

Copy link
Copy Markdown
Owner Author

Merged updated feat/build-your-own-ui base into this branch; one mechanical MODULE_MAP.md conflict resolved by regenerating the file. State changed dirty → unstable (conflict cleared, CI re-triggered). If still not green in 10min, may need manual attention.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 1 potential drift finding(s)

1. Blueprint: Ingest key: observe an agent that is not on this machine

File: routes/meta.py:1313-1319

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters, causing TypeError at runtime when ingest keys are used.

Comment thread routes/meta.py
Comment on lines 1313 to 1319
content_encoding=request.headers.get("Content-Encoding"),
content_type=request.headers.get("Content-Type"),
runtime=ctx["runtime"],
env=ctx["env"],
)
return "{}", 200, {"Content-Type": "application/json"}
except OtlpProtobufUnavailable as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Ingest key: observe an agent that is not on this machine

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters, causing TypeError at runtime when ingest keys are used.

Copy link
Copy Markdown
Owner Author

PR sweep standing down: this PR (feat/ingest-key) is stacked on feat/build-your-own-ui as its base branch, and the base has advanced since this PR was opened — update_pull_request_branch returns a merge conflict between the two branches. Cannot auto-resolve without understanding both sides. Recommend rebasing feat/ingest-key onto the latest feat/build-your-own-ui head, or onto main once #5676 merges.


Generated by Claude Code

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Maintenance bot: merged updated feat/build-your-own-ui into this branch — #5676 (the base) was just rebased onto current main, so this branch was behind its base. CI will re-run.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 1 potential drift finding(s)

1. Blueprint: Ingest key: observe an agent that is not on this machine

File: routes/meta.py:1313-1319

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters, causing TypeError at runtime when ingest keys are used.

Comment thread routes/meta.py
Comment on lines 1313 to 1319
content_encoding=request.headers.get("Content-Encoding"),
content_type=request.headers.get("Content-Type"),
runtime=ctx["runtime"],
env=ctx["env"],
)
return "{}", 200, {"Content-Type": "application/json"}
except OtlpProtobufUnavailable as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Ingest key: observe an agent that is not on this machine

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters, causing TypeError at runtime when ingest keys are used.

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026

Copy link
Copy Markdown
Owner Author

✨ auto-fixed: merged feat/build-your-own-ui (base branch updated with main) into branch — now up to date with its base.


Generated by Claude Code

@8090-software-factory

Copy link
Copy Markdown

⚠️ Drift Bot (ClawMetry): 1 potential drift finding(s)

1. Blueprint: Ingest key: observe an agent that is not on this machine

File: routes/meta.py:1314-1319

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters, causing TypeError at runtime when ingest keys are used.

Comment thread routes/meta.py
Comment on lines 1314 to 1319
content_type=request.headers.get("Content-Type"),
runtime=ctx["runtime"],
env=ctx["env"],
)
return "{}", 200, {"Content-Type": "application/json"}
except OtlpProtobufUnavailable as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Drift Bot (ClawMetry) — Blueprint: Ingest key: observe an agent that is not on this machine

The _otlp_receive function passes runtime and env as keyword arguments to processor functions, but they only accept pb_data, content_encoding, and content_type. The blueprint specifies routing headers should be applied to resource attributes via _apply_ingest_routing before processing, not threaded as parameters, causing TypeError at runtime when ingest keys are used.

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants