paytrace 2.0.0__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 (94) hide show
  1. paytrace-2.0.0/.github/ISSUE_TEMPLATE/new_collector.md +18 -0
  2. paytrace-2.0.0/.gitignore +150 -0
  3. paytrace-2.0.0/AUDIT_RESPONSE.md +167 -0
  4. paytrace-2.0.0/AUDIT_RESPONSE_2.md +130 -0
  5. paytrace-2.0.0/AUDIT_RESPONSE_3.md +138 -0
  6. paytrace-2.0.0/AUDIT_RESPONSE_4.md +177 -0
  7. paytrace-2.0.0/AUDIT_RESPONSE_5.md +101 -0
  8. paytrace-2.0.0/AUDIT_RESPONSE_6.md +85 -0
  9. paytrace-2.0.0/AUDIT_RESPONSE_7.md +114 -0
  10. paytrace-2.0.0/CHANGELOG.md +355 -0
  11. paytrace-2.0.0/CITATION.cff +28 -0
  12. paytrace-2.0.0/CLOSEOUT.md +131 -0
  13. paytrace-2.0.0/CODE_OF_CONDUCT.md +17 -0
  14. paytrace-2.0.0/CONTRIBUTING.md +43 -0
  15. paytrace-2.0.0/DEPLOYMENT.md +281 -0
  16. paytrace-2.0.0/DESIGN.md +146 -0
  17. paytrace-2.0.0/LICENSE +146 -0
  18. paytrace-2.0.0/METHODOLOGY_AUDIT.md +319 -0
  19. paytrace-2.0.0/NOTICE +14 -0
  20. paytrace-2.0.0/PKG-INFO +780 -0
  21. paytrace-2.0.0/PUBLISHING.md +478 -0
  22. paytrace-2.0.0/README.md +743 -0
  23. paytrace-2.0.0/SECURITY.md +79 -0
  24. paytrace-2.0.0/SELF_AUDIT.md +113 -0
  25. paytrace-2.0.0/SELF_AUDIT_2.md +84 -0
  26. paytrace-2.0.0/VERIFYING.md +40 -0
  27. paytrace-2.0.0/adversarial/README.md +179 -0
  28. paytrace-2.0.0/adversarial/TRACE_clean.md +51 -0
  29. paytrace-2.0.0/adversarial/TRACE_defended.md +52 -0
  30. paytrace-2.0.0/adversarial/TRACE_diff.md +83 -0
  31. paytrace-2.0.0/adversarial/TRACE_naive.md +44 -0
  32. paytrace-2.0.0/demo/run_demo.py +243 -0
  33. paytrace-2.0.0/examples/end_to_end_domain.py +362 -0
  34. paytrace-2.0.0/examples/reference_collector.py +228 -0
  35. paytrace-2.0.0/pyproject.toml +75 -0
  36. paytrace-2.0.0/src/paytrace/__init__.py +98 -0
  37. paytrace-2.0.0/src/paytrace/adstxt.py +468 -0
  38. paytrace-2.0.0/src/paytrace/adversarial.py +334 -0
  39. paytrace-2.0.0/src/paytrace/agent/__init__.py +20 -0
  40. paytrace-2.0.0/src/paytrace/agent/agent.py +432 -0
  41. paytrace-2.0.0/src/paytrace/agent/fixtures.py +210 -0
  42. paytrace-2.0.0/src/paytrace/agent/guards.py +269 -0
  43. paytrace-2.0.0/src/paytrace/agent/tools.py +435 -0
  44. paytrace-2.0.0/src/paytrace/catalog.py +174 -0
  45. paytrace-2.0.0/src/paytrace/cli.py +284 -0
  46. paytrace-2.0.0/src/paytrace/collectors/__init__.py +26 -0
  47. paytrace-2.0.0/src/paytrace/collectors/analytics.py +288 -0
  48. paytrace-2.0.0/src/paytrace/collectors/artifacts.py +439 -0
  49. paytrace-2.0.0/src/paytrace/collectors/base.py +60 -0
  50. paytrace-2.0.0/src/paytrace/collectors/business.py +558 -0
  51. paytrace-2.0.0/src/paytrace/collectors/disclosure.py +347 -0
  52. paytrace-2.0.0/src/paytrace/collectors/infra.py +246 -0
  53. paytrace-2.0.0/src/paytrace/collectors/lookups.py +454 -0
  54. paytrace-2.0.0/src/paytrace/collectors/persona.py +411 -0
  55. paytrace-2.0.0/src/paytrace/collectors/records.py +200 -0
  56. paytrace-2.0.0/src/paytrace/collectors/registries.py +405 -0
  57. paytrace-2.0.0/src/paytrace/collectors/surface.py +385 -0
  58. paytrace-2.0.0/src/paytrace/data/registries.yaml +484 -0
  59. paytrace-2.0.0/src/paytrace/egress.py +491 -0
  60. paytrace-2.0.0/src/paytrace/enrich.py +400 -0
  61. paytrace-2.0.0/src/paytrace/extract.py +374 -0
  62. paytrace-2.0.0/src/paytrace/fingerprint.py +333 -0
  63. paytrace-2.0.0/src/paytrace/index.py +460 -0
  64. paytrace-2.0.0/src/paytrace/ingest.py +341 -0
  65. paytrace-2.0.0/src/paytrace/net.py +580 -0
  66. paytrace-2.0.0/src/paytrace/netsec.py +241 -0
  67. paytrace-2.0.0/src/paytrace/pinned.py +135 -0
  68. paytrace-2.0.0/src/paytrace/pivot.py +231 -0
  69. paytrace-2.0.0/src/paytrace/portfolio.py +331 -0
  70. paytrace-2.0.0/src/paytrace/robin_ingest.py +337 -0
  71. paytrace-2.0.0/src/paytrace/sellersjson.py +259 -0
  72. paytrace-2.0.0/tests/test_adstxt.py +200 -0
  73. paytrace-2.0.0/tests/test_adversarial.py +77 -0
  74. paytrace-2.0.0/tests/test_adversarial_example.py +200 -0
  75. paytrace-2.0.0/tests/test_agent.py +199 -0
  76. paytrace-2.0.0/tests/test_artifacts.py +202 -0
  77. paytrace-2.0.0/tests/test_catalog.py +118 -0
  78. paytrace-2.0.0/tests/test_collectors.py +52 -0
  79. paytrace-2.0.0/tests/test_disclosure.py +213 -0
  80. paytrace-2.0.0/tests/test_egress.py +232 -0
  81. paytrace-2.0.0/tests/test_enrich.py +232 -0
  82. paytrace-2.0.0/tests/test_extract.py +245 -0
  83. paytrace-2.0.0/tests/test_fingerprint.py +189 -0
  84. paytrace-2.0.0/tests/test_index.py +127 -0
  85. paytrace-2.0.0/tests/test_ingest.py +125 -0
  86. paytrace-2.0.0/tests/test_lookups.py +194 -0
  87. paytrace-2.0.0/tests/test_obfuscation_collectors.py +171 -0
  88. paytrace-2.0.0/tests/test_persona_gate.py +84 -0
  89. paytrace-2.0.0/tests/test_records.py +81 -0
  90. paytrace-2.0.0/tests/test_robin_ingest.py +115 -0
  91. paytrace-2.0.0/tests/test_security.py +170 -0
  92. paytrace-2.0.0/tests/test_sellersjson.py +167 -0
  93. paytrace-2.0.0/tests/test_surface.py +219 -0
  94. paytrace-2.0.0/tests/test_transport.py +524 -0
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: New collector proposal
3
+ about: Propose a new data source
4
+ labels: collector
5
+ ---
6
+
7
+ **Source** (URL and what it publishes)
8
+
9
+ **Legal basis** — statutory registry / IAB spec / RFC protocol / mandated disclosure
10
+
11
+ **Auth** — keyless, free key, or paid (paid sources need a strong argument)
12
+
13
+ **Rate limit and robots.txt status**
14
+
15
+ **Claims emitted** — predicate, reliability, and what constitutes one
16
+ `correlation_group`
17
+
18
+ **Selectivity** — roughly how many entities share a typical value?
@@ -0,0 +1,150 @@
1
+ # ─────────────────────────────────────────────────────────────────────────
2
+ # INVESTIGATION ARTIFACTS — never commit
3
+ #
4
+ # These are the real leak risk in this project. Case files carry the
5
+ # authorization reference (often an internal ticket number). Evidence
6
+ # packages carry bytes captured from a live target. Audit logs and graphs
7
+ # carry the identifiers of whoever was investigated. A corpus index carries
8
+ # a crawl of real domains.
9
+ #
10
+ # None of it is method, all of it is operational, and once it is in git
11
+ # history it is in every clone and fork forever.
12
+ # ─────────────────────────────────────────────────────────────────────────
13
+
14
+ # Case files (authorization refs, seeds, contact emails)
15
+ case.yaml
16
+ case-*.yaml
17
+ case_*.yaml
18
+ *.case.yaml
19
+ !case.example.yaml
20
+ !case.demo.yaml
21
+
22
+ # Run output
23
+ out/
24
+ out-*/
25
+ output/
26
+ results/
27
+ runs/
28
+
29
+ # Evidence packages: preserved response bodies, manifests, declarations
30
+ evidence/
31
+ captures/
32
+ evidence_manifest.json
33
+ DECLARATION_DRAFT.md
34
+ TIMESTAMP.md
35
+ verify.py
36
+ !src/**/verify.py
37
+ *.tsq
38
+ *.tsr
39
+
40
+ # Graphs and reports
41
+ investigation_graph.json
42
+ investigation_graph.minimized.json
43
+ entities.ftm.json
44
+ graph.cypher
45
+ attribution_report.md
46
+ attribution_report.html
47
+ verification_trail.md
48
+ verification_trail.json
49
+ portfolio.json
50
+ handle_graph.json
51
+ assessments.json
52
+
53
+ # Audit logs
54
+ audit.jsonl
55
+ audit-*.jsonl
56
+ *.audit.jsonl
57
+
58
+ # Corpus indexes and crawl data
59
+ *.sqlite
60
+ *.sqlite3
61
+ *.db
62
+ !tests/**/*.sqlite
63
+ paytrace.sqlite
64
+ domains.txt
65
+ tranco*.csv
66
+ tranco*.txt
67
+
68
+ # Operational blocklists — data, not method
69
+ infra_blacklist*.txt
70
+ *blacklist*.txt
71
+ !tests/**/*blacklist*.txt
72
+
73
+ # Imported third-party investigation exports
74
+ investigations/
75
+ spiderfoot*.db
76
+ spiderfoot*.csv
77
+ *.stix.json
78
+ opencti*.json
79
+ robin*.json
80
+
81
+ # Handle observations (real handles from real cases)
82
+ handles*.csv
83
+ observations*.csv
84
+ !examples/demo.csv
85
+
86
+ # Calibration corpora (labelled pairs from closed cases)
87
+ labelled_pairs.json
88
+ pairs.json
89
+ train.json
90
+ test.json
91
+ calibration_report.md
92
+ corpus/
93
+
94
+ # HTTP response cache
95
+ .eae-cache/
96
+ .cache/
97
+
98
+ # ─────────────────────────────────────────────────────────────────────────
99
+ # Secrets
100
+ # ─────────────────────────────────────────────────────────────────────────
101
+ .env
102
+ .env.*
103
+ !.env.example
104
+ *.pem
105
+ *.key
106
+ !**/tests/**/*.key
107
+ .pypirc
108
+ credentials.json
109
+ secrets.yaml
110
+
111
+ # ─────────────────────────────────────────────────────────────────────────
112
+ # Python
113
+ # ─────────────────────────────────────────────────────────────────────────
114
+ __pycache__/
115
+ *.py[cod]
116
+ *$py.class
117
+ *.so
118
+ .Python
119
+ build/
120
+ dist/
121
+ sdist/
122
+ wheels/
123
+ *.egg-info/
124
+ *.egg
125
+ MANIFEST
126
+ .venv/
127
+ venv/
128
+ ENV/
129
+ .tox/
130
+ .nox/
131
+ .pytest_cache/
132
+ .ruff_cache/
133
+ .mypy_cache/
134
+ .coverage
135
+ .coverage.*
136
+ htmlcov/
137
+ coverage.xml
138
+ .hypothesis/
139
+
140
+ # ─────────────────────────────────────────────────────────────────────────
141
+ # Editors and OS
142
+ # ─────────────────────────────────────────────────────────────────────────
143
+ .idea/
144
+ .vscode/
145
+ *.swp
146
+ *.swo
147
+ *~
148
+ .DS_Store
149
+ Thumbs.db
150
+ .verify-venv/
@@ -0,0 +1,167 @@
1
+ # Response to external audit
2
+
3
+ Twenty-five findings. I agree with twenty-three, partially disagree with one,
4
+ and disagree with one. This records what changed, what did not, and why.
5
+
6
+ **The audit's central point is correct and I have acted on it:** the system is
7
+ an evidence-ranking engine and the API described it as a probabilistic
8
+ attribution engine. My own methodology audit said "an ordering, not a measured
9
+ frequency" and then shipped a field called `probability` and a band called
10
+ `ATTRIBUTED`. That gap between documentation and surface is exactly the failure
11
+ the auditor identified.
12
+
13
+ ---
14
+
15
+ ## Fixed — the four that were dangerous
16
+
17
+ **1. CI failed open.** `|| true` on integration tests, examples and `pip-audit`.
18
+ Documentation claimed the integration suite gated releases; GitHub did not.
19
+ All three now fail the build. `pip-audit` is renamed "Dependency CVEs (fails the
20
+ build)" because a scan that cannot fail is theatre.
21
+
22
+ **2. `max_bytes` did not prevent OOM.** `r.content[:max_bytes]` materialises the
23
+ whole body first, so a 5 GB response exhausted memory before the slice applied.
24
+ The docstring claimed a protection the code did not provide, which is the worst
25
+ class of bug. Now streams with `aiter_bytes()` and stops reading at the cap,
26
+ after transfer decoding so decompression bombs are bounded by the same limit.
27
+
28
+ **3. Egress did not control transport.** `EgressPool` sat beside a single
29
+ `httpx.AsyncClient` that never used it; `self._clients` was dead. The manifest
30
+ would have recorded a vantage point the request never used — fabricated
31
+ provenance, worse than none. `Fetcher.client_for(label)` now builds one proxied
32
+ client per egress, and a misconfigured egress raises rather than silently
33
+ falling back to direct.
34
+
35
+ **4. Cache key omitted the egress.** A US capture would have served a request
36
+ that asked for a DE vantage point. Key now includes egress label and country.
37
+
38
+ ## Fixed — semantics
39
+
40
+ **5. Stopped calling the output a probability.** `log(1/selectivity)` informs
41
+ the *denominator* of a likelihood ratio; `P(evidence | same entity)` is not
42
+ modelled and varies enormously between a shared GA4 ID and a shared company
43
+ address. Multiplying by a subjective reliability coefficient does not restore
44
+ the missing term. Output now carries `calibration_status: "unvalidated"` and a
45
+ `probability_note` stating plainly what the number is.
46
+
47
+ **23. Renamed the bands.** `ATTRIBUTED` / `PROBABLE` / `POSSIBLE` became
48
+ `STRONG_EVIDENCE` / `MODERATE_EVIDENCE` / `LIMITED_EVIDENCE`, and ICD 203
49
+ estimative language ("almost certainly") became evidence-strength phrasing
50
+ ("strongly supported by the evidence"). The auditor's argument is the right one:
51
+ downstream users quote the strongest surface, and a disclaimer in a methodology
52
+ file does not travel with a word pasted into a report.
53
+
54
+ **7 & 21. Dependence is typed metadata, and the default is conservative.**
55
+ `Claim.dependence_class` is declared by the collector and always wins; regex
56
+ matching survives only as a fallback for older claims. Unrecognised evidence is
57
+ now `UNKNOWN` and discounted at 0.20, not assumed independent. My original
58
+ argument — that a wrong discount is harder to notice than a missing one — was
59
+ worse than the auditor's: for a model whose dominant failure mode is
60
+ overconfidence, unrecognised evidence should fail conservative.
61
+
62
+ **9. Agent no longer reimplements domain logic.** The `sellers.json` tool
63
+ hard-coded `ORG_NAME` while `classify_seller_name()` existed, so the agent path
64
+ typed individual publishers as organisations and routed them to corporate
65
+ registries that hold no record. It now delegates.
66
+
67
+ **12. Version reads source truth.** `importlib.metadata.version()` returned a
68
+ fallback for an uninstalled source tree, so a valid run reported a divergence
69
+ that did not exist.
70
+
71
+ **11. Test taxonomy.** Markers for `network`, `browser`, `integration`,
72
+ `adversarial`, `slow`. Default `addopts` excludes network and browser, so
73
+ `pytest` is green on a machine without DNS or Chromium — which is an environment
74
+ fact, not a defect.
75
+
76
+ **25. Exception taxonomy.** `EXPECTED_FAILURES` covers the world being
77
+ uncooperative; anything else is our defect and raises under `strict=True`. A
78
+ `TypeError` reporting as "tool failed" made production bugs indistinguishable
79
+ from a source being down.
80
+
81
+ ---
82
+
83
+ ## Where I partially disagree — the monorepo
84
+
85
+ The auditor is right about the operational cost: four CI files, four LICENSEs,
86
+ four CHANGELOGs, synchronised versioning, and integration tests that hope
87
+ siblings exist at `../`. That is a distributed monorepo, and it is fragile.
88
+
89
+ But I do not think the answer is one repository *and* one package. The
90
+ `attribution-graph` boundary is real and load-bearing: **it performs no network
91
+ I/O**, which is precisely what makes its scoring auditable, and that guarantee
92
+ is enforced by the package boundary rather than by convention. Someone who wants
93
+ the inference model should not have to install an HTTP client and 36 collectors.
94
+
95
+ So: **one repository, four published packages.** A workspace, not a merge. That
96
+ takes the auditor's operational fix — one CI pipeline where integration failures
97
+ are fatal, one release process, no duplicated workflow config — while keeping
98
+ the boundary that carries a technical guarantee.
99
+
100
+ This is staged rather than done. The repository layout change is mechanical but
101
+ touches every path in `PUSH_INSTRUCTIONS.md`, and doing it half-way would be
102
+ worse than either end state.
103
+
104
+ ---
105
+
106
+ ## Where I disagree
107
+
108
+ **14, on cache versus evidence.** The auditor reads these as conflated. They are
109
+ already separate objects: `EvidenceLog` writes content-addressed, hash-chained
110
+ captures under `evidence/`, while `.eae-cache/` is a replaceable performance
111
+ cache. The `net.py` comment saying "the cached body is the artifact" is wrong and
112
+ has been corrected — but the architecture was right; only the comment was not.
113
+
114
+ The auditor's underlying point about **capture completeness** is correct and is
115
+ now staged: the evidence record should carry the redirect chain, request and
116
+ response headers, TLS peer info, wire-body versus decoded-body hashes, and
117
+ truncation state. That is a real gap.
118
+
119
+ ---
120
+
121
+ ## Staged, with reasons
122
+
123
+ **3. DNS rebinding / TOCTOU.** Correct, and not yet fixed. Validation resolves
124
+ the host; `httpx` resolves again when connecting. Closing it properly means a
125
+ custom transport that pins the validated address while preserving Host header,
126
+ SNI and certificate verification — worth doing carefully rather than quickly.
127
+ **Documented as a known limitation in `SECURITY.md` and the audit** rather than
128
+ left implied.
129
+
130
+ **6. Candidate-generation context.** The auditor is right that a global prior
131
+ applied to deliberately-selected candidates is selection bias, and that no amount
132
+ of tuning fixes it. The fix is an `AssessmentContext` recording how a candidate
133
+ entered the system, so calibration can be conditional on the selection rule.
134
+ That is a modelling change that should land *with* the calibration study, not
135
+ before it.
136
+
137
+ **8. `Reliability` conflates three concepts** — observation fidelity, source
138
+ independence, binding strength. This is the deepest finding after #5, and the
139
+ auditor is right that `AUTHORITATIVE` on a subject-controlled page is
140
+ misleading. Splitting it is a breaking change across every collector, and it
141
+ should be done once, deliberately.
142
+
143
+ **13. Capture completeness.** Above.
144
+
145
+ **24. Predicate policy table.** Behaviour currently spread across `model.py`,
146
+ `scoring.py`, `dependence.py` and collectors. Consolidating is correct and
147
+ mechanical.
148
+
149
+ ---
150
+
151
+ ## What the audit changed about the project's claim
152
+
153
+ From:
154
+
155
+ > determine which legal entity or person operates a domain, with a confidence
156
+ > figure
157
+
158
+ To:
159
+
160
+ > build an auditable evidence graph for entity-attribution investigations, rank
161
+ > candidate relationships by evidence strength, and preserve the provenance
162
+ > necessary for independent review
163
+
164
+ The auditor's closing observation is the one worth keeping: narrowing the claim
165
+ makes the project **more** credible, not less. The narrow claim is supported by
166
+ what exists. The broad one is not, and would not have survived first contact
167
+ with a reviewer who checked.
@@ -0,0 +1,130 @@
1
+ # Response to re-audit (v1.6.0 → v1.7.0)
2
+
3
+ **Every finding I checked was reproducible.** I verified the five criticals
4
+ against the code before changing anything, and each behaved exactly as the
5
+ report described. No pushback on any P0 item.
6
+
7
+ The report's framing is the right one and I am adopting it: the problem is
8
+ **assurance mismatch**. Good local implementations existed while the
9
+ user-facing execution path did not compose their guarantees end-to-end. Fixing
10
+ that was this pass; no features were added.
11
+
12
+ ---
13
+
14
+ ## Verified before fixing
15
+
16
+ | Finding | Reproduced |
17
+ |---|---|
18
+ | EA-01 entry hash omits headers/collector/note/body_path | yes — all five unhashed |
19
+ | EA-02 suffix deletion verifies | yes — deleted a capture, verifier said PASSED |
20
+ | EA-03 runner never wires EvidenceLog | yes — `Fetcher(...)` built with no log |
21
+ | EA-04 definitional self-grant | yes — `subject_html` claim merged evil.example into Victim Corp |
22
+ | EA-05 unique emails corroborate | yes — 6 different emails → 7 points, HIGH, durable=True |
23
+ | EA-09 budget bypassed by redirects | yes — `max_requests` not re-checked per hop |
24
+ | EA-10 declared dependence ignored | yes — never gathered in `assess()`, never serialised |
25
+ | EA-16 VERIFY.sh absent from repos | yes — existed only at workspace root |
26
+
27
+ ---
28
+
29
+ ## P0 — fixed
30
+
31
+ **EA-01.** The entry hash now covers a versioned canonical envelope: request
32
+ headers, response headers, collector, note, body_path, egress, outcome. Bumping
33
+ `ENTRY_SCHEMA` is required to change the field set, so a future field cannot
34
+ fall outside it by omission. Four tamper cases are permanent tests.
35
+
36
+ **EA-02.** The generated verifier recomputes the chain head and compares it to
37
+ the declared `manifest_hash`, and reconciles `capture_count`. Deleting the final
38
+ capture now fails with an explicit message. The verifier also states what it
39
+ **cannot** prove — internal consistency is not provenance, and only an external
40
+ anchor establishes that the package was not regenerated wholesale.
41
+
42
+ **EA-03 / EA-06.** Capture recording and policy evaluation are now transport
43
+ invariants: the layer performing the request records it, including refusals and
44
+ errors. `run_case()` passes the log and the policy engine into `Fetcher`, and
45
+ reconciles fetch count against capture count — a non-empty run producing an
46
+ empty package now fails verification rather than reporting `evidence_verified=True`.
47
+
48
+ **EA-04.** `is_definitional()` requires an engine-recognised registry collector
49
+ or source class, matched literally. Reliability is a scalar the claim author
50
+ supplies, so keying the model's strongest privilege on it made the carve-out
51
+ self-service. The hostile claim now scores UNSUPPORTED and does not merge;
52
+ `gleif` retains the exemption.
53
+
54
+ **EA-05.** A correlation point must connect observations. Identifiers are keyed
55
+ by value and counted only when the same normalised value appears on two or more
56
+ distinct profiles; singletons are reported in the caveat rather than silently
57
+ dropped. The reproduced case is now a permanent test: six profiles, six
58
+ different emails → **1 point, INSUFFICIENT**, down from 7/HIGH.
59
+
60
+ One existing test had encoded the bug — five profiles with five *different*
61
+ names and domains, asserting MODERATE. Rewritten to share the identifiers so it
62
+ tests the cap it was written for.
63
+
64
+ **EA-07.** `Fetcher` is constructed from `EgressPool.from_case(scope.egress)`.
65
+
66
+ **EA-09.** Budget is checked before every physical request including redirect
67
+ hops. `max_requests=1` with a 302→200 chain now raises rather than completing at
68
+ count=2.
69
+
70
+ **EA-10.** `assess()` gathers declared classes per canonical group, rejects
71
+ conflicting declarations within a group, and passes them through. Serialised in
72
+ `Claim.to_dict()`.
73
+
74
+ **EA-11.** Merge policy moved out of probability space:
75
+ `MERGE_LLR_THRESHOLD` in nats, applied to `log_odds`. Renaming the display while
76
+ keeping a 0.90 cut on the same unvalidated number would have been cosmetic.
77
+
78
+ **EA-15.** CI clones sibling repositories before the integration step, so the
79
+ gate can actually pass rather than being structurally red.
80
+
81
+ **EA-16.** `VERIFY.sh` ships in all four repositories. It existed only at the
82
+ workspace root while the docs told users to run it.
83
+
84
+ **EA-17 / EA-18.** Screenshot tests use the declared `browser` marker rather
85
+ than a `skipif` alias, so default `addopts` actually excludes them.
86
+ `available_renderer()` resolves and stats the Chromium executable instead of
87
+ treating package importability as availability. `Fetcher` accepts an injected
88
+ resolver, making the transport tests deterministic — mocking HTTP was not
89
+ enough, because validation performed real DNS first.
90
+
91
+ **EA-22.** Stale `net.py` comment corrected. Test counts reconciled: **723
92
+ collected, 714 in the deterministic default.**
93
+
94
+ ---
95
+
96
+ ## Accepted, staged, with the reason
97
+
98
+ **EA-12 — screenshot is a second network stack outside the SSRF boundary.**
99
+ Correct, and it also breaks the "no network I/O in the core" argument I used to
100
+ justify the package split. `screenshot.py` should move to a capture package
101
+ behind the same policy boundary, and browser subresource requests need route
102
+ interception. That is a package boundary change plus a sandboxing design, and
103
+ doing it badly would be worse than the current documented state.
104
+
105
+ **EA-13 — `parent_body_sha256` is caller-declared.** Right: Playwright
106
+ navigates the live URL independently, so a page changing between fetch and
107
+ screenshot yields screenshot B labelled as rendering body A. The fix is to
108
+ render the preserved body or hash the browser's actual main-document response.
109
+
110
+ **EA-14 — screenshot CLI flags are no-ops.** Confirmed. Rather than leave a
111
+ flag that does nothing, the flags are staged with the capture path.
112
+
113
+ **EA-19 / EA-20 / EA-21.** Exception taxonomy narrowing in `Engine`, audit-log
114
+ minimisation at the serializer boundary, and operator/hostname metadata as an
115
+ explicit choice. All correct, none load-bearing for the P0 assurance story.
116
+
117
+ ---
118
+
119
+ ## On the release framing
120
+
121
+ I accept the recommendation. This is **not** "ready to publish". `DESIGN.md`
122
+ and `PUBLICATION_PLAN.md` now describe it as a **pre-release experimental
123
+ research codebase**, and the evidence package is no longer described as
124
+ tamper-evident without the qualifier that deletion resistance requires an
125
+ external anchor.
126
+
127
+ The two things that would change that assessment, in order: replace the
128
+ LICENSE/OWNER placeholders, and run the calibration study. Everything else on
129
+ the P1/P2 list is quality work on a system whose assurances now at least
130
+ compose.
@@ -0,0 +1,138 @@
1
+ # Response to release-readiness audit (v1.7.0 → v1.7.1)
2
+
3
+ **Decision accepted: HOLD was correct.** Four of the five mandatory blockers are
4
+ fixed; the fifth is licensing, which needs your decision rather than a code
5
+ change.
6
+
7
+ One finding I have to report before the substance.
8
+
9
+ ---
10
+
11
+ ## A discrepancy in the artifacts I shipped
12
+
13
+ The auditor tested the v1.7.0 tarballs. Reproducing their findings, I discovered
14
+ the working tree contained fixes for RB-01, RA-02 and RA-03 that were **not in
15
+ the tarballs I shipped**. I did not make those edits during the session that
16
+ produced v1.7.0.
17
+
18
+ I cannot explain the divergence, and for a tool whose entire purpose is evidence
19
+ integrity that is worth stating plainly rather than quietly reconciling. What I
20
+ have done:
21
+
22
+ - treated the **tarballs** as authoritative, since that is what was audited;
23
+ - re-run every one of the auditor's reproductions against the current tree
24
+ rather than assuming any fix was real;
25
+ - verified each remaining fix myself before claiming it.
26
+
27
+ Concretely: RB-01, RA-02 and RA-03 were already correct in the tree and I
28
+ confirmed them. RB-02, RB-03, RB-04, RA-01 and RA-04 were **not** fixed and I
29
+ fixed them in this pass.
30
+
31
+ ---
32
+
33
+ ## RB-02 — my own regression, and worse than reported
34
+
35
+ The auditor found `MERGE_LLR_THRESHOLD = 13.71` nats compared against
36
+ `a.probability`, which cannot exceed 1.0. Correct, and it was my fix from the
37
+ previous round: I renamed the constant's units without changing what it was
38
+ compared to, converting a tuning question into a correctness bug that failed
39
+ silently in the quiet direction.
40
+
41
+ The constant was also wrong on its own terms. `Assessment.log_odds` already
42
+ includes the prior, so adding `|log(PRIOR_ODDS)|` double-counted it. The
43
+ posterior threshold preserving the old 0.90 boundary is `log(0.9/0.1) = 2.197`.
44
+
45
+ **But fixing the threshold did not restore merging**, and this is the more
46
+ consequential finding. The resolver only scored directly-claimed
47
+ subject→object edges. Two domains sharing an identifier were **never assessed
48
+ as a pair at all** — the toolkit's canonical case. Each leg had one correlation
49
+ group, the corroboration rule correctly refused to merge on one group, and the
50
+ two-group evidence that actually existed was never pooled.
51
+
52
+ So no threshold could have helped: inferential merging had never worked.
53
+
54
+ `resolve()` now generates co-reference candidates — subjects sharing an object —
55
+ and pools both legs, which is the standard record-linkage construction.
56
+ Measured: two domains sharing two identifiers now merge at log_odds 8.89 with
57
+ two independent groups; sharing one identifier still does not merge.
58
+
59
+ ## RB-03 — the constraint passed for the wrong reason
60
+
61
+ The transitive must-not-link check appeared to pass in my first run. It passed
62
+ because nothing merged at all. With merging restored, the real behaviour is
63
+ correct: A~B merges, and B~C is **rejected** because the union would place A and
64
+ C in one cluster against an explicit contradiction.
65
+
66
+ That is a lesson worth recording: a constraint test that passes while the
67
+ mechanism it constrains is disabled proves nothing.
68
+
69
+ ## RB-04 — verifier no longer executes package code
70
+
71
+ `attribution verify` ran the `verify.py` inside the evidence directory. An
72
+ evidence package is untrusted input by definition. New
73
+ `attribution_graph.verify` reads the manifest as data, refuses `body_path`
74
+ values escaping the package, and the CLI uses it. Tested with a planted script
75
+ that writes a marker file: exit 0, marker absent.
76
+
77
+ The generated standalone `verify.py` still ships — a dependency-free checker a
78
+ reader can inspect in full has real value — and the CLI now prints a note saying
79
+ it was *not* executed and why.
80
+
81
+ ## RB-05 — licensing
82
+
83
+ Unresolved, and not something I should decide. All four LICENSE files remain
84
+ placeholders while `pyproject.toml` asserts Apache-2.0, so the built metadata
85
+ makes a claim the shipped file does not support. `VERIFY.sh` fails on this by
86
+ design. ~20 files still carry `github.com/tusharkarumudi/`.
87
+
88
+ ---
89
+
90
+ ## RA items
91
+
92
+ **RA-01 — evidence completeness.** 404s and redirect hops were already recorded
93
+ in the tree; I verified both. The cache case was not: a second run consumed
94
+ retrieved content and produced a package with zero captures that verified
95
+ clean. Cache hits now emit a capture with `outcome="cache"` carrying the body,
96
+ so the digest still commits to what was used and a reviewer can see the bytes
97
+ were not fetched during that run.
98
+
99
+ **RA-04 — provider/network mismatch.** `provider: oxylabs` with
100
+ `network: datacenter` was accepted and routed to `pr.oxylabs.io`, the profile's
101
+ own documented *residential* endpoint, while recording
102
+ `consent_sensitive: false`. Provenance and safeguard wrong at once. Profiles now
103
+ declare which networks they serve and a contradicting label is rejected.
104
+
105
+ **RA-06 / 8.3 / 8.4 — screenshots.** Taken off the release surface.
106
+ `--screenshots` now exits 2 with an explanation rather than being silently
107
+ accepted and doing nothing. Kept as an explicit error rather than removed so a
108
+ user with it in a script learns why.
109
+
110
+ ---
111
+
112
+ ## Phase D gate
113
+
114
+ All six tests the auditor specified now exist and pass:
115
+
116
+ 1. real `PolicyEngine` + real `Fetcher` across respect / record / ignore
117
+ 2. resolver positive merge — two independent groups, non-definitional
118
+ 3. resolver cluster constraint — A~B, B~C, A!~C
119
+ 4. verifier safety — planted script not executed
120
+ 5. evidence completeness — 404, redirect, cache
121
+ 6. case egress — survives `load()`, reaches transport, mismatch rejected
122
+
123
+ Writing (1) surfaced a further defect in my own test: a shared cache directory
124
+ served mode 2 from mode 1's fetch, so the assertion passed for the wrong reason.
125
+ Each mode now gets its own cache.
126
+
127
+ **739 tests collected, 739 passing in the deterministic default.**
128
+
129
+ ---
130
+
131
+ ## Still open, deliberately
132
+
133
+ DNS rebinding through remote proxies; moving `screenshot.py` out of the
134
+ inference package; collector-by-collector `dependence_class` adoption; the
135
+ calibration study. All are on the auditor's own defer list, and the claims
136
+ around them are restrained accordingly.
137
+
138
+ The remaining release gate is licensing, which is yours.