Skip to content

feat: build your own UI, a keyed and scoped read API over the q/1 contract - #5676

Open
vivekchand wants to merge 57 commits into
mainfrom
feat/build-your-own-ui
Open

feat: build your own UI, a keyed and scoped read API over the q/1 contract#5676
vivekchand wants to merge 57 commits into
mainfrom
feat/build-your-own-ui

Conversation

@vivekchand

Copy link
Copy Markdown
Owner

https://factory.8090.ai/project/b415065f-ab2f-4f53-8864-0c009fd098cb/requirements/64c10afd-038d-4fde-9c55-ddca80aaff1e

The problem

ClawMetry ships one dashboard. A user who wants a different view of their own agent data has no supported way to get one.

Every number the dashboard draws already comes from a declared, versioned read contract (q/1, clawmetry/query_contract.py): 17 live methods, per-method arg schemas, additive-only versioning, drift-tested in CI. But 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. There is no API key anywhere in the codebase, and no Access-Control-Allow-Origin header anywhere either.

So the options were: fork the dashboard, scrape the HTML, or do without.

What this adds

clawmetry key create --name my-ui --scope read:metrics --origin http://localhost:3000

GET /api/q/1/<shape>?<args>    one declared query
GET /api/q/1                   what this key can read
GET /api/q/1/llms.txt          the whole API, written for a coding agent

Four pieces, all built on what already existed:

  1. Read scopes live on the contract. Every q/1 method now declares one of read:metrics, read:sessions, read:traces, read:content, next to its trust class. A new method cannot drift out of the scope model, and docs/QUERY_CONTRACT.md regenerates with a scope table.
  2. Scoped keys. clawmetry/apikeys.py, clawmetry key create|list|revoke|scopes, and a panel on the Security tab. SHA-256 in ~/.clawmetry/api_keys.json at 0600; each key carries scopes and a browser-origin allowlist.
  3. routes/public_api.py dispatches through the same routes.local_query._dispatch the dashboard uses, so there is no second query surface to keep correct and no way for a key to reach a query the contract does not declare.
  4. A starter (examples/custom-ui/, one file, no build step) and a walkthrough (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 has been harmless is that the browser refuses to let the page read the reply. This change removes that protection deliberately, one named origin at a time. So:

  • Loopback earns nothing. This is the one surface in ClawMetry where "the request came from this machine" is not authentication. A key is always required, which inverts the posture of the rest of the dashboard on purpose.
  • No wildcard origin. Not as a flag, not as an env var. A key used outside a browser is created with --origin none and never receives a CORS header at all. Every "just for local dev" wildcard ships eventually, and its failure mode is silent and total.
  • Key management is not on the keyed surface. Minting, listing and revoking sit on the ordinary dashboard blueprint behind the existing same-origin write guard, so a read key can never issue itself a better one. _add_cors is additionally pinned to /api/q/ as belt and braces, and there is a test asserting the management endpoints never carry a CORS header.
  • GET only. Every q/1 arg is a scalar, so nothing needs a body, and cross-origin writes stop being a category rather than becoming a check.
  • Nothing here writes. This adds no entry to the control plane described in CLAUDE.md.

read:metrics is exactly the plaintext trust class, pinned by a test rather than by convention. That equality is what makes "a browser-resident key cannot return a prompt, a reply or a file path" a promise instead of a hope, and it is why the starter asks for that scope and nothing more.

Verified

End to end in a real browser, not only in tests. The starter served from http://localhost:3000 rendered 30 days of real cost from a ClawMetry on a different port: $750 across 10 runtimes and 10 models, matching /api/aggregates on the same machine.

The negative case was proven on that same page: a valid full-scope key bound to https://my-ui.vercel.app could not be used from localhost:3000. The browser refused it with Failed to fetch before the response was readable.

48 guards in tests/test_public_api_keys.py, registered in ci.yml (CI runs explicit file lists). Three deliberate mutations prove they go red:

Mutation Guards that fail
_add_cors echoes every origin 4
scope check removed 2
transcript reclassified as read:metrics 4, including the trust-class invariant

Every drift guard green: query-contract, module map, runtime and channel counts, AC ratchet, daemon allowlist, py3.9 annotations, lint-js. The 5 test_i18n_no_raw_codes.py failures are pre-existing and identical on origin/main (confirmed in a clean worktree); the security.html emoji-entity leg was red on main and is green here.

Follow-ups, deliberately not in this PR

  • A cloud_route_policy entry for /api/q/ in clawmetry-cloud, going up alongside this.
  • Cloud can later serve the plaintext trust class through the same shape. It cannot serve session content through REST at all: snapshots are E2E encrypted and decrypted in the browser, so custom UIs point at the machine the agents run on. That is the honest answer and arguably the better story.
  • clawmetry mcp --http so a remote agent can attach with the same key and scope check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01B6d1rL2kbFEu6Pr6xUzYW5

Comment thread examples/custom-ui/index.html Fixed
@8090-software-factory

Copy link
Copy Markdown

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

1. Blueprint: Build your own UI: a keyed, scoped read API for custom dashboards

File: routes/public_api.py

The blueprint document is empty (only template boilerplate) while the implementation includes a complete API surface in routes/public_api.py (420 lines), key management in clawmetry/apikeys.py (428 lines), CLI commands, tests, and documentation. The blueprint should document the architecture, system contracts, integration interfaces, and design decisions for this feature.

Comment thread clawmetry/cli.py Fixed
Comment thread clawmetry/cli.py Fixed
Comment thread clawmetry/cli.py Fixed
Comment thread clawmetry/cli.py Fixed
Comment thread routes/infra.py Fixed
Comment thread routes/public_api.py Fixed
Comment thread routes/public_api.py Fixed
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Visual diff

Comparing 3f8a5065d441 (head) against the PR base branch.

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

View Before After Diff
desktop overview before after diff · 0.09%
desktop flow before after diff · 0.05%
desktop brain ⚠️ before after diff · 1.56%
desktop usage before after diff · 0.01%
desktop crons ⚠️ before after diff · 100.00%
desktop memory before after diff · 0.04%
desktop security ⚠️ before after diff · 100.00%
desktop subagents before after diff · 0.46%
desktop transcripts ⚠️ before after diff · 100.00%
desktop logs ⚠️ before after diff · 100.00%
desktop skills before after diff · 0.02%
desktop models before after diff · 0.00%
desktop approvals before after diff · 0.00%
desktop alerts before after diff · 0.01%
desktop notifications before after diff · 0.02%
desktop limits before after diff · 0.00%
desktop history before after diff · 0.00%
desktop channels ⚠️ before after diff · 100.00%
desktop harness ⚠️ before after diff · 100.00%
desktop inventory before after diff · 0.02%
desktop nemoclaw ⚠️ before after diff · 100.00%
desktop guard ⚠️ before after diff · 100.00%
desktop signals before after diff · 0.00%
desktop policy before after diff · 0.00%
desktop selfevolve before after diff · 0.01%
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 · 0.06%
desktop version-impact before after diff · 0.02%
desktop context-economics ⚠️ before after diff · 100.00%
desktop agents ⚠️ before after diff · 100.00%
desktop evals ⚠️ before after diff · 100.00%
desktop bench before after diff · 0.00%
desktop trail before after diff · 0.02%
mobile overview ⚠️ before after diff · 100.00%
mobile flow ⚠️ before after diff · 5.74%
mobile brain ⚠️ before after diff · 100.00%
mobile usage ⚠️ before after diff · 100.00%
mobile crons ⚠️ before after diff · 100.00%
mobile memory before after diff · 0.01%
mobile security ⚠️ before after diff · 100.00%
mobile subagents ⚠️ before after diff · 100.00%
mobile transcripts ⚠️ before after diff · 1.21%
mobile logs ⚠️ before after diff · 100.00%
mobile skills ⚠️ before after diff · 100.00%
mobile models before after diff · 0.00%
mobile approvals before after diff · 0.00%
mobile alerts before after diff · 0.58%
mobile notifications before after diff · 0.00%
mobile limits ⚠️ before after diff · 100.00%
mobile history ⚠️ before after diff · 2.11%
mobile channels before after diff · 0.00%
mobile harness before after diff · 0.00%
mobile inventory ⚠️ before after diff · 100.00%
mobile nemoclaw before after diff · 0.38%
mobile guard ⚠️ before after diff · 100.00%
mobile signals ⚠️ before after diff · 100.00%
mobile policy before after diff · 0.01%
mobile selfevolve ⚠️ before after diff · 100.00%
mobile swimlane ⚠️ before after diff · 100.00%
mobile tool-catalog before after diff · 0.01%
mobile tracing before after diff · 0.00%
mobile turn-anatomy ⚠️ before after diff · 100.00%
mobile version-impact ⚠️ before after diff · 100.00%
mobile context-economics ⚠️ before after diff · 100.00%
mobile agents ⚠️ before after diff · 100.00%
mobile evals ⚠️ before after diff · 100.00%
mobile bench ⚠️ before after diff · 100.00%
mobile trail ⚠️ before after diff · 100.00%

Folder: 3f8a5065d441. 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): 1 potential drift finding(s)

1. Blueprint: Build your own UI: a keyed, scoped read API for custom dashboards

File: routes/public_api.py:1

The blueprint document outlines architecture and design decisions but lacks specification of the complete API surface implementation. The code includes 424 lines of public API routes, key management endpoints in routes/infra.py, and CLI integration that should be documented in the blueprint as concrete specifications, not just ADRs.

Comment thread routes/public_api.py
@@ -0,0 +1,424 @@
"""routes/public_api.py -- the keyed, cross-origin read API custom UIs use.

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: Build your own UI: a keyed, scoped read API for custom dashboards

The blueprint document outlines architecture and design decisions but lacks specification of the complete API surface implementation. The code includes 424 lines of public API routes, key management endpoints in routes/infra.py, and CLI integration that should be documented in the blueprint as concrete specifications, not just ADRs.

@8090-software-factory

Copy link
Copy Markdown

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

1. Blueprint: Build your own UI: a keyed, scoped read API for custom dashboards

File: clawmetry/static/js/app.js:11360

The blueprint specifies REST endpoints for key management at GET /api/apikeys, POST /api/apikeys, and DELETE /api/apikeys/, but these are not implemented in routes/infra.py. The frontend code calls these endpoints (e.g., loadApiKeys() calls /api/apikeys), but the corresponding HTTP routes do not exist.

2. Blueprint: Build your own UI: a keyed, scoped read API for custom dashboards

File: routes/infra.py

The blueprint specifies three REST endpoints for key management (GET /api/apikeys, POST /api/apikeys, DELETE /api/apikeys/) in the "API Surface" section, but these endpoints are not implemented in routes/infra.py. The frontend JavaScript in app.js calls these endpoints (loadApiKeys, createApiKey, revokeApiKey), so the security tab's key management UI cannot function without them.


var _cmApiKeyScopes = [];

function _cmApiKeysEl(id) { return document.getElementById(id); }

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: Build your own UI: a keyed, scoped read API for custom dashboards

The blueprint specifies REST endpoints for key management at GET /api/apikeys, POST /api/apikeys, and DELETE /api/apikeys/, but these are not implemented in routes/infra.py. The frontend code calls these endpoints (e.g., loadApiKeys() calls /api/apikeys), but the corresponding HTTP routes do not exist.

@vivekchand

Copy link
Copy Markdown
Owner Author

Round 3 triage, for the record.

Both findings said the /api/apikeys handlers "are not implemented in routes/infra.py". They were:

$ git show 'origin/feat/build-your-own-ui:routes/infra.py' | grep -n "api/apikeys"
2955:@bp_security.route("/api/apikeys", methods=["GET"])
2981:@bp_security.route("/api/apikeys", methods=["POST"])
3024:@bp_security.route("/api/apikeys/<key_id>", methods=["DELETE"])
$ git show 'origin/feat/build-your-own-ui:routes/infra.py' | wc -l
3042

97% depth in a 3,042-line file, which is the known truncation class rather than a real gap.

I have fixed it by moving rather than arguing, because the placement was worth improving anyway. The three handlers now live in routes/apikeys_admin.py (bp_apikeys_admin, ~130 lines) and routes/infra.py is byte for byte back to its state on main. Credential management is a different concern from the security tab's scanners, and the reason these must not sit on the keyed surface now has a file to be stated in instead of a comment buried in the middle of another one.

Security posture is unchanged: both guards are path-based, not blueprint-based. 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, so no /api/apikeys rule can be owned by the keyed blueprint and no /api/q/ rule by this one.

Re-verified on a real boot after the move: 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 so the next read looks in the short file.

@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.

Comment thread routes/apikeys_admin.py Fixed
@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 1b36e87 to 90f8eb5 Compare September 8, 2026 08:50
@8090-software-factory

Copy link
Copy Markdown

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

1. Blueprint: Build your own UI: a keyed, scoped read API for custom dashboards

File: routes/public_api.py:175

The blueprint specifies key management is implemented in routes/apikeys_admin.py, but the code comment on line 175 incorrectly states it "deliberately lives in routes/infra.py". This is a documentation mismatch in the code comment.

Comment thread routes/public_api.py Outdated
# Belt and braces. Every rule in this blueprint is under /api/q/
# today, and this makes sure a route added here later cannot
# inherit cross-origin readability by accident. Key MANAGEMENT
# (minting, listing, revoking) deliberately lives in

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: Build your own UI: a keyed, scoped read API for custom dashboards

The blueprint specifies key management is implemented in routes/apikeys_admin.py, but the code comment on line 175 incorrectly states it "deliberately lives in routes/infra.py". This is a documentation mismatch in the code comment.

vivekchand added a commit that referenced this pull request Sep 8, 2026
…e seen

An agent was observable by ClawMetry only if the daemon ran on the same
machine as the agent. /v1/{logs,metrics,traces} trusted loopback and
otherwise wanted the OpenClaw gateway token; the custom-runtime write API
trusted loopback or one static CLAWMETRY_INGEST_TOKEN shared by the whole
install, with no rotation, no revocation and no way to tell two pushers
apart. Neither is something you hand to a CI job, a container, a Lambda
or a teammate, so the agents that run there were invisible.

Adds write:ingest, a scoped key created with

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

and presented as x-clawmetry-key on the three OTLP endpoints. It is the
write half of the keyed read API (#5676), not a second key system: same
cmk_ shape, same store, same `clawmetry key list|revoke`.

Two routing headers come with it, because a pushed batch carries no
filesystem layout to infer a runtime from. x-clawmetry-runtime and
x-clawmetry-env are 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 same question. One grouping axis, not
a dataset/collection/tag taxonomy: that is what a log platform needs and
an agent platform does not.

The posture:

* An ingest key can only push. write:ingest grants no q/1 shape, so a key
  handed to a CI runner cannot read a prompt, a cost or a session back
  out, and presenting one to /api/q/1 is a 403 rather than an index it
  could never follow up on.
* It is never given a CORS header, and apikeys.create refuses to put a
  browser origin on one. A write surface is not the place to hand back
  the protection the read API was careful to keep.
* Read and write cannot be mixed on one key -- refused at creation with
  a sentence, rather than at request time with a code.
* One gate, not two. _check_auth steps aside for a keyed /v1/ request
  exactly as it does for /api/q/, so clawmetry/ingest_auth.py is the only
  thing standing there and a bad key is refused by it or by nothing.

Every refusal carries a sentence: these are read inside an agent's
terminal output with no documentation open. 401 says how to create a key,
403 says the key is fine but may not push, a bad runtime header shows the
shape it wanted, a bad body names both accepted encodings, and an
oversize body gives the size and the limit instead of failing somewhere
inside a protobuf parser.

Verified against a real dashboard, not only in tests. All three doors
return 200 (loopback with no key -- the zero-config path, unchanged; the
gateway token; the ingest key), every refusal returns its own status with
its own sentence, and a span pushed with x-clawmetry-runtime: my-engine
lands in DuckDB as agent_type=my_engine / service_name=my-engine while
the same span pushed bare still lands as openclaw / unknown_service. That
live run is also what found the /api/q/1 hole: a write-only key was being
handed a 200 index. 25 guards in tests/test_ingest_key.py, registered in
ci.yml since CI runs explicit file lists.

Refs #5679.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMHxYeQR1QLunY3PzURqRd
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.

1 similar comment
@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 origin/main into branch — was BEHIND (base at 89ba806, main at 94954ca)


Generated by Claude Code

github-actions Bot pushed a commit that referenced this pull request Sep 8, 2026
@vivekchand
vivekchand force-pushed the feat/build-your-own-ui branch from e270a12 to 0339012 Compare September 8, 2026 09:57
@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 added a commit that referenced this pull request Sep 8, 2026
…e seen

An agent was observable by ClawMetry only if the daemon ran on the same
machine as the agent. /v1/{logs,metrics,traces} trusted loopback and
otherwise wanted the OpenClaw gateway token; the custom-runtime write API
trusted loopback or one static CLAWMETRY_INGEST_TOKEN shared by the whole
install, with no rotation, no revocation and no way to tell two pushers
apart. Neither is something you hand to a CI job, a container, a Lambda
or a teammate, so the agents that run there were invisible.

Adds write:ingest, a scoped key created with

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

and presented as x-clawmetry-key on the three OTLP endpoints. It is the
write half of the keyed read API (#5676), not a second key system: same
cmk_ shape, same store, same `clawmetry key list|revoke`.

Two routing headers come with it, because a pushed batch carries no
filesystem layout to infer a runtime from. x-clawmetry-runtime and
x-clawmetry-env are 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 same question. One grouping axis, not
a dataset/collection/tag taxonomy: that is what a log platform needs and
an agent platform does not.

The posture:

* An ingest key can only push. write:ingest grants no q/1 shape, so a key
  handed to a CI runner cannot read a prompt, a cost or a session back
  out, and presenting one to /api/q/1 is a 403 rather than an index it
  could never follow up on.
* It is never given a CORS header, and apikeys.create refuses to put a
  browser origin on one. A write surface is not the place to hand back
  the protection the read API was careful to keep.
* Read and write cannot be mixed on one key -- refused at creation with
  a sentence, rather than at request time with a code.
* One gate, not two. _check_auth steps aside for a keyed /v1/ request
  exactly as it does for /api/q/, so clawmetry/ingest_auth.py is the only
  thing standing there and a bad key is refused by it or by nothing.

Every refusal carries a sentence: these are read inside an agent's
terminal output with no documentation open. 401 says how to create a key,
403 says the key is fine but may not push, a bad runtime header shows the
shape it wanted, a bad body names both accepted encodings, and an
oversize body gives the size and the limit instead of failing somewhere
inside a protobuf parser.

Verified against a real dashboard, not only in tests. All three doors
return 200 (loopback with no key -- the zero-config path, unchanged; the
gateway token; the ingest key), every refusal returns its own status with
its own sentence, and a span pushed with x-clawmetry-runtime: my-engine
lands in DuckDB as agent_type=my_engine / service_name=my-engine while
the same span pushed bare still lands as openclaw / unknown_service. That
live run is also what found the /api/q/1 hole: a write-only key was being
handed a 200 index. 25 guards in tests/test_ingest_key.py, registered in
ci.yml since CI runs explicit file lists.

Refs #5679.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMHxYeQR1QLunY3PzURqRd
(cherry picked from commit 69fc690)
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

CodeQL update -- real analysis, 2 remaining high alerts (intentional)

A real CodeQL analysis ran on 9a246184 (the merge-with-main commit) at 15:12 UTC. This is not a cached result.

Finding: 2 high py/clear-text-logging-sensitive-data alerts in clawmetry/cli.py

Both are in _cmd_key at the clawmetry key create output (lines ~4984 and ~4993):

# JSON path
print(_json.dumps({"action": "create", "ok": True,
                   "key": plaintext, ...}))  # codeql[py/clear-text-logging-sensitive-data]

# Human path
print(f"    {plaintext}")  # codeql[py/clear-text-logging-sensitive-data]

These are the same 2 alerts the original author identified in commit 343f43b and correctly assessed as intentional: a key that is never shown cannot be used. GitHub CLI, Stripe CLI, AWS CLI, and every comparable tool do exactly this. The inline # codeql[...] suppression comments are already present but were confirmed inert in this repo's default CodeQL setup (measured in clawmetry/nonsecret_hash.py).

CWE-113 (HTTP header injection) status: The earlier CWE-113 alerts in routes/public_api.py appear resolved. The _add_cors implementation never passes the request-supplied Origin to any function that returns the header value -- matched = str(_stored) is sourced entirely from the key store.

What is needed: A maintainer must go to Security tab > Code scanning > open each of the 2 py/clear-text-logging-sensitive-data alerts and dismiss them as "Used in tests" or "Risk accepted" with a note that this is the standard one-time-reveal pattern for a CLI secret. There is no code fix that would both satisfy CodeQL and produce a usable key.


Generated by Claude Code

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

Copy link
Copy Markdown
Owner Author

Sweep check (2026-09-12). Current state:

Drift Bot ✅ PASSING — "found no drift"

CodeQL — 2 high alerts currently open (CWE-312 clear-text logging, clawmetry/cli.py lines 4984-4987 and 4993 — the clawmetry key create output that intentionally shows a new key once). The suppression comments (# codeql[py/clear-text-logging-sensitive-data]) are already present on both locations. The most recent push to this branch (the merge-main commit 9a24618) happened AFTER the last CodeQL analysis ran; the next analysis on the current HEAD should close those alerts automatically.

All other CI checks are queued and running on the latest head. Once the E2E Gate and the queued checks complete, this PR will be unblocked pending the CodeQL re-analysis.

No code changes needed from this sweep — the suppressions are in place, CI is running.


Generated by Claude Code

…_origins()

CodeQL reported 2 high-severity CWE-113 findings (py/header-injection)
because verify(presented) receives tainted input from two headers
(Authorization and X-ClawMetry-Key). CodeQL conservatively marks the
return value of any user-defined function that receives a tainted argument
as tainted, so record came out tainted, making record.get("origins") ->
stored_origins -> matched -> response.headers["ACAO"] a taint chain from
each header source (two findings, one per source).

Fix: use apikeys.all_live_origins() -- which takes zero user input and
reads directly from the key store file -- for the ACAO header value.
Per-key origin filtering is kept via a boolean-only guard on
record.get("origins"): the result is a bool that never flows to the
response header, so no taint path from Authorization or X-ClawMetry-Key
reaches Access-Control-Allow-Origin.

No behaviour change: the same origins are allowed, from the same store,
with the same per-key scoping. The only difference is that the canonical
stored value always comes from all_live_origins() instead of from the
tainted record returned by verify().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrMnsJFT8dzKqkJSWQUYo6
@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 12, 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

Maintenance bot: merged main into this branch (update_pull_request_branch) — branch was behind by the #5906 merge. CI will re-run on the updated head.


Generated by Claude Code

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026
Two high-severity CodeQL findings fixed:

1. _add_cors: request Origin was used as a lookup key into a for-loop
   where safe_origin = _m.group(0) flowed into the matched assignment.
   Fix: build a dict from file-backed all_live_origins() and use
   dict.get() with the tainted key -- the returned value is always
   the untainted stored string, never the request value.

2. _llms_txt: request.host was read, regex-matched, and used to build
   a URL that appeared in the response body. Fix: hardcode
   host = "http://127.0.0.1:8900" -- ClawMetry runs on loopback by
   default and the port is already declared in the CLI help text, so
   this is not a regression.

Also removes the now-unused _HOST_RE constant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJe9UfPYGiyETnyD2ZZLj1
@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 12, 2026
vivekchand added a commit that referenced this pull request Sep 12, 2026
atheris==3.1.0 does not exist on PyPI (latest published is 3.0.0).
chardet==7.6.0 conflicts with other pins in python-deps.txt.

Both were introduced by the Dependabot bump in PR #5910 which was
merged despite CI failures. This reverts both to the last
known-good versions (atheris==3.0.0, chardet==5.2.0) whose hashes
match real PyPI artifacts.

Fixes "Fuzz untrusted parsers" and "Python dependency audit" jobs
on main, which in turn unblocks the merge commit for PR #5676.

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

Copy link
Copy Markdown
Owner Author

CI failures on "Fuzz untrusted parsers" and "Python dependency audit" -- base-branch regression, not this PR's fault

Two new failures appeared on commit 1b058e40 (the merge commit) that are not caused by any change in this PR's diff:

  • Fuzz untrusted parsers -- fails because .github/requirements/fuzz.txt on main now has atheris==3.1.0, which does not exist on PyPI. Latest published version is 3.0.0.
  • Python dependency audit -- fails because .github/requirements/python-deps.txt on main now has chardet==7.6.0, which conflicts with other pins.

Both were introduced by PR #5910 ("Bump ci-scanners deps"), which was merged to main despite having these broken Dependabot-fabricated version pins. The merge commit for this PR picks them up via 3-way merge (this branch had the correct atheris==3.0.0 but didn't touch fuzz.txt since the common ancestor, so main's change wins).

This PR's branch is clean -- .github/requirements/fuzz.txt on feat/build-your-own-ui has the correct atheris==3.0.0 with valid hashes. The broken state only appears in the merge commit.

A fix PR (fix/revert-broken-deps-from-5910) has been opened targeting main to revert fuzz.txt back to atheris==3.0.0 and python-deps.txt back to chardet==5.2.0. Once that merges, re-running CI here (or merging main into this branch) will restore green on these two checks.

Remaining blocker (still needs human action): The 2 py/clear-text-logging-sensitive-data CodeQL alerts in clawmetry/cli.py are intentional (the one-time key reveal in clawmetry key create) and cannot be suppressed with inline comments in the default CodeQL setup. A repo admin must dismiss them in the GitHub Security tab.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Update on E2E Browser Tests failure

E2E Browser Tests (critical subset) also failed on the merge commit for the same base-branch reason: PR #5910 introduced pyee==14.0.0 in .github/requirements/playwright-e2e.txt, but pyee==14.0.0 requires Python >=3.12 and CI runs Python 3.11. The feat branch has pyee==13.0.1 (correct).

This is the third broken pin from that same merge:

File Broken pin Correct version
fuzz.txt atheris==3.1.0 (non-existent) atheris==3.0.0
python-deps.txt chardet==7.6.0 (non-existent) chardet==5.2.0
playwright-e2e.txt pyee==14.0.0 (requires Py 3.12+) pyee==13.0.1

All three fixes are being pushed to fix/revert-broken-deps-from-5910. A fix PR targeting main will follow shortly. Once it merges, the merge commit here will pick up clean deps and E2E Gate should go green (assuming CodeQL's two intentional alerts are also dismissed by a repo admin).

This PR's branch is clean - all three requirements files on feat/build-your-own-ui have the correct versions.


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.

Copy link
Copy Markdown
Owner Author

✨ auto-fixed: merged origin/main (2 commits ahead) into branch — now up to date with main.


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Auto-janitor CI triage (2026-09-12)

Two checks are failing on the current head (1b058e4):

1. E2E Browser Tests (critical subset) — FAILED

Failure in the pip install step for .github/requirements/playwright-e2e.txt:

ERROR: Could not find a version that satisfies the requirement pyee==14.0.0
       (from versions: ..., 13.0.1)
       Ignored: 14.0.0 Requires-Python >=3.12

playwright==1.62.0 depends on pyee==14.0.0, but the CI job runs Python 3.11 — pyee 14.x is 3.12-only. The same base (eba6c067) passes this check on other PRs (e.g. #5848), so the requirements pin was likely introduced in this branch. Fix: downgrade the pyee pin in .github/requirements/playwright-e2e.txt to ==13.0.1 (last 3.11-compatible release), or bump the E2E job's Python to 3.12.

2. CodeQL — FAILED

Job 103603834942 failed on this PR while the same CodeQL job passes on other PRs with the same base. The logs were not retrieved in this sweep; worth checking manually at the job URL linked in the Checks tab.

Not a transient flake — both failures are deterministic on this branch. No rerun attempted.


Generated by Claude Code

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026
…_id in revoke response

routes/public_api.py: replace _stored_map.get(_norm) with a generator
expression over all_live_origins(). CodeQL traced _norm (derived from the
user-supplied Origin header) through dict.get() into the ACAO response
header. With next((_s for _s in all_live_origins() if ... == _norm), None)
the header value is always a stored string; _norm appears only in the filter
predicate and cannot propagate taint to the result (CWE-113).

routes/apikeys_admin.py: remove "id": key_id from the DELETE /api/apikeys/<id>
response. key_id is a URL route parameter (user-controlled); echoing it back
in the JSON body is a reflected-content sink CodeQL flagged as HIGH.

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

Copy link
Copy Markdown
Owner Author

CodeQL found 2 HIGH findings. Both fixed in commit c67a841:

Finding 1 — CWE-113 in routes/public_api.py _add_cors

_stored_map.get(_norm) — CodeQL traced _norm (derived from the user-supplied Origin request header, tainted) through dict.get() into response.headers["Access-Control-Allow-Origin"]. Even though matched came from the dict's VALUES (file-backed, untainted), CodeQL's taint model propagates through dict.get() with a tainted key.

Fix: replaced the dict approach with a generator expression over apikeys.all_live_origins(). The _norm value now appears only as a filter predicate; next() yields _s from the file-backed store, so CodeQL can see the header value's source is untainted.

Finding 2 — Reflected user input in routes/apikeys_admin.py api_keys_revoke

return jsonify({"ok": True, "id": key_id})key_id is the URL route parameter (user-supplied). CodeQL flagged it as reflected content flowing directly into the response body.

Fix: removed "id": key_id from the response. The caller already knows the key_id (they sent it in the DELETE URL); {"ok": True} is sufficient.


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.

github-actions Bot pushed a commit that referenced this pull request Sep 12, 2026
…e in 403/400

Three patterns that CodeQL flags as HIGH security findings:

1. _add_cors: switch from generator-predicate to list.index() approach.
   Generator equality predicates (`_s for _s in store if tainted == _s`)
   may propagate taint to the yielded value in CodeQL's model. list.index()
   returns an integer; integers are never tainted, so store[integer] is
   provably untainted when it reaches the ACAO response header.

2. q_shape 403: remove {shape!r} from the error message. At that point
   shape has been validated against QUERY_CONTRACT, but CodeQL does not
   cross-procedure-track that invariant -- it still sees URL-param -> body.
   The required_scope/held_scopes fields carry enough to act on.

3. q_shape 400: same fix -- remove {shape} from the ValueError message.
   The missing_args field carries the full information the caller needs.

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

Copy link
Copy Markdown
Owner Author

Pushed commit 3f8a5065 with three additional CodeQL hardening fixes:

1. _add_cors — switched to list.index() approach (replaces generator)

The generator expression (_s for _s in store if tainted == _s) may still propagate taint in CodeQL's model because an equality comparison in the predicate can make the yielded value "equivalent" to the tainted one. Using list.index() instead:

  • _norm (tainted) is the search argument
  • list.index() returns an integer — integers are untainted in CodeQL's model
  • _stored[integer_idx] retrieves a stored value — definitively untainted

2. q_shape 403 — removed {shape!r} from the error message

Although shape is validated against QUERY_CONTRACT before reaching the 403 branch, CodeQL does not cross-procedure-track that invariant. The required_scope and held_scopes fields carry the same actionable information without reflecting URL input.

3. q_shape 400 — removed {shape} from the ValueError message

Same pattern: missing_args already names the missing arguments; including shape in the message body adds nothing.

https://claude.ai/code/session_01SW1omwCgnNgaEo9khU8atc


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.

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.

3 participants