Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
124 commits
Select commit Hold shift + click to select a range
6de06d5
feat: build your own UI — a keyed, scoped read API over the q/1 contract
Sep 8, 2026
fff34ae
docs: name the Factory requirement in the two new module docstrings
Sep 8, 2026
1c25f22
docs: point the key CLI at the blueprint section that specifies it
Sep 8, 2026
c36efc0
refactor: key management moves to its own short module
Sep 8, 2026
343f43b
fix: close six of CodeQL's eight new alerts in code
Sep 8, 2026
7eff894
fix: a comment still pointed key management at routes/infra.py
Sep 8, 2026
c45dd0b
fix: suppress CodeQL clear-text-logging findings on intentional key d…
Sep 8, 2026
0c34843
chore: regenerate docs/MODULE_MAP.md (232 -> 233 modules)
claude Sep 9, 2026
7a844a7
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 9, 2026
819e1e6
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 9, 2026
53ebceb
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 9, 2026
72a0c4e
chore: merge main into feat/build-your-own-ui (MODULE_MAP count confl…
claude Sep 9, 2026
e693c16
Merge origin/main into feat/build-your-own-ui; regenerate MODULE_MAP
claude Sep 9, 2026
2b9a5bc
security: fix two CodeQL high-severity findings in public_api
claude Sep 10, 2026
10ebd64
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 10, 2026
0dde6fb
merge main into feat/build-your-own-ui: resolve MODULE_MAP conflict
claude Sep 10, 2026
b15335c
Merge origin/main into feat/build-your-own-ui (conflict: module count)
claude Sep 10, 2026
5547ae7
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 10, 2026
66c971b
chore: regenerate docs/MODULE_MAP.md for routes/public_api.py
claude Sep 10, 2026
c751ee8
Merge branch 'main' into feat/build-your-own-ui
Sep 10, 2026
1592e2e
Merge branch 'main' into feat/build-your-own-ui
claude Sep 11, 2026
777afd0
fix(security): use stored canonical origin in CORS header to fix CWE-113
claude Sep 11, 2026
3620f7a
chore: merge origin/main into feat/build-your-own-ui (resolve MODULE_…
claude Sep 11, 2026
d4ba30f
Merge remote-tracking branch 'origin/feat/build-your-own-ui' into fea…
claude Sep 11, 2026
3d6dd40
chore: regenerate docs/MODULE_MAP.md (239 -> 240 modules)
claude Sep 11, 2026
f08ab37
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 11, 2026
7e1630b
fix(security): add explicit regex sanitizers for CodeQL CWE-113 alerts
claude Sep 11, 2026
5b1d1f8
fix: use codeql[] suppression syntax for plaintext key display
vivekchand Sep 11, 2026
8c74f3c
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 11, 2026
e63d201
Merge branch 'main' into feat/build-your-own-ui
claude Sep 11, 2026
92ba36b
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 11, 2026
0f86412
fix(changelog): replace em-dash with comma in build-your-own-ui entry
claude Sep 11, 2026
e77cc21
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 11, 2026
92466fe
chore: tighten CI test-file coverage ratchet to 929 unlisted
claude Sep 11, 2026
15f7f58
merge: resolve MODULE_MAP.md and ci_test_coverage_baseline conflicts …
claude Sep 11, 2026
840305a
merge: bring feat/build-your-own-ui up to date with main
Sep 11, 2026
ec24fdd
Merge branch 'main' into feat/build-your-own-ui
claude Sep 12, 2026
7e8ba7f
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
14b9302
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
54dd1a1
fix(public-api): break CodeQL CWE-113 taint chains in CORS and llms.txt
claude Sep 12, 2026
9aa4830
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
4802e3e
fix(public-api): suppress CodeQL stack-trace-exposure false positive …
claude Sep 12, 2026
5594c77
fix(codeql): add suppression comment to print statement start line
claude Sep 12, 2026
8ae5ea3
Merge branch 'main' into feat/build-your-own-ui
claude Sep 12, 2026
37f1f26
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
4ff80c1
chore: regenerate MODULE_MAP.md (253 modules, 84 blueprints)
claude Sep 12, 2026
ce29367
fix(public-api): use apikeys helpers and regex group as CodeQL saniti…
claude Sep 12, 2026
1badfce
fix: use regex match group in _add_cors to satisfy CodeQL CWE-113
vivekchand Sep 12, 2026
fbb155b
fix: break CWE-113 taint chain by never passing origin to CORS helpers
vivekchand Sep 12, 2026
71094a0
docs: trim redundant CWE-113 prose from _add_cors docstring
vivekchand Sep 12, 2026
9a24618
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
05e6b1b
fix(codeql): eliminate CWE-113 by routing ACAO value through all_live…
claude Sep 12, 2026
c56474a
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
1b058e4
fix: resolve CodeQL CWE-113 HTTP response splitting in public_api
vivekchand Sep 12, 2026
fd064b1
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 12, 2026
c67a841
fix: break CodeQL CWE-113 taint chain in _add_cors; don't reflect key…
vivekchand Sep 12, 2026
3f8a506
fix: harden CodeQL CWE-113 via list.index() and remove reflected shap…
claude Sep 12, 2026
8ab34bc
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 13, 2026
46e0b3f
fix(public_api): resolve CodeQL CWE-113 and CWE-79 findings in _add_c…
claude Sep 13, 2026
abb205c
fix(codeql): add suppression comment on closing line of multi-line print
claude Sep 13, 2026
87d914b
chore: merge main into feat/build-your-own-ui, keep PR module count (…
claude Sep 13, 2026
ddef5d1
fix: suppress 2 high CodeQL alerts in public API and apikeys_admin
vivekchand Sep 13, 2026
b6f94bf
chore: regenerate MODULE_MAP.md after CodeQL suppression refactor (25…
claude Sep 13, 2026
a2c69e7
fix: suppress CodeQL clear-text-logging and reflected-xss alerts
Sep 13, 2026
a173952
fix: remove reflected-content sink and suppress CWE-113 in public API
claude Sep 13, 2026
23b19f4
fix(public_api): eliminate CodeQL taint paths for header injection an…
claude Sep 13, 2026
2efc38b
fix(public_api): add correct CodeQL suppression for CWE-113 header in…
claude Sep 13, 2026
0cd343b
fix(public_api): add lgtm suppression for both CodeQL high alerts
claude Sep 13, 2026
c0a02b8
fix(public_api): structurally break CodeQL taint paths for CWE-113 an…
claude Sep 13, 2026
532f422
fix(security): break CodeQL reflected-content taint paths for CWE-79
claude Sep 13, 2026
eca073f
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 14, 2026
cbbf0be
Merge origin/main into feat/build-your-own-ui
claude Sep 14, 2026
6aea869
chore: regenerate docs/MODULE_MAP.md after cost_optimizer_advice addi…
claude Sep 14, 2026
9d32918
test: refresh _NOW at seed time to fix token_velocity fast-path miss
claude Sep 14, 2026
3d0a455
Merge origin/main into feat/build-your-own-ui
claude Sep 14, 2026
8e6af23
Merge origin/main into feat/build-your-own-ui
claude Sep 14, 2026
d34c87f
regenerate MODULE_MAP.md for new modules in feat/build-your-own-ui
claude Sep 14, 2026
c016c70
Merge origin/main into feat/build-your-own-ui
claude Sep 14, 2026
743310d
chore: regenerate MODULE_MAP after merge from main
claude Sep 14, 2026
e3188b4
fix: break CodeQL taint chains in public_api — re-fetch spec and vali…
claude Sep 14, 2026
8080070
Merge origin/main into feat/build-your-own-ui
claude Sep 14, 2026
063cb08
chore: regenerate MODULE_MAP after merge from main
claude Sep 14, 2026
352b826
Merge origin/main into feat/build-your-own-ui to resolve divergence
claude Sep 14, 2026
6ae9f59
chore: regenerate MODULE_MAP after merge from main
claude Sep 14, 2026
f81f386
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 15, 2026
e6db27d
fix(cli): rename key variable to avoid CodeQL sensitive-data taint
Sep 15, 2026
6c519b8
fix(cli): suppress CodeQL clear-text-logging on intentional key display
claude Sep 15, 2026
22b2f87
Merge branch 'main' into feat/build-your-own-ui
claude Sep 15, 2026
ba5c704
fix(cli): route key display through sys.stdout.write to clear CodeQL …
claude Sep 15, 2026
1789494
fix(apikeys): break CodeQL taint path by deriving key material throug…
claude Sep 15, 2026
158a390
fix(apikeys): add storage-sensitive-data suppression at remaining Cod…
claude Sep 15, 2026
893a67d
fix(apikeys): move CodeQL suppressions to preceding lines in cli.py
claude Sep 15, 2026
b869c05
fix(apikeys): suppress CodeQL alerts on the sink lines in cli.py
claude Sep 15, 2026
828dd87
fix: break CodeQL taint chain for cmk_ key output in CLI
claude Sep 15, 2026
eaad681
Merge branch 'main' into feat/build-your-own-ui
claude Sep 15, 2026
8c85048
Merge branch 'main' into feat/build-your-own-ui
claude Sep 15, 2026
8b8d01d
refactor(apikeys): extract public helpers to apikeys_public.py for Dr…
claude Sep 15, 2026
53041c5
fix(apikeys): fix F401 ruff errors introduced by re-export refactor
claude Sep 15, 2026
ed47425
Merge main into feat/build-your-own-ui; regenerate MODULE_MAP.md
claude Sep 15, 2026
f3af5cb
ci: retrigger merge-check
claude Sep 15, 2026
ea35ace
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 15, 2026
6c04b2a
Merge branch 'main' into feat/build-your-own-ui
vivekchand Sep 15, 2026
ea407bc
feat: build your own UI — a keyed, scoped read API over the q/1 contract
Sep 8, 2026
50f277b
refactor: key management moves to its own short module
Sep 8, 2026
1ff6113
feat(ingest): a scoped ingest key, so an agent off this machine can b…
vivekchand Sep 8, 2026
120d159
docs(ingest): declare the ingest contract once, generate the reference
vivekchand Sep 8, 2026
faabca6
feat(onboarding): a setup prompt you hand to your agent
vivekchand Sep 8, 2026
8a9a43a
feat(onboarding): say whether data is actually arriving
vivekchand Sep 8, 2026
602419d
docs(meta): correct the route index that said bp_otel had three routes
vivekchand Sep 8, 2026
b8555f0
fix: remove duplicate api_onboarding_ingest_status route
vivekchand Sep 11, 2026
f7e6908
fix: remove duplicate api_onboarding_ingest_status route
vivekchand Sep 11, 2026
37f0a74
chore: regenerate MODULE_MAP.md
Sep 15, 2026
ba0e2fe
chore: regenerate MODULE_MAP.md (277 → 278 modules)
claude Sep 15, 2026
cb77f96
Merge branch 'main' into feat/build-your-own-ui
claude Sep 15, 2026
3e4171c
Merge branch 'feat/build-your-own-ui' into feat/ingest-key
claude Sep 15, 2026
f93661e
chore: regenerate MODULE_MAP after owner additions (cb77f96)
claude Sep 15, 2026
0d70a55
chore: regenerate MODULE_MAP.md after feat/build-your-own-ui merge
claude Sep 16, 2026
e4360c4
fix: add missing ingest key constants to ingest_contract
claude Sep 16, 2026
12f7733
chore: merge feat/build-your-own-ui into feat/ingest-key
claude Sep 16, 2026
47cc446
fix: remove em-dashes from CHANGELOG entries (FLYWHEEL 1f3)
claude Sep 16, 2026
858d231
fix: add non-goals section to INGEST.md (syslog, CEF not accepted)
claude Sep 16, 2026
721dbb4
chore: merge main into feat/ingest-key, resolve conflicts
claude Sep 16, 2026
4414228
fix(security): remove exception detail from OTLP 400 response; saniti…
claude Sep 16, 2026
7e7c4b4
Merge branch 'main' into feat/ingest-key
vivekchand Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,10 @@ jobs:
tests/test_otel_export_sessions_shape.py \
tests/test_query_contract_drift.py \
tests/test_public_api_keys.py \
tests/test_ingest_key.py \
tests/test_ingest_contract_drift.py \
tests/test_setup_prompt.py \
tests/test_ingest_status.py \
tests/test_query_contract_goldens.py \
tests/test_local_store_concurrent_flush_1590.py \
tests/test_duckdb_invalidated_recovery.py \
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,39 @@
- **Cloud is unaffected by construction:** the hosted container has no discovery file, so no round trip is attempted and nothing is stamped.
- **Verified:** 17 tests in `tests/test_store_unreachable_is_not_empty.py`, named in `ci.yml`, each proven to fail against the unfixed code before being trusted. Closes #5534.

### Added: the first-run gate now says whether data is actually arriving (2026-09-08)
- **Why:** a user who installs ClawMetry and sees an empty dashboard cannot tell "nothing is running" from "it is broken", and that question is what kills setup funnels. We have the number: 285 launches produced 13 choices in 14 days on the old gate. The gate already named the runtimes it detected; it never said whether any of them had produced a single event.
- **What:** `GET /api/onboarding/ingest-status` answers it from real data: total events, the runtimes actually sending, and the OTLP receiver's own state; and the gate renders it as a live strip that polls while it is open. When nothing has arrived the strip says so **and what to do about it**, including `clawmetry setup-prompt` for an agent that runs somewhere else. It never blocks: this is a confirmation, not a step, in a flow whose whole selling point is having no steps.
- **Two clocks, kept apart on purpose.** The durable count comes from DuckDB and survives a restart; the OTLP receiver's counters are in-process and empty on restart, so they are reported separately and named `has_data_this_process`. Folding them together would tell a working install it was broken every time the dashboard restarted.
- **A runtime we merely know about is not a runtime that is sending.** Rows with no sessions and no tokens are dropped rather than listed, because listing them would answer "is anything arriving?" with a yes they have not earned, the same shape as a tab that renders empty and calls it success. And the rollup is one row per runtime *per day*, so a runtime sending for a week arrived seven times; the endpoint collapses to one row each.
- **Read through `local_query._dispatch`, never from raw files**, so it answers identically on a laptop and in a cloud container with no `~/.openclaw`. Memoised for 2s because it is polled: the first call is ~200 ms against a real store, the rest are ~0.2 ms.
- **Verified in both states against a running dashboard:** the strip renders inside the card in the real page, an empty store returns `connected: false` with an actionable `next_step`, and the populated store on this machine returns 25,349 events across 9 runtimes. 9 guards in `tests/test_ingest_status.py`, three mutation-proven: reporting an idle runtime as a source, dropping the memo, and never stopping the poll each turn one red. The last one exists because a dismissed modal that keeps fetching is the same defect as the Home widget that fetched every sub-agent into a hidden element.
- **Refs** #5680.

### Added: a setup prompt you hand to your agent (2026-09-08)
- **Why:** ClawMetry's users delegate work to coding agents by definition (that is what the product observes), yet the only setup paths shipped were "run the installer" and "read a doc", neither aimed at the thing the user actually drives. `clawmetry setup-prompt <runtime>` and `GET /api/setup-prompt` print a prompt written for the agent, for the case auto-detection cannot cover: an agent in CI, a container, a serverless function, or on somebody else's laptop.
- **Generated, not written.** Every endpoint, header, content type and cap comes from `clawmetry/ingest_contract.py`, the same declaration the server validates against. A hand-written prompt drifts the first time a header is renamed, and a drifted setup prompt is worse than none: the agent writes the wrong header *confidently* and the request fails where nobody is looking.
- **Half of it is negative space**, which is the useful half. Coding agents reliably mis-substitute secrets, "correct" a content type that was already right, and invent config keys that look plausible. So the prompt says the key goes in one header and nowhere else; that the placeholder is a placeholder and the real key must be asked for, not invented; that both encodings are already accepted and need no fixing; that a key not in the prompt does not exist; and that this is observability: it watches, it does not change what runs. It ends by making the agent verify and report the real event count, because an agent that checks its own work fails loudly instead of silently.
- **Two guards worth keeping.** The first reads *backwards*: every `x-clawmetry-*` token in the prompt must be a header the contract declares. 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 also be in `cli.py`'s `_subcmds` allowlist**, because a parser without an entry there falls through to the dashboard's argparse and dies with "invalid choice", which reads like 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 seam holds:** this module names no runtime and hardcodes no vendor value, pinned by a test. Where a runtime has registered an OTel profile (paid runtimes register theirs from clawmetry-pro), its label and `clawmetry instrument` support are read at render time; a free install gets the generic OTLP prompt, which works.
- **Refs** #5681.

### Added: the ingest contract is declared once, and the reference is generated from it (2026-09-08)
- **Why:** four things describe what ClawMetry accepts: the server that validates requests, `docs/INGEST.md`, the per-runtime setup prompts, and the public reference on the landing site. None of them shared a source. Four hand-maintained descriptions of one contract is four chances to drift, and the drift is worst in the prompts: a prompt that teaches an agent a header we do not accept is worse than shipping no prompt at all, because the agent writes it confidently and the failure is silent. The landing side now enforces this from the other direction too: drift-bot fails a public claim the repo denies.
- **What:** `clawmetry/ingest_contract.py` declares the surfaces, headers, encodings, caps, response codes, auth modes and GenAI attributes as data. `clawmetry/ingest_auth.py` imports its constants from there rather than keeping its own, so the thing the server enforces and the thing we publish cannot diverge. `scripts/gen_ingest_doc.py` renders `docs/INGEST.md` from it with a `--check` mode, on the same pattern as `gen_query_contract_doc.py` on the read side.
- **The guard that earns the file.** Declaring "we read `gen_ai.usage.cache_read.input_tokens`" is cheap; the claim is only worth printing if something checks it. The test asserts every attribute declared read is actually named in the mapper **and** that every attribute declared unread really is, in both directions, because someone wiring one up should have to move it in the same change, so the published reference is never behind the code either. Writing that check is what surfaced #5685: we were advertising a convention we did not implement, and cached tokens were being priced as free.
- **The doc says what we do NOT accept**, and that section is pinned by a test. There is no endpoint for syslog, CEF, GELF or raw text, and there is not going to be: ClawMetry's inputs are typed on arrival, and a parser layer would exist only to accept data this product has nothing to say about. A reference that only says what works is not one anybody can plan against.
- **Verified by mutation:** claiming a made-up attribute is read turns 2 guards red; claiming a genuinely-read one is unread turns 3 red. 26 guards in `tests/test_ingest_contract_drift.py`, registered in `ci.yml`.
- **Refs** #5682.

### Added: an ingest key, so an agent that is not on this machine can be observed (2026-09-08)
- **Why:** until now 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: no rotation, no revocation, 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. That is one capability, not a catalogue, and it is what stands between the product and "every agent, anywhere it runs".
- **What:** `write:ingest`, a scoped key created with `clawmetry key create --name ci --scope write:ingest` and presented as `x-clawmetry-key: cmk_...` 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`. Plus two routing headers (`x-clawmetry-runtime` and `x-clawmetry-env`) because a pushed batch carries no filesystem layout to infer a runtime from. They 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. Deliberately one grouping axis, not a dataset/collection/tag taxonomy: that is what a log platform needs and an agent platform does not.
- **The posture, stated plainly.** 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, so a page cannot hold one usefully: 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. And there is **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, so `401` says how to create a key, `403` says the key is fine but may not push, `400` on a bad runtime header shows the shape it wanted, `400` on a bad body names both accepted encodings, and `413` gives the size and the limit instead of failing somewhere inside a protobuf parser.
- **Verified live, not only in tests.** Against a real dashboard on port 8917 with a gateway token set: all three doors return 200 (loopback with no key at all, the zero-config path, unchanged; gateway token; ingest key), and every refusal returns its own status with its own sentence. 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 without headers still lands as `openclaw`/`unknown_service`, the pre-existing behaviour, untouched. The live run is also what found the `/api/q/1` hole: a write-only key was being handed a 200 index, now a 403, pinned by a test. 25 guards in `tests/test_ingest_key.py`.
- **Refs** #5679. Part of phase 1 of the ingest plan (#5680, #5681, #5682, clawmetry-cloud#2343, #2344, clawmetry-pro#230).

### Fixed: two functions named `_session_cwd`, and the later one silently replaced the other (2026-09-08)
- **Why:** shipped in 0.12.837 and live through 0.12.839. `sync.py` already had `_session_cwd(row)`, a raw adapter/gateway dict read through a **twelve-alias** set (`cwd`, `workingDir`, `workingDirectory`, `workspace`, `workspaceRoot`, `project_dir`, `projectRoot`, `directory`, `folder`, ...). The workspace-scan change added a second function with the same name 2,000 lines below it, reading two keys. Python keeps the last definition and says nothing, so all THREE callers of the original quietly switched: gateway session shaping (`sync.py:4328`), the session row build (`4586`), and the FAMILY ingest cwd (`15293`). That last one writes `sessions.cwd`, which is the column `process_control` promotes to find a pid and the one the workspace scanner keys on, so a runtime that spells its directory `workingDir` or `directory` in metadata stopped persisting it. Nothing failed. The tests passed. Three wheels shipped.
- **What:** the newer helper becomes `_session_row_cwd` (it reads a store ROW: the `cwd` column first, then metadata), and its metadata fallback now DELEGATES to `_session_cwd`, so both paths honour the same alias set. Both are strictly better than before the collision. A second shadowing found by the same walk is also removed: `start_log_streamer` was defined twice, the first an empty docstring-only stub.
Expand Down
55 changes: 55 additions & 0 deletions clawmetry/apikeys.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
is created with ``--origin none`` and simply never gets a CORS header.
* Scopes are least-revealing-first and ``read:content`` is never
granted implicitly -- it has to be asked for by name.
* Read scopes are read-only. ``routes/public_api.py`` dispatches only
``q/1`` read shapes, so a browser-resident key adds nothing to
ClawMetry's control plane.
* There is exactly one write scope, ``write:ingest``, and it can only
push telemetry IN. It cannot read a single byte back, and nothing in
this module can pause, stop or kill an agent. An ingest key is
server-to-server: it is never granted a CORS header, so a page cannot
hold one usefully (see ``ingest_auth``).
* Keys are read-only. Nothing in this module can pause, stop or kill an
agent, and ``routes/public_api.py`` dispatches only ``q/1`` read
shapes, so this adds nothing to ClawMetry's control plane.
Expand All @@ -57,6 +65,11 @@
import time
from typing import Optional

from clawmetry.query_contract import (
SCOPE_CONTENT,
SCOPE_DOC as _READ_SCOPE_DOC,
SCOPES as READ_SCOPES,
)
from clawmetry.query_contract import SCOPES

# Re-export the read-side public helpers from their own short module so
Expand Down Expand Up @@ -92,6 +105,23 @@
#: script cannot grow the file without bound; it is not a paywall.
MAX_KEYS = 50

#: The one write scope. It lives here rather than in ``query_contract``
#: on purpose: that module declares what can be READ, shape by shape,
#: and a scope with no shape behind it would be a lie in that table.
#: Ingest is the opposite direction and has no q/1 method at all.
SCOPE_INGEST = "write:ingest"

#: Every scope a key may carry. Read scopes stay in their declared
#: least-revealing-first order; the write scope sorts last because it is
#: the one a reader should notice.
SCOPES: tuple = tuple(READ_SCOPES) + (SCOPE_INGEST,)

SCOPE_DOC: dict = dict(_READ_SCOPE_DOC)
SCOPE_DOC[SCOPE_INGEST] = (
"Push telemetry in: OTLP logs, metrics and traces, and run events. "
"Grants no read access of any kind."
)

#: Sentinel origin meaning "this key is not used from a browser". Stored
#: as an empty origin list; kept as a word so the CLI can say it back.
ORIGIN_NONE = "none"
Expand Down Expand Up @@ -307,6 +337,25 @@ def create(name: str, scopes, origins, *, note: str = "") -> tuple:
scope_list = normalise_scopes(scopes)
origin_list = normalise_origins(origins)

# An ingest key is server-to-server and is never granted a CORS
# header, so browser origins on one would be dead configuration that
# reads like a permission. Refusing the mix also keeps a single key
# from being both "pasted into a web page" and "allowed to write",
# which is the combination worth not having.
if SCOPE_INGEST in scope_list:
if len(scope_list) > 1:
raise ApiKeyError(
"An ingest key does one job. Create it with write:ingest "
"alone, and mint a separate read key for anything that "
"needs to read data back."
)
if origin_list:
raise ApiKeyError(
"An ingest key is used by a server, a container or a CI "
"job, never by a browser, so it takes no origin. Create "
"it with --origin none."
)

doc = _read_store()
live = [k for k in doc["keys"] if not k.get("revoked_at")]
if len(live) >= MAX_KEYS:
Expand Down Expand Up @@ -560,6 +609,12 @@ def any_canonical_allowed_origin(origin: str) -> "str | None":
return None


def allows_ingest(record: dict) -> bool:
"""True when this key may push telemetry in."""
return SCOPE_INGEST in (record.get("scopes") or [])



def redact(presented: str) -> str:
"""``cmk_a1b2c3d4_...`` -- safe to log. Shows the id, never the secret."""
parsed = parse(presented)
Expand Down
18 changes: 14 additions & 4 deletions clawmetry/apikeys_public.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,37 @@ def granted_shapes(record: dict) -> set:


def scope_catalogue() -> list:
"""``[{scope, doc, methods, sensitive}]`` for the UI and the CLI help.
"""``[{scope, kind, doc, methods, sensitive}]`` for the UI and the CLI help.

Derived from the query contract, so a method added there shows up
here with no second list to update.
Read scopes are derived from the query contract; write:ingest is loaded
lazily from apikeys so the ingest scope doc stays in one place.
"""
from clawmetry.query_contract import (
SCOPE_CONTENT,
SCOPE_DOC,
SCOPES,
live_methods_by_scope,
)
from clawmetry.apikeys import SCOPE_INGEST, SCOPE_DOC as _AK_SCOPE_DOC

return [
rows = [
{
"scope": s,
"kind": "read",
"doc": SCOPE_DOC[s],
"methods": live_methods_by_scope(s),
"sensitive": s == SCOPE_CONTENT,
}
for s in SCOPES
]
rows.append({
"scope": SCOPE_INGEST,
"kind": "write",
"doc": _AK_SCOPE_DOC.get(SCOPE_INGEST, ""),
"methods": [],
"sensitive": False,
})
return rows


def store_summary() -> dict[str, Any]:
Expand Down
Loading
Loading