Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 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
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ jobs:
tests/test_outcome_trend.py \
tests/test_otel_export_sessions_shape.py \
tests/test_query_contract_drift.py \
tests/test_public_api_keys.py \
tests/test_query_contract_goldens.py \
tests/test_local_store_concurrent_flush_1590.py \
tests/test_duckdb_invalidated_recovery.py \
Expand Down
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,13 @@
- **It found pre-existing debt, recorded as a ratchet rather than fixed here:** `dashboard.py` carries 59 shadowed names (39 byte-identical dead pairs and **20 that differ**, meaning someone edited one copy and not the other), and `routes/entitlement.py` carries 1. `verification/shadowed_definitions.json` holds those counts; the test asserts a file never rises above its baseline and that each baseline equals reality, so no headroom is left for the next one and the debt can only shrink.
- **Verified:** restoring the old name reproduces `_session_cwd: 2` in the AST walk and reds the guard; with the fix, 235 module files pass and 273 tests pass across the daemon-wiring, detector and workspace-kind suites.

### Added: build your own UI, a keyed and scoped read API over the q/1 contract (2026-09-08)
- **Why:** ClawMetry ships one dashboard, and a user who wants a different view of their own data has no supported way to get one. Every number the dashboard draws already comes from a declared, versioned read contract (`q/1`, 17 live methods, per-method arg schemas, additive-only, drift-tested). 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 was no API key anywhere, and no `Access-Control-Allow-Origin` header anywhere in the codebase. So the options were fork the dashboard, scrape the HTML, or do without. Meanwhile the generation tools got good enough that a purpose-built view is an afternoon of work *if* the data has an addressable API. Ours did not have a door.
- **What:** 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`, in `clawmetry/query_contract.py` 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. (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 carrying scopes and a browser-origin allowlist. (3) **`GET /api/q/1/<shape>`** (`routes/public_api.py`), dispatching through the *same* `routes.local_query._dispatch` the dashboard uses, so there is no second query surface to keep correct. (4) **`GET /api/q/1/llms.txt`**, the whole API written for a coding agent to read in one pass, generated from the contract and scoped to the presented key. Plus 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, stated plainly.** 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 feature 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, and a key is always required); **there is no wildcard origin**, not as a flag and not as an env var, because every "just for local dev" wildcard ships eventually and fails silently and totally; **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, and `_add_cors` is pinned to `/api/q/` as belt and braces); and **the API is GET only**, which removes cross-origin writes as a category rather than as a check. Nothing here can pause, stop or kill an agent: this adds no entry to the control plane.
- **`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 ships asking 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**, the numbers 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 refusing it with `Failed to fetch` before the response was readable. Plus **48 guards** in `tests/test_public_api_keys.py` (registered in `ci.yml`, since CI runs explicit file lists), with three deliberate mutations proving they go red: making `_add_cors` echo every origin fails 4, removing the scope check fails 2, and reclassifying `transcript` as `read:metrics` fails 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`.

### Fixed: a poisoned linked worktree scanned CLEAN (2026-09-08)
- **Why:** `repo_scan` opened `<workspace>/.git/config` and nothing else. In a **linked worktree** `.git` is a FILE holding `gitdir: /abs/path`, and the config git actually reads lives in the common directory that path points at, so the scanner found no config and returned no findings. That is worse than returning nothing: it is a confident all-clear over a repository git will happily execute `core.fsmonitor` from, on a layout agents and CI use routinely. CVE-2026-55607 is the vendor-confirmed version of the same git-directory confusion. Found by `scripts/redteam/audit.py` itself, which filed the gap automatically: the audit doing exactly the job it was wired into CI for one release earlier.
- **What:** `_git_dirs()` resolves `(git_dir, common_dir)` through the `.git` file's `gitdir:` and the worktree's `commondir`. `_git_config_paths()` returns every config git reads, which is `<common>/config` plus `<gitdir>/config.worktree` (honoured when `extensions.worktreeConfig` is set, and writable by whoever supplied the worktree). The finding reports the config it was actually read from, trimmed to its last three components, because an incident travels to a UI and to a device and must not carry the user's home directory. Ordinary checkouts are untouched: `_git_dirs` answers with `.git` for both values and the label stays `.git/config`.
Expand All @@ -357,8 +364,7 @@
- **What:** three PASS and are now cases. `npm-postinstall-visible-harvest` (credential_access, critical), `mcp-tool-output-prompt-injection` (credential_access, critical), and `mcp-injection-without-exfil` (privilege_change). The two MCP cases are deliberately a pair: the injection itself is invisible to us, since it arrives in a tool RESULT and no detector reads a result as an instruction, so what the pair measures is whether the BEHAVIOUR an injection produces is caught, in both the exfiltration shape and a non-exfiltration one. `control-ordinary-dev-session` is the negative control that belongs beside them: install, read `.env.example`, run tests, push, and stay silent.
- **The fourth is a MISS, and it is filed rather than shipped red.** `npm-postinstall-credential-harvest`: nothing fired at all. It is not a tuning problem. For the invisible half of the attack the agent chose exactly one action, `npm install`, and npm's child processes did the rest, so the event stream is byte-for-byte the stream of a developer installing a dependency. No tool-stream detector can separate them. The fix is the workspace surface, the same answer `repo_scan` gave for GitSpawn, and the gap carries the audit's own verdict on the private tracker.
- **The README now records what is deliberately NOT in the corpus**, so the next person does not rediscover it: cross-session coordination (the Hugging Face evaluation swarm) is not merely undetected but **inexpressible**, because `run_all` takes one session's events and so does the corpus runner; and credential-scoping bypass has no observable in the stream at all.
- **Verified:** `scripts/redteam/audit.py` reports 13/13 pass, 0 gaps, 0 control failures; the corpus suite is 28 tests, up from 24. Both run on every PR through the gate added in 0.12.837.

- **Verified:** `scripts/redteam/audit.py` reports 13/13 pass, 0 gaps, 0 control failures; the corpus suite is 28 tests, up from 24. Both run on every PR through the gate added in 0.12.837. b2bf019 (feat: build your own UI, a keyed, scoped read API over the q/1 contract)
### Added: the daemon scans a session's workspace, which is the surface the tool stream cannot see (2026-09-08)
- **Why:** every behavioural detector reads what the agent *chose* to do. GitSpawn is the proof that this is a partial view of the machine: a poisoned `.git/config` makes **git** spawn the payload during the background `git status` a runtime fires on open, the agent calls no tool, and `detectors.run_all` sees a clean session. `clawmetry/repo_scan.py` closes that gap and has shipped for a while, detecting all three GitSpawn variants plus the CHAINDROP hook shapes, with `clawmetry scan-repo` exposing it to a human. Nothing in the product called it. A detector nobody runs protects nobody.
- **What:** `sync._emit_detector_incidents` now scans each session's `cwd` and emits findings on the path detector incidents already take: a `loop_signals` row (`daemon_detect_repo_config_exec` / `daemon_detect_agent_config_tamper`), `incident_alerts` delivery, and the heartbeat fold. The scan is cached on the mtime and size of exactly the files it reads (`.git/config`, `.vscode/tasks.json`, the `_AGENT_HOOK_FILES` entries), and nothing else, so an unchanged repo is never re-read and a repo poisoned *after* it was first seen clean is re-scanned on the next tick. Cache bounded at 500 directories, oldest evicted. `CLAWMETRY_REPO_SCAN=0` turns it off.
Expand Down
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ All HTTP endpoints live here, organised by feature: 70 modules, 82 blueprints, l
| `routes/channels.py` | `bp_channels` — 24 chat-channel adapters (Telegram, Signal, WhatsApp, Discord, Slack, IRC, iMessage, WebChat, …) |
| `routes/components.py` | `bp_components` — Flow-panel detail endpoints (tool / runtime / machine / gateway / brain) |
| `routes/local_query.py` | `bp_local_query` — `/api/local/*` DuckDB read API + the daemon-proxy `_dispatch` (shape→store bridge shared by HTTP and the cloud relay) |
| `routes/public_api.py` | `bp_public_api` — `/api/q/1/*`, the **keyed, cross-origin** read API custom UIs are built on (`docs/BUILD_YOUR_OWN_UI.md`). Same `_dispatch`, but it is the one surface that does not trust loopback: every request needs a scoped `cmk_` key, and CORS is echoed only for an origin that key named |
| `routes/apikeys_admin.py` | `bp_apikeys_admin` — `/api/apikeys`, minting and revoking the keys `public_api` accepts. Kept apart from that surface on purpose: it sits behind the dashboard's own cross-origin write guard and never carries a CORS header, so a page holding a read key can neither list this node's keys nor issue itself a wider one |
| `routes/guard.py` | `bp_guard` — live session control (Pause/Stop/Kill), Guard policy CRUD, policy decision log, learned baselines. Sessions ranked by **spend at risk**, not severity |
| `routes/policy.py` | `bp_policy` — the *pre-tool* sandbox/permission surface (`/api/tool-policy`). Deliberately a different axis from `routes/guard.py`: different table, no shared state |
| `routes/hooks.py` | `bp_hooks` — hook install / status / uninstall per runtime, and the gate's decision log |
Expand All @@ -66,11 +68,12 @@ All HTTP endpoints live here, organised by feature: 70 modules, 82 blueprints, l

| File | Purpose |
|------|---------|
| `clawmetry/cli.py` | CLI entry point — `clawmetry`, `connect`, `sync`, `status`, `license`, `hook`, `update` |
| `clawmetry/cli.py` | CLI entry point — `clawmetry`, `connect`, `sync`, `status`, `license`, `hook`, `key`, `update` |
| `clawmetry/sync.py` | Cloud sync daemon — ingests into DuckDB, owns the writer lock, runs the detectors and Guard policies, streams the E2E-encrypted (AES-256-GCM) snapshot to `ingest.clawmetry.com`. Holds `_FAMILY_ADAPTER_SPECS` (the adapters that actually load) and `_CHANNEL_DIRS` |
| `clawmetry/local_store.py` | **DuckDB store** — the single data layer features read and write (the daemon holds the writer lock). Schema v15 |
| `clawmetry/local_server.py` | Daemon-hosted localhost query server (`/local/query`, discovered through `~/.clawmetry/local_query.json`) so the dashboard reads DuckDB without grabbing the writer lock |
| `clawmetry/query_contract.py` | The declared node query surface (`q/1`), rendered to `docs/QUERY_CONTRACT.md`. Additive-only inside a version |
| `clawmetry/apikeys.py` | Scoped read keys (`cmk_…`) for custom UIs: mint, verify, revoke. SHA-256 in `~/.clawmetry/api_keys.json` (0600); the scope a key carries maps to `query_contract`'s per-method `scope` |
| `clawmetry/entitlements.py` | Single source of truth for tiers, `FREE_RUNTIMES` / `PAID_RUNTIMES`, `ALL_CHANNELS` and every capacity limit. GRACE by default |
| `clawmetry/license.py` | Offline Ed25519 verification of self-hosted license keys |
| `clawmetry/proxy.py` | Enforcement proxy — budget limits, loop detection, model routing (port 4100) |
Expand Down Expand Up @@ -116,6 +119,7 @@ All HTTP endpoints live here, organised by feature: 70 modules, 82 blueprints, l
| `docs/ENTITLEMENTS.md` | Open-core split: FREE runtimes/features, paid tiers, GRACE mode, `/api/entitlement` shape, `clawmetry license` CLI |
| `docs/EGRESS.md` | Every outbound destination, what it carries, and how to verify it on the wire |
| `docs/HOOK_COEXISTENCE.md` | How ClawMetry shares a runtime's hook config with other writers |
| `docs/BUILD_YOUR_OWN_UI.md` | The keyed read API, its scopes, and how to point a coding agent at it |
| `docs/CUSTOM_RUNTIME_INGEST.md` | The HTTP ingest API for a runtime with no adapter |
| `docs/EVENT_RETENTION.md` | Store growth and trimming |
| `CHANGELOG.md` | Version history |
Expand Down Expand Up @@ -170,6 +174,7 @@ The complete surface is generated at `/openapi.json` and browsable at `/api/docs
- `/api/signals` — Behaviour signal rates per window (`1d|7d|30d`) and `?runtime=`, with coverage and headline; `/api/signals/<name>/sessions` lists matching sessions, never phrases
- `/api/guard/sessions` — What is running, what a detector thinks has gone off track, and whether each session can be controlled at all; `/api/guard/control` is the Pause / Stop / Kill button and `/api/guard/policies` the autonomous rules
- `/api/entitlement` — The resolved entitlement (tier, allowed runtimes, features, capacity). GRACE mode answers "allowed" for everything until the announced enforce date
- `/api/q/1/*` — The **public** read API: the same q/1 methods, gated by a scoped API key instead of by being local. `GET /api/q/1` says what a key can read and `GET /api/q/1/llms.txt` describes the whole surface for a coding agent. `docs/BUILD_YOUR_OWN_UI.md`
- `/api/local/*` — The DuckDB read API, proxied to the daemon. The method set is declared in `clawmetry/query_contract.py`; `make lint-daemon-allowlist` fails when a route calls one the daemon does not serve
- `/v1/metrics`, `/v1/traces`, `/v1/logs` — OTLP receiver (binds `127.0.0.1` by default)

Expand Down
Loading
Loading