sm-divergence 0.7.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 (51) hide show
  1. sm_divergence-0.7.0/.github/workflows/ci.yml +30 -0
  2. sm_divergence-0.7.0/.github/workflows/release.yml +49 -0
  3. sm_divergence-0.7.0/.gitignore +14 -0
  4. sm_divergence-0.7.0/CHANGELOG.md +172 -0
  5. sm_divergence-0.7.0/CONTRIBUTING.md +31 -0
  6. sm_divergence-0.7.0/GOVERNANCE.md +31 -0
  7. sm_divergence-0.7.0/LICENSE +21 -0
  8. sm_divergence-0.7.0/Makefile +11 -0
  9. sm_divergence-0.7.0/PKG-INFO +273 -0
  10. sm_divergence-0.7.0/PUBLISHING.md +41 -0
  11. sm_divergence-0.7.0/README.md +245 -0
  12. sm_divergence-0.7.0/SPEC.md +259 -0
  13. sm_divergence-0.7.0/WHITEPAPER.md +218 -0
  14. sm_divergence-0.7.0/examples/__init__.py +0 -0
  15. sm_divergence-0.7.0/examples/identity_key_divergence.py +39 -0
  16. sm_divergence-0.7.0/examples/registry_divergence.py +47 -0
  17. sm_divergence-0.7.0/examples/signed_self_description.py +51 -0
  18. sm_divergence-0.7.0/pyproject.toml +59 -0
  19. sm_divergence-0.7.0/sm_divergence/__init__.py +119 -0
  20. sm_divergence-0.7.0/sm_divergence/__main__.py +3 -0
  21. sm_divergence-0.7.0/sm_divergence/_signing.py +88 -0
  22. sm_divergence-0.7.0/sm_divergence/cli.py +109 -0
  23. sm_divergence-0.7.0/sm_divergence/detector.py +61 -0
  24. sm_divergence-0.7.0/sm_divergence/discovery/__init__.py +32 -0
  25. sm_divergence-0.7.0/sm_divergence/discovery/attestation.py +81 -0
  26. sm_divergence-0.7.0/sm_divergence/discovery/client.py +73 -0
  27. sm_divergence-0.7.0/sm_divergence/discovery/closure.py +15 -0
  28. sm_divergence-0.7.0/sm_divergence/discovery/http_by_id.py +54 -0
  29. sm_divergence-0.7.0/sm_divergence/discovery/index_v2.py +132 -0
  30. sm_divergence-0.7.0/sm_divergence/discovery/views.py +32 -0
  31. sm_divergence-0.7.0/sm_divergence/identity/__init__.py +54 -0
  32. sm_divergence-0.7.0/sm_divergence/identity/closure.py +64 -0
  33. sm_divergence-0.7.0/sm_divergence/identity/description_layer.py +164 -0
  34. sm_divergence-0.7.0/sm_divergence/identity/didkey.py +46 -0
  35. sm_divergence-0.7.0/sm_divergence/identity/didweb.py +98 -0
  36. sm_divergence-0.7.0/sm_divergence/identity/selfdesc.py +202 -0
  37. sm_divergence-0.7.0/sm_divergence/identity/universal.py +76 -0
  38. sm_divergence-0.7.0/sm_divergence/identity/views.py +41 -0
  39. sm_divergence-0.7.0/tests/test_attestation.py +77 -0
  40. sm_divergence-0.7.0/tests/test_cli.py +109 -0
  41. sm_divergence-0.7.0/tests/test_client.py +70 -0
  42. sm_divergence-0.7.0/tests/test_closure.py +108 -0
  43. sm_divergence-0.7.0/tests/test_core.py +82 -0
  44. sm_divergence-0.7.0/tests/test_description_layer.py +139 -0
  45. sm_divergence-0.7.0/tests/test_detector.py +121 -0
  46. sm_divergence-0.7.0/tests/test_diff.py +101 -0
  47. sm_divergence-0.7.0/tests/test_identity.py +142 -0
  48. sm_divergence-0.7.0/tests/test_index_v2.py +123 -0
  49. sm_divergence-0.7.0/tests/test_resolver.py +79 -0
  50. sm_divergence-0.7.0/tests/test_selfdesc.py +148 -0
  51. sm_divergence-0.7.0/uv.lock +572 -0
@@ -0,0 +1,30 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ ci-local:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - name: Pin Python
18
+ run: uv python pin ${{ matrix.python-version }}
19
+ # The same gate as `make ci-local` — green locally == green here.
20
+ # sm-resolver (the kernel) resolves from PyPI.
21
+ - run: uv sync --extra dev
22
+ - run: uv run ruff check .
23
+ - run: uv run ruff format --check .
24
+ - run: uv run mypy sm_divergence
25
+ - run: uv run pytest -v
26
+ - name: Examples run
27
+ run: |
28
+ uv run python examples/registry_divergence.py
29
+ uv run python examples/identity_key_divergence.py
30
+ uv run python examples/signed_self_description.py
@@ -0,0 +1,49 @@
1
+ # Publish sm-divergence to PyPI on a version tag, via PyPI Trusted Publishing (OIDC).
2
+ #
3
+ # No API token anywhere: PyPI is told (once, in its web UI) to trust THIS repo +
4
+ # THIS workflow file, and the `id-token: write` permission below lets the job
5
+ # mint a short-lived OIDC token PyPI accepts. See PUBLISHING.md for the one-time
6
+ # setup — including the prerequisite of publishing sm-resolver first and swapping
7
+ # the git-URL dependency for a version spec (PyPI rejects direct references).
8
+ #
9
+ # Fires only when a tag like `v0.7.0` is pushed — never on normal commits.
10
+ name: release
11
+
12
+ on:
13
+ push:
14
+ tags:
15
+ - "v*"
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ publish:
22
+ name: Build + publish to PyPI
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ # A job-level permissions block REPLACES the workflow-level one (it does not
26
+ # merge), so contents:read must be restated here or actions/checkout 404s on
27
+ # its own repo. Both lines are required.
28
+ contents: read # for actions/checkout
29
+ id-token: write # REQUIRED for Trusted Publishing — this is what replaces the token.
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.12"
36
+
37
+ - name: Build sdist + wheel
38
+ run: |
39
+ python -m pip install --upgrade build
40
+ python -m build
41
+
42
+ - name: Check the built distributions
43
+ run: |
44
+ python -m pip install --upgrade twine
45
+ python -m twine check dist/*
46
+
47
+ - name: Publish to PyPI
48
+ # No `with: password:` — the action uses the OIDC token automatically.
49
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ .venv/
8
+ dist/
9
+ build/
10
+ uv.lock.tmp
11
+
12
+ # Development & reconnaissance notes — design scoping, competitive research,
13
+ # decision records. Kept local, never published.
14
+ dev/
@@ -0,0 +1,172 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/) and this project adheres to
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.7.0] — 2026-07-04
8
+
9
+ Self-description hardening — replay resistance, dogfooded distribution, and alias
10
+ closure. Also the system's first **agent-equivocation** detection.
11
+
12
+ ### Added
13
+ - **`seq` + supersession** — the self-description carries a monotonic `seq`
14
+ (signed); highest supersedes. `build_self_description(..., seq=N)` is now
15
+ required. Closes the replay hole where an unexpired but superseded description
16
+ could be served as "the agent's own signature."
17
+ - **`reconcile_descriptions` / `DescriptionReconciliation`** — reconcile
18
+ descriptions gathered from multiple sources (`seq`-aware): the surviving
19
+ highest-`seq` `canonical`, plus findings `agent_equivocation` (two valid
20
+ descriptions at the same `seq` with different content → the agent's key signed
21
+ a conflict; correspondence fails closed), `omission`, and `stale_description`.
22
+ - **`HttpDescriptionResolver` + `corroborate_descriptions`** — fetch + verify the
23
+ description from every source and reconcile, so the description's own
24
+ distribution (omission / replay) is corroborated, not trusted from one source.
25
+ - **`AliasClosureResolver` + `binds_by_did` (discovery) / `binds_by_key`
26
+ (identity)** — a record reached via a self-asserted alias that does not bind
27
+ back to the agent's `did:key` contributes no claim, never a divergence. Prevents
28
+ a malicious agent from manufacturing a divergence by naming a victim's alias.
29
+
30
+ ### Changed
31
+ - `build_self_description` gains a required `seq`; `SelfDescription` gains `seq`
32
+ and `content_key()`. `verify_self_description` parses and requires `seq`.
33
+
34
+ ## [0.6.0] — 2026-07-04
35
+
36
+ The kernel is extracted. The source-agnostic core (`Resolver[T]`, `View`,
37
+ `diff_views`, `Corroborator`, `Finding`) now lives in its own package,
38
+ [`sm-resolver`](https://github.com/Sharathvc23/sm-resolver), and `sm-divergence`
39
+ depends on it — the extraction the layered design was built toward, triggered
40
+ once the identity layer joined discovery as a second consumer.
41
+
42
+ ### Changed
43
+ - **`sm_divergence.core` removed**; the kernel is the `sm-resolver` dependency.
44
+ The top-level public API is unchanged (`from sm_divergence import Corroborator,
45
+ Finding, Resolver, Status, View, ViewT, diff_views, OMISSION, …` still work — now
46
+ re-exported from `sm-resolver`). Deep imports of `sm_divergence.core.*` no longer
47
+ exist; import from `sm_resolver` or the top level.
48
+ - CI installs the private `sm-resolver` git dependency (needs the
49
+ `SM_RESOLVER_PAT` secret, or a public `sm-resolver`).
50
+ - Version realigned (pyproject had drifted at 0.3.0 while the package was at 0.5.0).
51
+
52
+ ## [0.5.0] — 2026-07-04
53
+
54
+ Signed portable-identity self-description (Phase 3) — identity correspondence
55
+ becomes an agent-signed fact instead of caller-configured trust.
56
+
57
+ ### Added
58
+ - **`sm_divergence.identity` self-description** — the agent signs a bundle
59
+ asserting its own aliases (its name per namespace) and service endpoints with
60
+ its `did:key`; a consumer verifies it offline and drives resolvers from the
61
+ *verified* names, so a source contradicting a signed alias/endpoint diverges
62
+ from the agent's own signature:
63
+ - `build_self_description(aliases, services, private_key)` → signed
64
+ `{descriptor, sig}` (did derived from the key; 24h default TTL).
65
+ - `verify_self_description(bundle)` → `SelfDescription | None` (offline sig +
66
+ freshness; string→string alias/service maps; never raises).
67
+ - `verified_descriptions(bundles)` → verify a map, keep only what verifies.
68
+ - `signed_alias_for(descriptions, namespace)` → a `did_for` / `locator_for`
69
+ backed by verified aliases; **falls back to the id when unverified**, so a
70
+ forged description can't redirect a resolver.
71
+ - **`sm_divergence._signing`** — the shared Ed25519 / `did:key` / JCS primitives,
72
+ now used by both the discovery attestation adapter and the self-description
73
+ (behind the `attestation` extra). No behaviour change to `verified_did`.
74
+
75
+ ## [0.4.0] — 2026-07-04
76
+
77
+ The identity layer (Phase 2 of the resolver-stack design) — the core's second
78
+ consumer, proving the kernel generalizes past discovery. Corroborates the one
79
+ field that must agree across an agent's DID methods: its verification key.
80
+
81
+ ### Added
82
+ - **`sm_divergence.identity`** — key-consistency across DID methods, built on
83
+ the same core (`DidView` implements `View`; the resolvers produce it,
84
+ `Corroborator` + `diff_views` do the rest):
85
+ - `DidKeyResolver` — resolves a `did:key` offline (the self-certifying root);
86
+ - `DidWebResolver` — fetches a `did:web` document and reads its
87
+ `publicKeyMultibase`;
88
+ - `UniversalResolverResolver` — resolves any method via a DIF Universal
89
+ Resolver endpoint (`GET {base}/1.0/identifiers/{did}`).
90
+ A method that serves a different key than the root `did:key` is a `key`
91
+ divergence — the identity-layer analogue of endpoint tampering (a hijacked
92
+ domain, a stale mirror, or a lying resolver). Keys compare as `did:key`
93
+ multibase strings, so no crypto dependency is added.
94
+ - Public: `DidView`, `DidKeyResolver`, `DidWebResolver`, `UniversalResolverResolver`,
95
+ `key_from_did_key`, `key_from_did_document`, `did_web_url`, `identity_did`, `KEY`.
96
+
97
+ ## [0.3.0] — 2026-07-04
98
+
99
+ The core/discovery seam (Phase 1 of the resolver-stack design). Splits the
100
+ package into a source-agnostic **kernel** and the discovery **layer** on top, so
101
+ the kernel is extractable and a thin per-format adapter (e.g. a NANDA Index
102
+ adapter) needs no discovery internals.
103
+
104
+ ### Added
105
+ - **`sm_divergence.core`** — the layer-agnostic kernel: the `View` contract
106
+ (a claim reduced to named comparable fields via `comparable()`), a generic
107
+ `Resolver[T]` protocol, a generic `diff_views` (findings named after whatever
108
+ fields the view exposes), the `Corroborator[T]` orchestration, and `Finding`.
109
+ Nothing here knows any registry format. Intended to extract as a standalone
110
+ kernel once a second layer consumes it.
111
+ - **`sm_divergence.discovery`** — the registry-integrity layer: `RecordView`
112
+ plus the `HttpByIdResolver` (NEST) and `NandaIndexV2Resolver` adapters, all
113
+ thin on the core.
114
+ - `Corroborator`, `View`, `ViewT` are now public (for adapter/layer authors).
115
+
116
+ ### Changed
117
+ - **Finding detail is uniform across layers.** A field divergence now carries
118
+ `{"field": <name>, "values": {source: value}}` (was `{"endpoints": {...}}` /
119
+ `{"dids": {...}}`), so any consumer parses findings the same way regardless of
120
+ layer or field. `omission` detail is unchanged. `kind` is unchanged
121
+ (`omission`, or the field name — `endpoint`/`did`).
122
+ - Module layout moved under `core/` and `discovery/`. The top-level public API
123
+ (`from sm_divergence import …`) is unchanged; only deep imports
124
+ (`sm_divergence.client`, `.resolver`, `.index_v2`, `.models`, `.attestation`)
125
+ moved (to `sm_divergence.discovery.*`).
126
+
127
+ ## [0.2.0] — 2026-07-04
128
+
129
+ Heterogeneous registries: corroborate across registries that speak *different*
130
+ formats, not just different JSON at the same by-id endpoint.
131
+
132
+ ### Added
133
+ - **The `Resolver` protocol** (`sm_divergence.resolver`, SPEC §6) — a per-registry
134
+ component mapping a canonical id to a normalized `(Status, RecordView)`. Each
135
+ resolver owns one registry's transport, record extraction, and native
136
+ verification; the diff is untouched because every resolver returns the same view.
137
+ - **`HttpByIdResolver`** — the `GET /api/agents/{id}` shape (e.g. NEST), the v0.1
138
+ behavior repackaged as the first resolver.
139
+ - **`NandaIndexV2Resolver`** (`sm_divergence.index_v2`) — the NANDA Index v2 two-hop
140
+ resolve (`/api/v1/resolve?locator=…` → `{registry_url, identifier}` → fetch the
141
+ record), normalized to a `RecordView`. `locator_for` maps a canonical id to the
142
+ Index's URN locator (identity correspondence is the caller's to define).
143
+ - **`DivergenceDetector` accepts `str | Resolver`** — bare URLs are wrapped in
144
+ `HttpByIdResolver` (back-compatible), and resolver instances of different formats
145
+ can be mixed in one detector. Views are keyed by each resolver's `label`.
146
+ - **CLI `--nanda-index URL`** — corroborate a NANDA Index v2 alongside `--registry`.
147
+
148
+ ### Changed
149
+ - `looks_absent` (was `_looks_absent`) is now a shared public helper so every HTTP
150
+ resolver classifies soft-404 identically.
151
+
152
+ ## [0.1.0] — 2026-07-04
153
+
154
+ Initial public release of sm-divergence — cross-registry divergence detection for
155
+ accountable agent discovery.
156
+
157
+ ### Added
158
+ - **The diff** (`diff_views`, SPEC §4) — the pure, deterministic core: per-registry
159
+ claims → `omission` / `endpoint` / `did` findings. No I/O; an unreachable
160
+ registry (an `error` claim) is excluded rather than counted as absence.
161
+ - **Claim classification** (`fetch_view`, SPEC §3) — one registry's answer about one
162
+ id as `present` / `absent` / `error`, with hard-404 and soft-404 (`200` + not-found
163
+ body) both mapping to `absent`. Injectable by-id path and record adapter.
164
+ - **`DivergenceDetector`** — fetch + diff + dedupe orchestration with an
165
+ `on_finding` callback that fires once per distinct finding; `check_once` one-shot.
166
+ - **Verified-DID extraction** (`sm_divergence.attestation`, SPEC §5) — optional
167
+ `attestation` extra: Ed25519 / `did:key` / JCS verification of a self-certifying
168
+ record, plus a subject-match guard, feeding the `did` diff. Kept out of the core
169
+ so omission/endpoint checks carry no cryptographic dependency.
170
+ - **CLI** — `python -m sm_divergence check --registry … --watch …`; exit `2` on any
171
+ divergence (cron/CI-friendly), `--json`, `--attestation`.
172
+ - Whitepaper, spec, and governance documents.
@@ -0,0 +1,31 @@
1
+ # Contributing to sm-divergence
2
+
3
+ Contributions are accepted under the Developer Certificate of Origin (DCO)
4
+ sign-off model. Add `Signed-off-by: Your Name <you@example.com>` to every commit
5
+ (`git commit -s`).
6
+
7
+ ## Change process
8
+
9
+ 1. Spec-affecting changes open a PR that updates the **spec (`SPEC.md`), the tests,
10
+ and the code together**. Drift between these is a defect — the `tests/` suite is
11
+ the authoritative behavioural specification of the diff.
12
+ 2. The gate is `make ci-local` (uv: `ruff` → `ruff format` → `mypy --strict` →
13
+ `pytest`). CI runs the same on Python 3.11 and 3.12. Push only when it is green.
14
+ 3. Every new guarantee needs a happy-path test; every classification and every
15
+ divergence kind needs its own case. The negative cases — unreachable ≠ absent,
16
+ one-verified-DID ≠ divergence, soft-404 = absent — are the point.
17
+
18
+ ## House rules
19
+
20
+ - **A timeout is not a claim.** An unreachable or erroring source MUST be
21
+ excluded from the diff, never counted as an omission. This is the axiom the whole
22
+ library rests on; a change here needs an RFC-style PR to `SPEC.md` first.
23
+ - **Only proven values disagree.** An unverified value (e.g. an unverified DID)
24
+ never enters the diff. Do not weaken the verification path.
25
+ - **The diff stays pure and generic.** `diff_views` is deterministic, does no
26
+ I/O, never raises, and never learns its layer. All I/O lives in the resolvers;
27
+ a layer supplies a view (`comparable()`) and thin resolvers, nothing more.
28
+ - **No new crypto.** The optional DID check reuses the family convention
29
+ (Ed25519 / `did:key` / JCS). Do not re-implement it elsewhere.
30
+ - **No expansion of the classification (SPEC §3), the view contract (SPEC §4), or
31
+ the diff (SPEC §5) without an RFC-style PR to `SPEC.md` first.**
@@ -0,0 +1,31 @@
1
+ # Governance
2
+
3
+ ## Scope
4
+
5
+ | In scope | Out of scope |
6
+ | --- | --- |
7
+ | the claim classification (`present`/`absent`/`error`), the view contract (`comparable()`), the divergence diff (`omission` + per-field: `endpoint`/`did`/`key`/…), and the optional verified-DID extraction | the source itself; the record/document format and its signing (AgentFacts / self-certifying records / DID documents — the *tampering* half); a transparency log or witness network (single-source consistent lying); remediation policy on a finding; and each source's transport/schema (injected by a resolver) |
8
+
9
+ The primitive owns one thing — answering *"do these sources disagree about this
10
+ agent, and how?"* — across any layer (discovery, identity, and future
11
+ capability/evidence). Anything outside the table belongs to a companion package
12
+ or the caller.
13
+
14
+ ## Versioning
15
+
16
+ Semantic Versioning 2.0.0. The claim classification (SPEC §3), the view contract
17
+ (SPEC §4), and the diff (SPEC §5) are frozen within a major; a change requires an
18
+ RFC-style PR to `SPEC.md` before code. The library API (`diff_views`,
19
+ `Corroborator`, `DivergenceDetector`, `check_once`) follows the same major.
20
+
21
+ ## Conformance
22
+
23
+ An implementation is conforming iff it reproduces the finding set in SPEC §10 for
24
+ the specified claim inputs. The diff is pure and deterministic by construction, so
25
+ conformance is mechanical.
26
+
27
+ ## Changes
28
+
29
+ Spec-affecting changes: open a PR editing `SPEC.md` (and `WHITEPAPER.md` if the
30
+ rationale moves), then the implementation and tests. Non-spec changes (adapters,
31
+ CLI, performance) are ordinary PRs against a green `make ci-local`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stellarminds.ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ .PHONY: ci-local
2
+ # One-command pre-push gate — the same steps CI runs, hard-failing on first red.
3
+ ci-local:
4
+ uv sync --extra dev
5
+ uv run ruff check .
6
+ uv run ruff format --check .
7
+ uv run mypy sm_divergence
8
+ uv run pytest -v
9
+ uv run python examples/registry_divergence.py
10
+ uv run python examples/identity_key_divergence.py
11
+ uv run python examples/signed_self_description.py
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.4
2
+ Name: sm-divergence
3
+ Version: 0.7.0
4
+ Summary: Catch a cheating registry by corroboration — omission/endpoint/DID + cross-DID-method key divergence for federated agent discovery, on the sm-resolver kernel.
5
+ Project-URL: Homepage, https://github.com/Sharathvc23/sm-divergence
6
+ Project-URL: Spec, https://github.com/Sharathvc23/sm-divergence/blob/main/SPEC.md
7
+ Project-URL: Whitepaper, https://github.com/Sharathvc23/sm-divergence/blob/main/WHITEPAPER.md
8
+ Author: Sharath (Stellarminds)
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: accountability,ai-agents,discovery,federation,nanda,registry
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: httpx>=0.24
14
+ Requires-Dist: sm-resolver>=0.1
15
+ Provides-Extra: attestation
16
+ Requires-Dist: base58>=2.1; extra == 'attestation'
17
+ Requires-Dist: cryptography>=42; extra == 'attestation'
18
+ Requires-Dist: jcs>=0.2.1; extra == 'attestation'
19
+ Provides-Extra: dev
20
+ Requires-Dist: base58>=2.1; extra == 'dev'
21
+ Requires-Dist: cryptography>=42; extra == 'dev'
22
+ Requires-Dist: jcs>=0.2.1; extra == 'dev'
23
+ Requires-Dist: mypy>=1.10; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # sm-divergence — catch a cheating registry by corroboration
30
+
31
+ **Integrity for the sources agents resolve through. A registry (or a name
32
+ service, or a DID method) can lie by omission, tampering, or equivocation;
33
+ signing an artifact closes only tampering. This asks several sources the same
34
+ questions and makes any disagreement loud.**
35
+
36
+ Agents reach each other through middlemen — registries, indexes, DID methods —
37
+ and a middleman can lie:
38
+
39
+ - **tamper** — serve a false endpoint (or key) for an agent,
40
+ - **omit** — hide an agent that is actually registered,
41
+ - **equivocate** — tell different clients different things.
42
+
43
+ A self-certifying artifact (a signed AgentFacts-style record, a `did:key`)
44
+ defeats tampering — it can't be altered without breaking its signature. But *no
45
+ signature can prove what a source chose **not** to serve.* Omission and
46
+ equivocation are invisible to a single source.
47
+
48
+ The cheap, robust defense is one decentralized discovery already hands you:
49
+ **corroboration.** If an agent is reachable through two or more sources, ask all
50
+ of them the same question and treat any disagreement as a signal. That is all
51
+ this library does — enough to turn a silently cheating source into a loudly
52
+ diverging one. See the [whitepaper](WHITEPAPER.md) for the argument and the
53
+ [spec](SPEC.md) for the normative procedure.
54
+
55
+ > **Not a transparency log.** Corroboration catches a source that disagrees with
56
+ > its peers; it does not stop a *single* source lying *consistently* to a
57
+ > first-time caller. That last mile is transparency-log territory (WHITEPAPER §7).
58
+
59
+ ## Where it fits
60
+
61
+ A **DNS/CA-pillar** primitive in the [Project NANDA](https://projectnanda.org)
62
+ four-pillar model (DNS / CA / Orchestration / Attestation): it protects the
63
+ integrity of *discovery and identity resolution themselves*. NANDA's discovery is
64
+ multi-source **by design** — a lean Index delegating to a quilt of registries —
65
+ and corroboration is only *possible* because of it: the redundancy built for
66
+ resilience doubles as an integrity check. This library turns that latent property
67
+ into an active one, the concrete mechanism behind **accountable discovery** — a
68
+ federated registry that is not merely decentralized but *auditable*.
69
+
70
+ ## Install
71
+
72
+ ```bash
73
+ pip install sm-divergence # omission + endpoint/key checks (httpx only)
74
+ pip install "sm-divergence[attestation]" # + verified-DID checks (Ed25519/did:key)
75
+ ```
76
+
77
+ ## Quick start
78
+
79
+ ```python
80
+ import asyncio
81
+ from sm_divergence import check_once
82
+
83
+ findings = asyncio.run(check_once(
84
+ ["https://registry-a.example", "https://registry-b.example"],
85
+ ["agent-1", "agent-2"],
86
+ ))
87
+ for f in findings:
88
+ print(f.kind, f.agent_id, f.detail)
89
+ # endpoint agent-1 {'field': 'endpoint',
90
+ # 'values': {'https://registry-a.example': 'https://real.example',
91
+ # 'https://registry-b.example': 'https://attacker.example'}}
92
+ ```
93
+
94
+ Long-running? Keep a detector and route new findings to your alert sink — it
95
+ deduplicates, so a persisting divergence fires your callback once:
96
+
97
+ ```python
98
+ from sm_divergence import DivergenceDetector
99
+
100
+ detector = DivergenceDetector(
101
+ ["https://registry-a.example", "https://registry-b.example"],
102
+ on_finding=lambda f: alert(f.kind, f.agent_id, f.detail),
103
+ )
104
+ await detector.check(watch_ids=my_agent_ids) # call each poll cycle
105
+ ```
106
+
107
+ ## CLI
108
+
109
+ ```bash
110
+ python -m sm_divergence check \
111
+ --registry https://registry-a.example \
112
+ --registry https://registry-b.example \
113
+ --watch agent-1 --watch agent-2
114
+ ```
115
+
116
+ Exit **0** when the sources agree, **2** when any divergence is found (so a cron
117
+ or CI step fails loudly), **1** on a usage error. Add `--json` for machine
118
+ output, `--attestation` to also compare verified DIDs, `--nanda-index URL`
119
+ beside `--registry URL` to corroborate a NANDA Index.
120
+
121
+ ## What it compares
122
+
123
+ Every finding is `omission` (present on one source, positively absent on another)
124
+ or the name of a view field whose values disagree. The detail is uniform:
125
+ `{"field": <name>, "values": {source: value}}` (or `{"present_on", "missing_from"}`
126
+ for omission), so a consumer parses findings the same way at every layer.
127
+
128
+ | Kind | Meaning |
129
+ | --- | --- |
130
+ | `omission` | One source serves the id; another **positively** reports it absent (404 / soft-404). An *unreachable* source is excluded — a timeout is not a claim. |
131
+ | `endpoint` | Discovery sources disagree on an agent's endpoint. |
132
+ | `did` | Discovery sources with a **verified** attestation disagree on the key. One valid record + one forged one is *not* a disagreement — only proven DIDs count. |
133
+ | `key` | Identity sources (DID methods) disagree on the agent's verification key. |
134
+ | `agent_equivocation` | The agent's *own key* signed two different self-descriptions at the same `seq` — key compromise or a lying agent (the one finding about the agent, not a source). |
135
+ | `stale_description` | A source serves a self-description behind the highest `seq` (replay / lag). |
136
+
137
+ ## Architecture — a kernel and its layers
138
+
139
+ The primitive factors into a source-agnostic **kernel** and a **layer** per thing
140
+ a middleman can be asked. Each layer supplies its own view and thin resolvers;
141
+ the kernel is untouched.
142
+
143
+ ```
144
+ sm-resolver (dependency) the source-agnostic kernel — Resolver[T], the View
145
+ contract, diff_views, Corroborator, Finding
146
+ sm_divergence/
147
+ discovery/ registry-integrity layer — RecordView + HttpByIdResolver (NEST)
148
+ + NandaIndexV2Resolver
149
+ identity/ key-consistency layer — DidView + DidKeyResolver / DidWebResolver
150
+ / UniversalResolverResolver + signed self-description
151
+ ```
152
+
153
+ The kernel now lives in its own package,
154
+ [`sm-resolver`](https://github.com/Sharathvc23/sm-resolver) — `sm-divergence` is
155
+ the reference set of layers built on it. Everything below is re-exported from the
156
+ top level, so `from sm_divergence import Corroborator, Finding, …` is unchanged.
157
+
158
+ **Sources of different formats mix in one run.** A `Resolver` hides one source's
159
+ wire format and normalizes its answer to the same view the diff understands:
160
+
161
+ ```python
162
+ from sm_divergence import DivergenceDetector, HttpByIdResolver, NandaIndexV2Resolver
163
+
164
+ await DivergenceDetector([
165
+ HttpByIdResolver("https://nest.example"), # GET /api/agents/{id}
166
+ NandaIndexV2Resolver("https://index.example"), # two-hop resolve → record
167
+ ]).check(["agent-1"])
168
+ ```
169
+
170
+ **Identity layer** — catches a `did:web` (or a Universal Resolver) serving a
171
+ different verification key than the agent's self-certifying `did:key` root, a
172
+ `key` divergence (the identity analogue of a tampered endpoint):
173
+
174
+ ```python
175
+ from sm_divergence import Corroborator, DidKeyResolver, DidWebResolver
176
+
177
+ await Corroborator([
178
+ DidKeyResolver(did_for=lambda a: agent_did_key[a]), # offline root of truth
179
+ DidWebResolver(did_for=lambda a: agent_did_web[a]), # fetched, may be hijacked
180
+ ]).check(agent_ids)
181
+ ```
182
+
183
+ **A new format or layer is a thin adapter** — a view with a `comparable()` and a
184
+ `Resolver` that returns it; the generic `Corroborator` + `diff_views` do the rest:
185
+
186
+ ```python
187
+ from collections.abc import Mapping
188
+ from dataclasses import dataclass
189
+ from sm_divergence import Corroborator
190
+
191
+ @dataclass(frozen=True)
192
+ class MyView:
193
+ field: str | None = None
194
+ def comparable(self) -> Mapping[str, str | None]:
195
+ return {"field": self.field}
196
+
197
+ class MyResolver:
198
+ label = "src-a"
199
+ async def resolve(self, agent_id): ... # -> (Status, MyView | None)
200
+
201
+ await Corroborator([MyResolver(), ...]).check(agent_ids)
202
+ ```
203
+
204
+ **Signed correspondence** — the same agent is a bare id on one registry, a URN
205
+ locator on an Index, a `did:web` on a domain, and corroboration can't infer that
206
+ these are the same agent. Instead of trusting a caller-supplied mapping, let the
207
+ agent *sign* its own aliases + endpoints; verify offline and drive the resolvers
208
+ from the verified names, so a source that disagrees is contradicting the agent's
209
+ own signature:
210
+
211
+ ```python
212
+ from sm_divergence import (
213
+ build_self_description, HttpDescriptionResolver, corroborate_descriptions,
214
+ signed_alias_for, DidWebResolver, AliasClosureResolver, binds_by_key,
215
+ )
216
+
217
+ # the agent signs (bumping seq on every re-issue), with its did:key's private key:
218
+ bundle = build_self_description(
219
+ aliases={"web": "did:web:acme.org", "nanda": "urn:ai:…:acme"},
220
+ services={"discovery": "https://acme.example/agents/acme"},
221
+ private_key=agent_ed25519_private_bytes, seq=5,
222
+ )
223
+
224
+ # a consumer fetches the description from EVERY source and reconciles it —
225
+ # omission / replay / agent-equivocation on the description itself become findings:
226
+ recs = await corroborate_descriptions(
227
+ [HttpDescriptionResolver(s) for s in source_urls], ["acme"],
228
+ )
229
+ canonical = recs["acme"].canonical # None if the agent equivocates
230
+ sds = {"acme": canonical} if canonical else {}
231
+
232
+ # drive resolvers from the PROVEN aliases, with closure so a claimed alias that
233
+ # doesn't bind back to the did:key contributes no claim (never a false divergence):
234
+ web = DidWebResolver(did_for=signed_alias_for(sds, "web"))
235
+ web = AliasClosureResolver(web, lambda a: sds[a].did if a in sds else None, binds_by_key)
236
+ ```
237
+
238
+ > The kernel lives in [`sm-resolver`](https://github.com/Sharathvc23/sm-resolver);
239
+ > `sm-divergence` is the reference layers built on it. The top-level public API is
240
+ > unchanged.
241
+
242
+ For verified-DID comparison over signed records shaped
243
+ `{"record": {..., "did": "did:key:z…", "endpoint": …}, "sig": "<b64 ed25519 over JCS(record)>"}`,
244
+ use the built-in adapter (needs the `attestation` extra):
245
+
246
+ ```python
247
+ from sm_divergence import DivergenceDetector, signed_record_adapter
248
+ DivergenceDetector(registries, adapter=signed_record_adapter())
249
+ ```
250
+
251
+ ## Related packages
252
+
253
+ | Package | Role |
254
+ | --- | --- |
255
+ | [`sm-bridge`](https://github.com/Sharathvc23/sm-bridge) | NANDA-compatible registry endpoints + Quilt-style deltas — *produces* the multi-source surface this corroborates across. |
256
+ | [`sm-chapter`](https://github.com/Sharathvc23/sm-chapter) | Minimal registry/discovery server — a natural *consumer* that runs a divergence check each cycle. |
257
+ | [`sm-conformance`](https://github.com/Sharathvc23/sm-conformance) | The shared Ed25519 / JCS signing convention this library's optional DID check verifies. |
258
+
259
+ ## Develop
260
+
261
+ ```bash
262
+ make ci-local # uv: sync → ruff → format → mypy --strict → pytest
263
+ ```
264
+
265
+ ## License
266
+
267
+ [MIT](LICENSE)
268
+
269
+ ---
270
+
271
+ *First published: 2026-07-04 | Last modified: 2026-07-04*
272
+
273
+ *Personal research contributions aligned with [Project NANDA](https://projectnanda.org) standards. [Stellarminds.ai](https://stellarminds.ai)*