diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 0000000..908a77e --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1,2 @@ +https://github.com/blitzcrieg1/agentmetry/blob/master/funding.json +https://raw.githubusercontent.com/blitzcrieg1/agentmetry/master/funding.json diff --git a/apps/orchestrator/tests/test_funding_manifest.py b/apps/orchestrator/tests/test_funding_manifest.py new file mode 100644 index 0000000..045d7b6 --- /dev/null +++ b/apps/orchestrator/tests/test_funding_manifest.py @@ -0,0 +1,85 @@ +"""`funding.json` quotes numbers about this repository. Numbers rot. + +The manifest is published to the FLOSS/fund directory and read by people +deciding whether to give money, which makes it the worst place in the project +for a stale figure. `CONTRIBUTORS.md` proved the point the week this was +written: it advertised commit counts that its own printed command did not +produce, and nothing failed. + +`test_readme_claims.py` already does this for the README and `test_version.py` +for the version string. This does the same for the manifest, so a figure that +drifts fails here instead of in front of a funder. + +Only the claims that a command can settle are pinned. The download count and +the test count are stamped with a date in the manifest itself and are left +alone, because a number that says when it was true does not become false. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import pytest + +from agentmetry.core.audit.detection.benchmark import load_corpus +from agentmetry.core.version import __version__ + +MANIFEST = Path(__file__).resolve().parents[3] / "funding.json" + + +def _manifest() -> dict: + if not MANIFEST.is_file(): + pytest.skip(f"funding.json not found at {MANIFEST}") + return json.loads(MANIFEST.read_text(encoding="utf-8")) + + +def _project_description() -> str: + projects = _manifest().get("projects") or [] + assert projects, "funding.json lists no projects" + return projects[0]["description"] + + +def test_the_manifest_is_valid_json_with_the_required_shape(): + """Cheap, and it catches the edit that breaks the directory listing.""" + manifest = _manifest() + for key in ("entity", "funding"): + assert key in manifest, f"funding.json is missing the required {key!r} object" + funding = manifest["funding"] + assert funding.get("channels"), "no funding channels declared" + assert funding.get("plans"), "no funding plans declared" + + channels = {c["guid"] for c in funding["channels"]} + for plan in funding["plans"]: + unknown = set(plan["channels"]) - channels + assert not unknown, f"plan {plan['guid']!r} points at undeclared channel(s) {unknown}" + + +def test_quoted_version_matches_the_package(): + quoted = re.search(r"version (\d+\.\d+\.\d+) on PyPI", _project_description()) + assert quoted, "funding.json no longer quotes a version; update this test or the manifest" + assert quoted.group(1) == __version__, ( + "funding.json quotes a different version than the package ships" + ) + + +def test_quoted_benchmark_case_count_matches_the_corpus(): + quoted = re.search(r"(\d+) case detection benchmark", _project_description()) + assert quoted, "funding.json no longer quotes a case count; update this test or the manifest" + assert int(quoted.group(1)) == len(load_corpus()), ( + "funding.json quotes a different case count than the corpus holds" + ) + + +def test_declared_licence_matches_the_package_metadata(): + pyproject = (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text( + encoding="utf-8" + ) + declared = re.search(r'license\s*=\s*\{\s*text\s*=\s*"([^"]+)"', pyproject) + assert declared, "pyproject.toml no longer declares a licence in the expected form" + + licences = _manifest()["projects"][0]["licenses"] + assert licences == [f"spdx:{declared.group(1)}"], ( + "funding.json declares a different licence than the package does" + ) diff --git a/funding.json b/funding.json new file mode 100644 index 0000000..c380811 --- /dev/null +++ b/funding.json @@ -0,0 +1,125 @@ +{ + "version": "v1.1.0", + "entity": { + "type": "individual", + "role": "owner", + "name": "Ioannis Loutsis", + "email": "g.loutsis95@gmail.com", + "description": "Solo maintainer, based in Greece. I build and maintain Agentmetry, a local-first flight recorder for AI coding agents, alongside other work. The project has received no funding to date.\n\nThe thing I care most about here is that every published number comes from a command somebody else can run. The detection ruleset is frozen behind a published hash during evaluation periods, so a claim of four clean weeks cannot quietly mean four different rulesets. The benchmark corpus carries the project's own known false positives as cases that must stay silent. Tests assert that figures quoted in the README still match what the commands print, because they had drifted once already.", + "webpageUrl": { + "url": "https://agentmetry.ai", + "wellKnown": "https://agentmetry.ai/.well-known/funding-manifest-urls" + } + }, + "projects": [ + { + "guid": "agentmetry", + "name": "Agentmetry", + "description": "AI coding agents run shell commands, edit files and call third-party tools hundreds of times an hour, largely unattended. What they actually did is recorded nowhere by default. The agent transcript is a conversation rather than a record of effects, the editor keeps no durable log, and endpoint security sees only that one process spawned another, which is what a coding agent looks like when it is working correctly.\n\nAgentmetry records what an agent did at the tool boundary, correlates sequences into detections, and writes a hash-chained JSONL trail the operator owns, forwarding into whatever SIEM they already run. It is local-first: there is no vendor cloud, and no telemetry reaches the maintainer. Tool arguments are hashed inside the hook process, so plaintext never crosses the wire.\n\nVerifiable as of 2026-09-02: version 0.7.0 on PyPI, Apache-2.0, 1,186 tests, a 54 case detection benchmark at zero misses and zero false positives, MITRE ATT&CK and ATLAS mappings, and four SIEM adapters. Every one of those figures is printed by a command in the README.\n\nFunding would go to making the format underneath it common rather than ours: a standalone agent session record specification, mappings into Elastic Common Schema, OCSF, Splunk CIM and Google SecOps UDM, and a conformance suite so a third party can verify their own mapping without reading our Python. The success condition is other vendors adopting the fields, including ones who compete with us.", + "webpageUrl": { + "url": "https://agentmetry.ai", + "wellKnown": "https://agentmetry.ai/.well-known/funding-manifest-urls" + }, + "repositoryUrl": { + "url": "https://github.com/blitzcrieg1/agentmetry", + "wellKnown": "https://github.com/blitzcrieg1/agentmetry/blob/master/.well-known/funding-manifest-urls" + }, + "licenses": [ + "spdx:Apache-2.0" + ], + "tags": [ + "security", + "privacy", + "cryptography", + "developer-tools", + "devops", + "artificial-intelligence", + "software-engineering", + "data", + "analytics" + ] + } + ], + "funding": { + "channels": [ + { + "guid": "github-sponsors", + "type": "payment-provider", + "address": "https://github.com/sponsors/blitzcrieg1", + "description": "GitHub Sponsors, the only channel currently open." + } + ], + "plans": [ + { + "guid": "supporter", + "status": "active", + "name": "Supporter", + "description": "Any amount, recurring. Goes to maintenance: triaging false positive reports, dependency review and releases.", + "amount": 0, + "currency": "EUR", + "frequency": "monthly", + "channels": [ + "github-sponsors" + ] + }, + { + "guid": "mcp-remote-transports", + "status": "active", + "name": "MCP over HTTP and SSE", + "description": "80 hours at 50 EUR per hour. The recorder proxies local stdio MCP servers today, so remote tool servers are outside what it can see. This closes that.", + "amount": 4000, + "currency": "EUR", + "frequency": "one-time", + "channels": [ + "github-sponsors" + ] + }, + { + "guid": "sensor-runtime", + "status": "active", + "name": "Sensor-grade runtime", + "description": "120 hours at 50 EUR per hour. Watchdog, hook tamper detection, a resource budget and delivery guarantees on the spool. A recorder that can be silently stopped produces a record nobody should rely on.", + "amount": 6000, + "currency": "EUR", + "frequency": "one-time", + "channels": [ + "github-sponsors" + ] + }, + { + "guid": "conformance-suite", + "status": "active", + "name": "Conformance suite and validator", + "description": "110 hours at 50 EUR per hour. A language-independent validator and test suite for the event format, so a third party can check their mapping is correct without reading our Python. A specification without a test suite is a document. With one, it is something a competitor can adopt on a Friday and verify by Monday.", + "amount": 5500, + "currency": "EUR", + "frequency": "one-time", + "channels": [ + "github-sponsors" + ] + }, + { + "guid": "open-format-programme", + "status": "active", + "name": "The full open format programme", + "description": "940 hours at 50 EUR per hour over twelve months, which is roughly eighteen hours a week sustained. The three plans above, plus the session record specification, the four SIEM mappings with an upstream class proposal to OCSF, call-time behaviour drift detection, fingerprint hardening, and growing the benchmark corpus beyond one machine. Full task breakdown with hours available on request.", + "amount": 50000, + "currency": "EUR", + "frequency": "one-time", + "channels": [ + "github-sponsors" + ] + } + ], + "history": [ + { + "year": 2026, + "income": 0, + "expenses": 0, + "taxes": 0, + "currency": "EUR", + "description": "No funding received and no revenue. Everything released so far was built unfunded alongside other work." + } + ] + } +}