robustrep 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- robustrep-0.1.2/CHANGELOG.md +132 -0
- robustrep-0.1.2/MANIFEST.in +9 -0
- robustrep-0.1.2/PKG-INFO +90 -0
- robustrep-0.1.2/README.md +51 -0
- robustrep-0.1.2/SECURITY.md +80 -0
- robustrep-0.1.2/constraints.txt +118 -0
- robustrep-0.1.2/pyproject.toml +69 -0
- robustrep-0.1.2/robustrep/__init__.py +12 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/aggregate.py +11 -9
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/cli.py +240 -142
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/config.py +37 -1
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/evidence.py +29 -2
- robustrep-0.1.2/robustrep/normalize.py +218 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/pipeline.py +50 -5
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/adversarial.py +42 -7
- robustrep-0.1.2/robustrep/report/export.py +142 -0
- robustrep-0.1.2/robustrep/report/provenance_keys.py +34 -0
- robustrep-0.1.2/robustrep/report/publish.py +319 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/render.py +193 -24
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/sensitivity.py +28 -13
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/sources/base_erc8004.py +254 -29
- robustrep-0.1.2/robustrep/sources/blockscout_util.py +106 -0
- robustrep-0.1.2/robustrep/sources/evidence_batch.py +380 -0
- robustrep-0.1.2/robustrep/sources/evidence_fetch.py +517 -0
- robustrep-0.1.2/robustrep/sources/http_util.py +206 -0
- robustrep-0.1.2/robustrep/sources/rater_profile.py +758 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/sources/rpc.py +46 -4
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/store.py +109 -7
- robustrep-0.1.2/robustrep/sybil.py +525 -0
- robustrep-0.1.2/robustrep.egg-info/PKG-INFO +90 -0
- robustrep-0.1.2/robustrep.egg-info/SOURCES.txt +66 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/requires.txt +2 -1
- robustrep-0.1.2/tests/adversarial/test_attacks.py +310 -0
- robustrep-0.1.2/tests/conftest.py +94 -0
- robustrep-0.1.2/tests/e2e/fixtures/base_logs_2000.json +1 -0
- robustrep-0.1.2/tests/e2e/test_live.py +183 -0
- robustrep-0.1.2/tests/e2e/test_replay.py +119 -0
- robustrep-0.1.2/tests/unit/test_aggregate.py +306 -0
- robustrep-0.1.2/tests/unit/test_base_erc8004.py +1036 -0
- robustrep-0.1.2/tests/unit/test_blockscout_util.py +104 -0
- robustrep-0.1.2/tests/unit/test_cli.py +1627 -0
- robustrep-0.1.2/tests/unit/test_conn_guard.py +43 -0
- robustrep-0.1.2/tests/unit/test_evidence.py +218 -0
- robustrep-0.1.2/tests/unit/test_evidence_batch.py +519 -0
- robustrep-0.1.2/tests/unit/test_evidence_fetch.py +728 -0
- robustrep-0.1.2/tests/unit/test_explain.py +131 -0
- robustrep-0.1.2/tests/unit/test_http_util.py +248 -0
- robustrep-0.1.2/tests/unit/test_live_guard.py +223 -0
- robustrep-0.1.2/tests/unit/test_normalize.py +404 -0
- robustrep-0.1.2/tests/unit/test_pipeline.py +308 -0
- robustrep-0.1.2/tests/unit/test_rater_profile.py +1463 -0
- robustrep-0.1.2/tests/unit/test_report.py +770 -0
- robustrep-0.1.2/tests/unit/test_report_publish.py +361 -0
- robustrep-0.1.2/tests/unit/test_rpc.py +459 -0
- robustrep-0.1.2/tests/unit/test_schema.py +201 -0
- robustrep-0.1.2/tests/unit/test_store.py +432 -0
- robustrep-0.1.2/tests/unit/test_sybil.py +557 -0
- robustrep-0.1.2/tests/unit/test_version.py +15 -0
- robustrep-0.1.0/PKG-INFO +0 -34
- robustrep-0.1.0/README.md +0 -9
- robustrep-0.1.0/pyproject.toml +0 -45
- robustrep-0.1.0/robustrep/__init__.py +0 -6
- robustrep-0.1.0/robustrep/normalize.py +0 -73
- robustrep-0.1.0/robustrep/report/export.py +0 -52
- robustrep-0.1.0/robustrep/sources/evidence_fetch.py +0 -395
- robustrep-0.1.0/robustrep/sources/rater_profile.py +0 -272
- robustrep-0.1.0/robustrep/sybil.py +0 -260
- robustrep-0.1.0/robustrep.egg-info/PKG-INFO +0 -34
- robustrep-0.1.0/robustrep.egg-info/SOURCES.txt +0 -31
- {robustrep-0.1.0 → robustrep-0.1.2}/LICENSE +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/explain.py +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/__init__.py +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/figures.py +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/schema.py +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/sources/__init__.py +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/dependency_links.txt +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/entry_points.txt +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/top_level.txt +0 -0
- {robustrep-0.1.0 → robustrep-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to robustrep are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project is pre-1.0,
|
|
5
|
+
so a `0.x` bump may change published numbers without changing the API.
|
|
6
|
+
|
|
7
|
+
## [0.1.2] — 2026-10-03
|
|
8
|
+
|
|
9
|
+
A dependency security release. No API, CLI or scoring changes.
|
|
10
|
+
|
|
11
|
+
### Security — Fixed
|
|
12
|
+
|
|
13
|
+
- **urllib3 floor raised to 2.8.** urllib3 2.6–2.7 can enter an infinite loop
|
|
14
|
+
on a chunked deflate stream (PYSEC-2026-4176) and buffer an unbounded
|
|
15
|
+
chunk-size line into memory (PYSEC-2026-4177). Both are reachable from
|
|
16
|
+
`fetch`, which reads responses from attacker-chosen evidence hosts, so a
|
|
17
|
+
hostile URI could hang or exhaust the fetcher. 2.8 also fixes HTTPS-proxy TLS
|
|
18
|
+
settings being ignored (PYSEC-2026-4175). The CI pin moves to 2.8.0 as well.
|
|
19
|
+
|
|
20
|
+
## [0.1.1] — 2026-09-15
|
|
21
|
+
|
|
22
|
+
A security release: it closes every finding of an external security review of
|
|
23
|
+
0.1.0. There are no API changes — the library entry points and CLI commands are
|
|
24
|
+
the same, and existing calls keep working — but **scores will differ from 0.1.0
|
|
25
|
+
for any (tag, scale) group that held a poisoning value**, because normalization
|
|
26
|
+
no longer rescales a group by its own extremes.
|
|
27
|
+
|
|
28
|
+
### Security — Fixed
|
|
29
|
+
|
|
30
|
+
- **SSRF guard bypass via a URL-parser differential.** The guard vetted what
|
|
31
|
+
`urlsplit` saw, but two transforms ran between it and the socket. `urllib3`'s
|
|
32
|
+
parser treats a backslash as an authority terminator, so
|
|
33
|
+
`http://127.0.0.1:6379\@example.com/` vetted as `example.com` and connected to
|
|
34
|
+
`127.0.0.1:6379`; and `requests`' `requote_uri()` percent-*decodes* unreserved
|
|
35
|
+
characters in the authority after the guard has run, so
|
|
36
|
+
`http://169.254.169.25%34/` vetted as an unresolvable-looking host and
|
|
37
|
+
connected to `169.254.169.254`. The request URL is now rebuilt from the vetted
|
|
38
|
+
components rather than the attacker's string, hostnames must be LDH labels
|
|
39
|
+
(nothing requoting would rewrite), and `urlsplit` and `urllib3` must agree on
|
|
40
|
+
scheme/host/port before anything is fetched.
|
|
41
|
+
**Operators who ran `fetch` with 0.1.0 on a host with reachability into an
|
|
42
|
+
internal network should treat it as a possible internal-GET / IMDS-touch
|
|
43
|
+
event.**
|
|
44
|
+
- **Normalization no longer min-max rescales by group extremes.** `value` is an
|
|
45
|
+
attacker-chosen int128 on ERC-8004, and rule selection used to key off a
|
|
46
|
+
group's own min and max: one record of `2**127-1` pushed an honest `percent`
|
|
47
|
+
group into a min-max fallback and squashed every honest score in it to ~0, and
|
|
48
|
+
two records (+max and −min) flattened the group to exactly 0.5 — a breakdown
|
|
49
|
+
point of 1/n in the normalizer, ahead of the weighted median's ~0.5. Rule
|
|
50
|
+
selection now tolerates a bounded *count* of out-of-range records, clips those
|
|
51
|
+
records into the range they are scored against, and falls back to a percentile
|
|
52
|
+
rank; every rung but `rank` maps a record from its own value alone.
|
|
53
|
+
- **Sybil clustering degrades under pair budgets instead of aborting.**
|
|
54
|
+
`sybil_max_group`, `sybil_max_pairs` and `sybil_max_pairs_per_ratee` bound the
|
|
55
|
+
candidate pairs a run examines. A run that hits a budget finishes with an
|
|
56
|
+
under-merged clustering rather than failing, and says so: what was skipped is
|
|
57
|
+
disclosed in the report's Limitations and in `cluster_stats` in `scores.json`,
|
|
58
|
+
including which way the error points (under-merged, never over-merged).
|
|
59
|
+
- **Evidence tx-hash lookups are capped per URI and per run.** A single 200 KB
|
|
60
|
+
evidence document can pack ~2,985 distinct 64-hex strings, each of which used
|
|
61
|
+
to cost an `eth_getTransactionByHash` call. At most 8 distinct hashes per URI
|
|
62
|
+
and 20,000 per run are now looked up. A URI cut short by the run budget is
|
|
63
|
+
cached with a `lookup-budget:N` note and retried by later `fetch` runs up to 3
|
|
64
|
+
attempts, and the number of still-starved URIs is published in provenance.
|
|
65
|
+
- **Etherscan key redacted from urllib3 DEBUG logs.** urllib3 logs every request
|
|
66
|
+
line — path and query string included — at DEBUG, which `robustrep --verbose`
|
|
67
|
+
turns on, so a key passed as a query parameter could reach any log sink. A
|
|
68
|
+
logging filter now substitutes the key value in records passing urllib3's
|
|
69
|
+
loggers. **Rotate `ETHERSCAN_API_KEY` if DEBUG logs were ever shared or
|
|
70
|
+
stored.**
|
|
71
|
+
- **Further input bounds.** Response bodies are size-capped as they are read;
|
|
72
|
+
RPC-supplied block timestamps and log topics are validated, and an undecodable
|
|
73
|
+
log is skipped instead of aborting a sync; rater-profile pagination parameters
|
|
74
|
+
echoed back by the API are allowlisted rather than followed as given;
|
|
75
|
+
multi-tag bootstrap draws are chunked so peak memory stays bounded; and
|
|
76
|
+
`urllib3 >= 2.6` is now required — the decoded-read cap that bounds a
|
|
77
|
+
decompression bomb first exists in 2.6.0, and 2.0–2.5 decompress the whole
|
|
78
|
+
body before truncating.
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- `--sybil-max-group`, `--sybil-max-pairs` and `--sybil-max-pairs-per-ratee` on
|
|
83
|
+
`score` and `report`.
|
|
84
|
+
- `--min-clusters` on `report`.
|
|
85
|
+
- `fetch --reprofile-raters`, to re-profile raters already cached.
|
|
86
|
+
- `SECURITY.md` (threat model, supported versions, private reporting) and
|
|
87
|
+
`constraints.txt` (the fully pinned environment CI installs and audits).
|
|
88
|
+
- Provenance now carries `norm_fit_share`, `norm_rule_counts`, `cluster_stats`,
|
|
89
|
+
the evidence lookup caps the *fetch* run actually used,
|
|
90
|
+
`evidence_lookup_starved_uris`, and the full `versions` map — the same mapping
|
|
91
|
+
in `report.md` and `scores.json`, so the two artifacts cannot disagree about
|
|
92
|
+
the stack that produced them.
|
|
93
|
+
- Python 3.13 in the CI matrix (now 3.10–3.13).
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- `reports/latest/` publishes an allowlist of files (`report.md`,
|
|
98
|
+
`scores.json`, `fig*.png`), never whatever the last run left in the block
|
|
99
|
+
directory; it is served by GitHub Pages.
|
|
100
|
+
- `export_json` refuses to write a payload containing anything shaped like an
|
|
101
|
+
EVM address — a backstop against de-anonymising a rater through a future
|
|
102
|
+
column, not a sanitizer.
|
|
103
|
+
- IPFS gateways are `ipfs.io` and `dweb.link` (`cloudflare-ipfs.com` was
|
|
104
|
+
retired).
|
|
105
|
+
- `__version__` is derived from the installed distribution metadata instead of a
|
|
106
|
+
literal that could go stale after a version bump.
|
|
107
|
+
|
|
108
|
+
### Known limitations
|
|
109
|
+
|
|
110
|
+
- **DNS rebinding is unmitigated.** The guard resolves an evidence URI's host
|
|
111
|
+
and checks those addresses, but `requests`/`urllib3` resolve it again to make
|
|
112
|
+
the connection. The persisted `(level, note)` pair makes a bypass a narrow
|
|
113
|
+
oracle rather than a blind request, which is why it is tracked rather than
|
|
114
|
+
shrugged off; a pinned-IP transport adapter is planned for v0.2. Until then,
|
|
115
|
+
run `fetch` on a host with no privileged reach into an internal network.
|
|
116
|
+
- **Normalization precedes sybil collapse.** Rule selection runs per (tag,
|
|
117
|
+
scale) group over raw records, before clustering collapses a farm into one
|
|
118
|
+
vote, so a perfectly collapsed farm can still have forced a rung change that
|
|
119
|
+
re-levels every honest record in its group. `norm_rule_counts` in provenance
|
|
120
|
+
is the signal — diff it run over run.
|
|
121
|
+
- **`rank`-rung scores are group-relative.** When a group falls back to
|
|
122
|
+
percentile rank, a 0.8 there means a position within that group, not the same
|
|
123
|
+
thing as a 0.8 in another group.
|
|
124
|
+
|
|
125
|
+
## [0.1.0] — 2026-09-14
|
|
126
|
+
|
|
127
|
+
Initial release on PyPI: the scoring library, the Base ERC-8004 adapter, and the
|
|
128
|
+
reproducible report.
|
|
129
|
+
|
|
130
|
+
[0.1.2]: https://github.com/iamdongwang/robustrep/compare/v0.1.1...v0.1.2
|
|
131
|
+
[0.1.1]: https://github.com/iamdongwang/robustrep/compare/v0.1.0...v0.1.1
|
|
132
|
+
[0.1.0]: https://github.com/iamdongwang/robustrep/releases/tag/v0.1.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
graft tests
|
|
2
|
+
include constraints.txt
|
|
3
|
+
include SECURITY.md
|
|
4
|
+
include CHANGELOG.md
|
|
5
|
+
global-exclude __pycache__ *.pyc .DS_Store
|
|
6
|
+
# Local run artifacts that can sit in a dirty working tree at build time --
|
|
7
|
+
# a scratch sqlite db next to the e2e fixtures, a log, a .env. None of it
|
|
8
|
+
# belongs in a published sdist.
|
|
9
|
+
global-exclude *.db *.db-wal *.db-shm *.log *.sqlite .env
|
robustrep-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: robustrep
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Robust, transport-agnostic reputation scoring for AI agents
|
|
5
|
+
Author: Dong Wang
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/iamdongwang/robustrep
|
|
8
|
+
Project-URL: Repository, https://github.com/iamdongwang/robustrep
|
|
9
|
+
Project-URL: Issues, https://github.com/iamdongwang/robustrep/issues
|
|
10
|
+
Project-URL: Report, https://iamdongwang.github.io/robustrep/reports/latest/report.md
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: pandas<4,>=2.0
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: eth-abi>=5.0
|
|
26
|
+
Requires-Dist: eth-hash[pycryptodome]>=0.5
|
|
27
|
+
Requires-Dist: requests>=2.31
|
|
28
|
+
Requires-Dist: urllib3>=2.8
|
|
29
|
+
Requires-Dist: tabulate>=0.9
|
|
30
|
+
Requires-Dist: matplotlib>=3.7
|
|
31
|
+
Requires-Dist: typer>=0.12
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
35
|
+
Requires-Dist: build; extra == "dev"
|
|
36
|
+
Requires-Dist: twine; extra == "dev"
|
|
37
|
+
Requires-Dist: pip-audit; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# robustrep
|
|
41
|
+
|
|
42
|
+
Robust, transport-agnostic reputation scoring for AI agents.
|
|
43
|
+
|
|
44
|
+
Feed it any table of `(rater, ratee, value, scale, tag, ts, evidence_uri, source)` and it returns a
|
|
45
|
+
robust score per ratee that resists Sybil raters and evidence-free ratings. Ships with a Base-chain
|
|
46
|
+
ERC-8004 adapter and a reproducible report.
|
|
47
|
+
|
|
48
|
+
Status: v0.1.1 released on PyPI. Design spec and plan live in the parent workspace; a summary is in `reports/latest/report.md`.
|
|
49
|
+
Release notes, including the security fixes in 0.1.1: [CHANGELOG.md](CHANGELOG.md).
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
pip install robustrep
|
|
54
|
+
|
|
55
|
+
## Library
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import pandas as pd
|
|
59
|
+
from robustrep import Config, score
|
|
60
|
+
|
|
61
|
+
records = pd.DataFrame([...]) # columns: rater, ratee, value, scale, tag, ts, evidence_uri, source
|
|
62
|
+
result = score(records, Config())
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Base ERC-8004 report
|
|
66
|
+
|
|
67
|
+
robustrep fetch --db data/base.db # pull events (resumable)
|
|
68
|
+
robustrep score --db data/base.db --out scores.csv
|
|
69
|
+
robustrep report --db data/base.db --out-dir reports
|
|
70
|
+
|
|
71
|
+
Rater profiles: Blockscout (free, no key) by default; Etherscan V2 requires a paid plan for Base.
|
|
72
|
+
Pick a source explicitly with `fetch --profile-source {auto,blockscout,etherscan,none}`.
|
|
73
|
+
|
|
74
|
+
Latest scores: `reports/latest/scores.json`, also served at
|
|
75
|
+
https://iamdongwang.github.io/robustrep/reports/latest/scores.json
|
|
76
|
+
|
|
77
|
+
## Security considerations
|
|
78
|
+
|
|
79
|
+
Everything robustrep reads is written by untrusted parties: on-chain values, tags, evidence URIs and
|
|
80
|
+
addresses, plus whatever the RPC/Blockscout endpoints and evidence hosts return.
|
|
81
|
+
|
|
82
|
+
`robustrep fetch` makes outbound HTTP from *your* machine to evidence URIs chosen by the rated party.
|
|
83
|
+
Those go through an SSRF guard (scheme/host allowlist, parser cross-check, canonical rebuild, refusal
|
|
84
|
+
of private/reserved addresses, manual redirects, a size cap and per-socket-operation timeouts — not a
|
|
85
|
+
wall-clock bound). Its one known SSRF bypass, DNS rebinding, is unmitigated in v0.1 — so run `fetch`
|
|
86
|
+
on a host with no privileged reach into an internal network. API keys should come from the
|
|
87
|
+
environment (`ETHERSCAN_API_KEY`), never the source tree; `--etherscan-key` is an escape hatch that
|
|
88
|
+
takes precedence over the environment and is visible to anyone who can run `ps`.
|
|
89
|
+
|
|
90
|
+
Full threat model and private reporting instructions: [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# robustrep
|
|
2
|
+
|
|
3
|
+
Robust, transport-agnostic reputation scoring for AI agents.
|
|
4
|
+
|
|
5
|
+
Feed it any table of `(rater, ratee, value, scale, tag, ts, evidence_uri, source)` and it returns a
|
|
6
|
+
robust score per ratee that resists Sybil raters and evidence-free ratings. Ships with a Base-chain
|
|
7
|
+
ERC-8004 adapter and a reproducible report.
|
|
8
|
+
|
|
9
|
+
Status: v0.1.1 released on PyPI. Design spec and plan live in the parent workspace; a summary is in `reports/latest/report.md`.
|
|
10
|
+
Release notes, including the security fixes in 0.1.1: [CHANGELOG.md](CHANGELOG.md).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
pip install robustrep
|
|
15
|
+
|
|
16
|
+
## Library
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import pandas as pd
|
|
20
|
+
from robustrep import Config, score
|
|
21
|
+
|
|
22
|
+
records = pd.DataFrame([...]) # columns: rater, ratee, value, scale, tag, ts, evidence_uri, source
|
|
23
|
+
result = score(records, Config())
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Base ERC-8004 report
|
|
27
|
+
|
|
28
|
+
robustrep fetch --db data/base.db # pull events (resumable)
|
|
29
|
+
robustrep score --db data/base.db --out scores.csv
|
|
30
|
+
robustrep report --db data/base.db --out-dir reports
|
|
31
|
+
|
|
32
|
+
Rater profiles: Blockscout (free, no key) by default; Etherscan V2 requires a paid plan for Base.
|
|
33
|
+
Pick a source explicitly with `fetch --profile-source {auto,blockscout,etherscan,none}`.
|
|
34
|
+
|
|
35
|
+
Latest scores: `reports/latest/scores.json`, also served at
|
|
36
|
+
https://iamdongwang.github.io/robustrep/reports/latest/scores.json
|
|
37
|
+
|
|
38
|
+
## Security considerations
|
|
39
|
+
|
|
40
|
+
Everything robustrep reads is written by untrusted parties: on-chain values, tags, evidence URIs and
|
|
41
|
+
addresses, plus whatever the RPC/Blockscout endpoints and evidence hosts return.
|
|
42
|
+
|
|
43
|
+
`robustrep fetch` makes outbound HTTP from *your* machine to evidence URIs chosen by the rated party.
|
|
44
|
+
Those go through an SSRF guard (scheme/host allowlist, parser cross-check, canonical rebuild, refusal
|
|
45
|
+
of private/reserved addresses, manual redirects, a size cap and per-socket-operation timeouts — not a
|
|
46
|
+
wall-clock bound). Its one known SSRF bypass, DNS rebinding, is unmitigated in v0.1 — so run `fetch`
|
|
47
|
+
on a host with no privileged reach into an internal network. API keys should come from the
|
|
48
|
+
environment (`ETHERSCAN_API_KEY`), never the source tree; `--etherscan-key` is an escape hatch that
|
|
49
|
+
takes precedence over the environment and is visible to anyone who can run `ps`.
|
|
50
|
+
|
|
51
|
+
Full threat model and private reporting instructions: [SECURITY.md](SECURITY.md).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
robustrep is pre-1.0. Only the latest published `0.x` release receives security
|
|
6
|
+
fixes; older `0.x` releases are not patched.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| -------------- | --------- |
|
|
10
|
+
| latest `0.x` | yes |
|
|
11
|
+
| any earlier | no |
|
|
12
|
+
|
|
13
|
+
## Reporting a vulnerability
|
|
14
|
+
|
|
15
|
+
Please report privately, not in a public issue. Use GitHub's **Report a
|
|
16
|
+
vulnerability** button on the repository's
|
|
17
|
+
[Security tab](https://github.com/iamdongwang/robustrep/security/advisories)
|
|
18
|
+
(private vulnerability reporting is enabled). Include what you did, what
|
|
19
|
+
happened, and what you expected; a reproducer helps.
|
|
20
|
+
|
|
21
|
+
Response target: an acknowledgement within **7 days**, with an assessment and
|
|
22
|
+
a plan (fix, mitigation, or a reasoned "not a vulnerability") in the same
|
|
23
|
+
thread. Please give us a chance to ship a fix before disclosing publicly.
|
|
24
|
+
|
|
25
|
+
## Threat model
|
|
26
|
+
|
|
27
|
+
robustrep reads data written by arbitrary, unauthenticated third parties and
|
|
28
|
+
must never be trusted to be reading anything benign.
|
|
29
|
+
|
|
30
|
+
**Untrusted inputs.** On-chain feedback is attacker-controlled in every field:
|
|
31
|
+
values, tags, evidence URIs and addresses. So are the responses of the RPC
|
|
32
|
+
endpoints and the Blockscout API the adapters talk to, and so is every byte
|
|
33
|
+
served by an evidence host. Nothing from these sources is treated as
|
|
34
|
+
authoritative; all of it is validated, bounded and normalized before use.
|
|
35
|
+
|
|
36
|
+
**Outbound fetches are the sharp edge.** `robustrep fetch` performs outbound
|
|
37
|
+
HTTP from the operator's own machine to evidence URIs chosen by the party being
|
|
38
|
+
rated. Those requests go through an SSRF guard
|
|
39
|
+
(`robustrep/sources/evidence_fetch.py`): scheme/host allowlist (http/https on
|
|
40
|
+
ports 80/443, LDH hostnames only, no bare IP literals, no non-ASCII hosts),
|
|
41
|
+
a cross-check that `urllib3` and `urlsplit` agree on scheme/host/port, a
|
|
42
|
+
request URL rebuilt from the vetted components rather than the attacker's raw
|
|
43
|
+
string, refusal of any address that resolves to a private/loopback/link-local/
|
|
44
|
+
reserved range, manually followed redirects (re-vetted at every hop, capped in
|
|
45
|
+
count), a hard cap on response size, and per-socket-operation (connect and
|
|
46
|
+
read) timeouts -- which bound each socket operation, not the request as a
|
|
47
|
+
whole: there is no wall-clock cap on a slow-drip response.
|
|
48
|
+
|
|
49
|
+
**Residual limitation: DNS rebinding.** The guard resolves the hostname and
|
|
50
|
+
checks those addresses, but `requests`/`urllib3` resolves it again to make the
|
|
51
|
+
connection. An attacker who can answer differently between the two lookups can
|
|
52
|
+
still cause a GET to a private address. The response body is never persisted
|
|
53
|
+
or reported -- but what *is* persisted, the `(level, note)` pair recorded per
|
|
54
|
+
evidence URI, makes the bypass at least a four-state oracle about the target:
|
|
55
|
+
no response at all, a response with no markers in it, a response containing a
|
|
56
|
+
transaction hash or a task-id key, or a hash involving the rater/owner
|
|
57
|
+
addresses -- plus a state per further note the cache can hold (`fetch-error`,
|
|
58
|
+
`lookup-budget:N`). A narrow leak rather than a blind request, which is why the
|
|
59
|
+
gap is tracked instead of shrugged off. It is documented in full in
|
|
60
|
+
`robustrep/sources/evidence_fetch.py` and pinned by a strict-xfail test; a
|
|
61
|
+
pinned-IP transport adapter is scheduled for v0.2. Until then: **run `fetch`
|
|
62
|
+
on a host that has no privileged reachability into an internal network** (a
|
|
63
|
+
laptop on a plain internet connection, or a throwaway cloud box), not on a
|
|
64
|
+
machine inside a private network or with access to a cloud metadata service.
|
|
65
|
+
|
|
66
|
+
**Secrets.** API keys (e.g. an Etherscan key) should come from the environment
|
|
67
|
+
(`ETHERSCAN_API_KEY`). `--etherscan-key` exists as an escape hatch and takes
|
|
68
|
+
precedence over the environment, but a command line is world-readable via `ps`
|
|
69
|
+
on most systems and lands in shell history -- prefer the environment variable
|
|
70
|
+
and treat the flag as a last resort. Never put a key in a config file in the
|
|
71
|
+
repository, and never commit a `.env` file (`.gitignore` refuses them).
|
|
72
|
+
|
|
73
|
+
Do not embed a key in an RPC URL either. `RpcError` messages are redacted to
|
|
74
|
+
`scheme://host` precisely so a key in a URL cannot leak into CLI output or a
|
|
75
|
+
bug report -- but `--verbose` turns on DEBUG logging, and the DEBUG line for a
|
|
76
|
+
failed RPC attempt carries the full transport exception (`exc_info`), whose
|
|
77
|
+
text can include the request URL the exception was raised against. The
|
|
78
|
+
urllib3 log filter only scrubs an exact Etherscan key out of string log
|
|
79
|
+
records, not out of an exception's own repr. So: keep keys out of RPC URLs,
|
|
80
|
+
and do not hand `--verbose` logs to anyone you would not hand the keys to.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Fully-pinned CI environment for robustrep.
|
|
2
|
+
#
|
|
3
|
+
# Generated from a known-good local dev venv:
|
|
4
|
+
# .venv/bin/pip freeze | grep -v '^-e ' > constraints.txt
|
|
5
|
+
# (the editable `-e ...robustrep` line is dropped; everything else is pinned `==`).
|
|
6
|
+
#
|
|
7
|
+
# Used ONLY as a pip constraints file in CI:
|
|
8
|
+
# pip install -e ".[dev]" -c constraints.txt
|
|
9
|
+
# so every workflow run resolves the same dependency set and `pip-audit` sees
|
|
10
|
+
# exactly what the tests ran against. It is deliberately NOT the library's
|
|
11
|
+
# dependency metadata: `pyproject.toml` keeps open lower bounds so installing
|
|
12
|
+
# `robustrep` never forces these exact versions on a downstream user.
|
|
13
|
+
#
|
|
14
|
+
# Two things `pip freeze` does not emit are appended by hand:
|
|
15
|
+
# * `pip` itself (freeze omits it), pinned so the runner image's own pip does
|
|
16
|
+
# not drift -- CI installs it explicitly, a constraint alone won't upgrade pip;
|
|
17
|
+
# * dependencies that only install on Linux (keyring's secret-storage backend
|
|
18
|
+
# and its crypto chain), which a macOS freeze can never see. They are
|
|
19
|
+
# marker-gated, so they stay uninstalled here and pinned in CI.
|
|
20
|
+
#
|
|
21
|
+
# Refresh by re-running the command above after upgrading the dev venv, keeping
|
|
22
|
+
# the hand-added block below.
|
|
23
|
+
annotated-doc==0.0.5
|
|
24
|
+
annotated-types==0.8.0
|
|
25
|
+
backports.tarfile==1.2.0
|
|
26
|
+
boolean.py==5.0
|
|
27
|
+
build==1.6.1
|
|
28
|
+
CacheControl==0.14.4
|
|
29
|
+
certifi==2026.7.22
|
|
30
|
+
charset-normalizer==3.5.1
|
|
31
|
+
contourpy==1.3.2
|
|
32
|
+
coverage==7.16.1
|
|
33
|
+
cycler==0.12.1
|
|
34
|
+
cyclonedx-python-lib==11.12.0
|
|
35
|
+
cytoolz==1.1.0
|
|
36
|
+
defusedxml==0.7.1
|
|
37
|
+
docutils==0.23
|
|
38
|
+
eth-hash==0.8.0
|
|
39
|
+
eth-typing==6.0.0
|
|
40
|
+
eth-utils==6.0.0
|
|
41
|
+
eth_abi==6.0.0
|
|
42
|
+
exceptiongroup==1.3.1
|
|
43
|
+
filelock==3.32.6
|
|
44
|
+
fonttools==4.65.0
|
|
45
|
+
id==1.6.1
|
|
46
|
+
idna==3.19
|
|
47
|
+
importlib_metadata==9.0.1
|
|
48
|
+
iniconfig==2.3.0
|
|
49
|
+
jaraco.classes==3.4.0
|
|
50
|
+
jaraco.context==6.1.2
|
|
51
|
+
jaraco.functools==4.6.0
|
|
52
|
+
keyring==25.7.0
|
|
53
|
+
kiwisolver==1.5.1
|
|
54
|
+
license-expression==30.4.4
|
|
55
|
+
markdown-it-py==4.2.0
|
|
56
|
+
matplotlib==3.10.9
|
|
57
|
+
mdurl==0.1.2
|
|
58
|
+
more-itertools==11.1.0
|
|
59
|
+
msgpack==1.2.2
|
|
60
|
+
nh3==0.3.7
|
|
61
|
+
numpy==2.2.6
|
|
62
|
+
packageurl-python==0.17.6
|
|
63
|
+
packaging==26.3
|
|
64
|
+
pandas==2.3.3
|
|
65
|
+
parsimonious==0.10.0
|
|
66
|
+
pillow==12.3.0
|
|
67
|
+
pip-requirements-parser==32.0.1
|
|
68
|
+
pip_api==0.0.35
|
|
69
|
+
pip_audit==2.10.1
|
|
70
|
+
platformdirs==4.11.8
|
|
71
|
+
pluggy==1.6.0
|
|
72
|
+
py-serializable==2.1.0
|
|
73
|
+
pycryptodome==3.23.0
|
|
74
|
+
pydantic==2.13.5
|
|
75
|
+
pydantic_core==2.46.5
|
|
76
|
+
Pygments==2.21.0
|
|
77
|
+
pyparsing==3.3.2
|
|
78
|
+
pyproject_hooks==1.2.0
|
|
79
|
+
pytest==9.1.1
|
|
80
|
+
pytest-cov==7.1.0
|
|
81
|
+
python-dateutil==2.9.0.post0
|
|
82
|
+
pytz==2026.3.post1
|
|
83
|
+
readme_renderer==46.0
|
|
84
|
+
regex==2026.9.10
|
|
85
|
+
requests==2.34.2
|
|
86
|
+
requests-toolbelt==1.0.0
|
|
87
|
+
rfc3986==2.0.0
|
|
88
|
+
rich==15.0.0
|
|
89
|
+
shellingham==1.5.4
|
|
90
|
+
six==1.17.0
|
|
91
|
+
sortedcontainers==2.4.0
|
|
92
|
+
tabulate==0.10.0
|
|
93
|
+
tomli==2.4.1
|
|
94
|
+
tomli_w==1.2.0
|
|
95
|
+
toolz==1.1.0
|
|
96
|
+
twine==7.0.0
|
|
97
|
+
typer==0.27.2
|
|
98
|
+
typing-inspection==0.4.4
|
|
99
|
+
typing_extensions==4.16.0
|
|
100
|
+
tzdata==2026.4
|
|
101
|
+
urllib3==2.8.0
|
|
102
|
+
zipp==4.1.0
|
|
103
|
+
# -- hand-added (not emitted by `pip freeze` on the dev box) -----------------
|
|
104
|
+
# pip itself: freeze omits it. CI installs this version explicitly before the
|
|
105
|
+
# project, since a constraint alone never upgrades the runner image's pip.
|
|
106
|
+
pip==26.2.1
|
|
107
|
+
# Linux-only: twine -> keyring pulls the secret-storage backend and its crypto
|
|
108
|
+
# chain under `sys_platform == "linux"`, so a macOS freeze can never see them
|
|
109
|
+
# while CI installs them on every job. Versions resolved with
|
|
110
|
+
# pip install --dry-run --platform manylinux2014_x86_64 \
|
|
111
|
+
# --only-binary=:all: SecretStorage jeepney
|
|
112
|
+
# (identical for 3.10 and 3.13). Marker-gated, so pinning them here is inert
|
|
113
|
+
# off Linux -- but it keeps them inside pip-audit's view.
|
|
114
|
+
SecretStorage==3.5.0
|
|
115
|
+
jeepney==0.9.0
|
|
116
|
+
cryptography==50.0.1
|
|
117
|
+
cffi==2.1.1
|
|
118
|
+
pycparser==3.0
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "robustrep"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Robust, transport-agnostic reputation scoring for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
authors = [{ name = "Dong Wang" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Security",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pandas>=2.0,<4",
|
|
26
|
+
"numpy>=1.24",
|
|
27
|
+
"eth-abi>=5.0",
|
|
28
|
+
"eth-hash[pycryptodome]>=0.5",
|
|
29
|
+
"requests>=2.31",
|
|
30
|
+
# >=2.6, not >=2: the decoded-read cap `evidence_fetch` relies on to bound a
|
|
31
|
+
# decompression bomb (`max_length` on urllib3's decoders) first exists in
|
|
32
|
+
# 2.6.0. On 2.0-2.5 the same call decompresses the whole body before
|
|
33
|
+
# truncating -- measured at a 455 MB peak against 0.8 MB on 2.7.
|
|
34
|
+
# >=2.8: 2.6-2.7 can loop forever on a chunked deflate stream and buffer an
|
|
35
|
+
# unbounded chunk-size line (PYSEC-2026-4176/4177), both reachable from an
|
|
36
|
+
# attacker-chosen evidence host.
|
|
37
|
+
"urllib3>=2.8",
|
|
38
|
+
"tabulate>=0.9",
|
|
39
|
+
"matplotlib>=3.7",
|
|
40
|
+
"typer>=0.12",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = ["pytest>=8", "pytest-cov>=5", "build", "twine", "pip-audit"]
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/iamdongwang/robustrep"
|
|
48
|
+
Repository = "https://github.com/iamdongwang/robustrep"
|
|
49
|
+
Issues = "https://github.com/iamdongwang/robustrep/issues"
|
|
50
|
+
Report = "https://iamdongwang.github.io/robustrep/reports/latest/report.md"
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
robustrep = "robustrep.cli:app"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
include = ["robustrep*"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
markers = ["live: needs network; run weekly, not on every push"]
|
|
61
|
+
addopts = "-m 'not live'"
|
|
62
|
+
filterwarnings = ["error"]
|
|
63
|
+
|
|
64
|
+
[tool.coverage.run]
|
|
65
|
+
source = ["robustrep"]
|
|
66
|
+
|
|
67
|
+
[tool.coverage.report]
|
|
68
|
+
fail_under = 80
|
|
69
|
+
show_missing = true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""robustrep: robust, transport-agnostic reputation scoring for AI agents."""
|
|
2
|
+
from .config import Config
|
|
3
|
+
from .pipeline import score
|
|
4
|
+
|
|
5
|
+
__all__ = ["Config", "score"]
|
|
6
|
+
from importlib import metadata as _metadata
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
# Single source of truth: pyproject.toml, via the installed distribution.
|
|
10
|
+
__version__ = _metadata.version("robustrep")
|
|
11
|
+
except _metadata.PackageNotFoundError: # running from a bare checkout without an install
|
|
12
|
+
__version__ = "0.0.0+unknown"
|
|
@@ -23,11 +23,13 @@ _TIE_EPS = 1e-9
|
|
|
23
23
|
# weight for the CI to be considered meaningful.
|
|
24
24
|
MIN_VALID_BOOT_FRACTION = 0.1
|
|
25
25
|
|
|
26
|
-
# Cap on the number of (resample x vote)
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
26
|
+
# Cap on the number of (resample x vote) cells materialized at once by a
|
|
27
|
+
# chunked bootstrap draw, so peak memory stays bounded even for a ratee with
|
|
28
|
+
# thousands of votes: a chunk holds at most this many cells regardless of how
|
|
29
|
+
# large `n_boot` is. Public because `robustrep.pipeline._ci` chunks its
|
|
30
|
+
# multi-tag bootstrap against the same budget (see M3 there); one constant so
|
|
31
|
+
# the two paths can never bound memory differently.
|
|
32
|
+
MAX_BOOT_CHUNK_CELLS = 2_000_000
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def _validate(values: np.ndarray, weights: np.ndarray, name: str) -> tuple[np.ndarray, np.ndarray]:
|
|
@@ -169,9 +171,9 @@ def bootstrap_ci(
|
|
|
169
171
|
and the cumulative sum of `counts * sorted_weights` along each row gives
|
|
170
172
|
every resample's weighted median in one vectorized pass (see
|
|
171
173
|
`_bootstrap_draws`). Processed in chunks of at most
|
|
172
|
-
`max(1,
|
|
173
|
-
a ratee with thousands of votes; this is what keeps the
|
|
174
|
-
bootstrap affordable at ~28k ratees.
|
|
174
|
+
`max(1, MAX_BOOT_CHUNK_CELLS // n)` resamples so peak memory stays
|
|
175
|
+
bounded even for a ratee with thousands of votes; this is what keeps the
|
|
176
|
+
per-ratee bootstrap affordable at ~28k ratees.
|
|
175
177
|
|
|
176
178
|
Inputs are validated exactly like `weighted_median` (see `_validate`).
|
|
177
179
|
A resample whose weights sum to zero has an undefined weighted median
|
|
@@ -186,7 +188,7 @@ def bootstrap_ci(
|
|
|
186
188
|
order = np.argsort(values, kind="stable")
|
|
187
189
|
v_sorted, w_sorted = values[order], weights[order]
|
|
188
190
|
|
|
189
|
-
chunk_size = max(1,
|
|
191
|
+
chunk_size = max(1, MAX_BOOT_CHUNK_CELLS // n)
|
|
190
192
|
boots = _bootstrap_draws(v_sorted, w_sorted, n_boot, rng, chunk_size)
|
|
191
193
|
|
|
192
194
|
min_valid = max(2, int(n_boot * MIN_VALID_BOOT_FRACTION))
|