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.
Files changed (79) hide show
  1. robustrep-0.1.2/CHANGELOG.md +132 -0
  2. robustrep-0.1.2/MANIFEST.in +9 -0
  3. robustrep-0.1.2/PKG-INFO +90 -0
  4. robustrep-0.1.2/README.md +51 -0
  5. robustrep-0.1.2/SECURITY.md +80 -0
  6. robustrep-0.1.2/constraints.txt +118 -0
  7. robustrep-0.1.2/pyproject.toml +69 -0
  8. robustrep-0.1.2/robustrep/__init__.py +12 -0
  9. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/aggregate.py +11 -9
  10. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/cli.py +240 -142
  11. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/config.py +37 -1
  12. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/evidence.py +29 -2
  13. robustrep-0.1.2/robustrep/normalize.py +218 -0
  14. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/pipeline.py +50 -5
  15. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/adversarial.py +42 -7
  16. robustrep-0.1.2/robustrep/report/export.py +142 -0
  17. robustrep-0.1.2/robustrep/report/provenance_keys.py +34 -0
  18. robustrep-0.1.2/robustrep/report/publish.py +319 -0
  19. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/render.py +193 -24
  20. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/sensitivity.py +28 -13
  21. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/sources/base_erc8004.py +254 -29
  22. robustrep-0.1.2/robustrep/sources/blockscout_util.py +106 -0
  23. robustrep-0.1.2/robustrep/sources/evidence_batch.py +380 -0
  24. robustrep-0.1.2/robustrep/sources/evidence_fetch.py +517 -0
  25. robustrep-0.1.2/robustrep/sources/http_util.py +206 -0
  26. robustrep-0.1.2/robustrep/sources/rater_profile.py +758 -0
  27. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/sources/rpc.py +46 -4
  28. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/store.py +109 -7
  29. robustrep-0.1.2/robustrep/sybil.py +525 -0
  30. robustrep-0.1.2/robustrep.egg-info/PKG-INFO +90 -0
  31. robustrep-0.1.2/robustrep.egg-info/SOURCES.txt +66 -0
  32. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/requires.txt +2 -1
  33. robustrep-0.1.2/tests/adversarial/test_attacks.py +310 -0
  34. robustrep-0.1.2/tests/conftest.py +94 -0
  35. robustrep-0.1.2/tests/e2e/fixtures/base_logs_2000.json +1 -0
  36. robustrep-0.1.2/tests/e2e/test_live.py +183 -0
  37. robustrep-0.1.2/tests/e2e/test_replay.py +119 -0
  38. robustrep-0.1.2/tests/unit/test_aggregate.py +306 -0
  39. robustrep-0.1.2/tests/unit/test_base_erc8004.py +1036 -0
  40. robustrep-0.1.2/tests/unit/test_blockscout_util.py +104 -0
  41. robustrep-0.1.2/tests/unit/test_cli.py +1627 -0
  42. robustrep-0.1.2/tests/unit/test_conn_guard.py +43 -0
  43. robustrep-0.1.2/tests/unit/test_evidence.py +218 -0
  44. robustrep-0.1.2/tests/unit/test_evidence_batch.py +519 -0
  45. robustrep-0.1.2/tests/unit/test_evidence_fetch.py +728 -0
  46. robustrep-0.1.2/tests/unit/test_explain.py +131 -0
  47. robustrep-0.1.2/tests/unit/test_http_util.py +248 -0
  48. robustrep-0.1.2/tests/unit/test_live_guard.py +223 -0
  49. robustrep-0.1.2/tests/unit/test_normalize.py +404 -0
  50. robustrep-0.1.2/tests/unit/test_pipeline.py +308 -0
  51. robustrep-0.1.2/tests/unit/test_rater_profile.py +1463 -0
  52. robustrep-0.1.2/tests/unit/test_report.py +770 -0
  53. robustrep-0.1.2/tests/unit/test_report_publish.py +361 -0
  54. robustrep-0.1.2/tests/unit/test_rpc.py +459 -0
  55. robustrep-0.1.2/tests/unit/test_schema.py +201 -0
  56. robustrep-0.1.2/tests/unit/test_store.py +432 -0
  57. robustrep-0.1.2/tests/unit/test_sybil.py +557 -0
  58. robustrep-0.1.2/tests/unit/test_version.py +15 -0
  59. robustrep-0.1.0/PKG-INFO +0 -34
  60. robustrep-0.1.0/README.md +0 -9
  61. robustrep-0.1.0/pyproject.toml +0 -45
  62. robustrep-0.1.0/robustrep/__init__.py +0 -6
  63. robustrep-0.1.0/robustrep/normalize.py +0 -73
  64. robustrep-0.1.0/robustrep/report/export.py +0 -52
  65. robustrep-0.1.0/robustrep/sources/evidence_fetch.py +0 -395
  66. robustrep-0.1.0/robustrep/sources/rater_profile.py +0 -272
  67. robustrep-0.1.0/robustrep/sybil.py +0 -260
  68. robustrep-0.1.0/robustrep.egg-info/PKG-INFO +0 -34
  69. robustrep-0.1.0/robustrep.egg-info/SOURCES.txt +0 -31
  70. {robustrep-0.1.0 → robustrep-0.1.2}/LICENSE +0 -0
  71. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/explain.py +0 -0
  72. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/__init__.py +0 -0
  73. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/report/figures.py +0 -0
  74. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/schema.py +0 -0
  75. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep/sources/__init__.py +0 -0
  76. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/dependency_links.txt +0 -0
  77. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/entry_points.txt +0 -0
  78. {robustrep-0.1.0 → robustrep-0.1.2}/robustrep.egg-info/top_level.txt +0 -0
  79. {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
@@ -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) float64 cells materialized at once
27
- # by `bootstrap_ci`'s chunked multinomial draw, so peak memory stays bounded
28
- # even for a ratee with thousands of votes: a chunk holds at most this many
29
- # cells regardless of how large `n_boot` is.
30
- _MAX_BOOT_CHUNK_CELLS = 2_000_000
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, 2_000_000 // n)` resamples so peak memory stays bounded even for
173
- a ratee with thousands of votes; this is what keeps the per-ratee
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, _MAX_BOOT_CHUNK_CELLS // n)
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))