sm-resolver 0.1.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.
- sm_resolver-0.1.0/.github/workflows/ci.yml +28 -0
- sm_resolver-0.1.0/.github/workflows/release.yml +48 -0
- sm_resolver-0.1.0/.gitignore +14 -0
- sm_resolver-0.1.0/CHANGELOG.md +23 -0
- sm_resolver-0.1.0/CONTRIBUTING.md +24 -0
- sm_resolver-0.1.0/GOVERNANCE.md +22 -0
- sm_resolver-0.1.0/LICENSE +21 -0
- sm_resolver-0.1.0/Makefile +10 -0
- sm_resolver-0.1.0/PKG-INFO +111 -0
- sm_resolver-0.1.0/PUBLISHING.md +42 -0
- sm_resolver-0.1.0/README.md +92 -0
- sm_resolver-0.1.0/SPEC.md +66 -0
- sm_resolver-0.1.0/WHITEPAPER.md +118 -0
- sm_resolver-0.1.0/examples/__init__.py +0 -0
- sm_resolver-0.1.0/examples/corroborate_sources.py +58 -0
- sm_resolver-0.1.0/examples/custom_layer.py +48 -0
- sm_resolver-0.1.0/pyproject.toml +42 -0
- sm_resolver-0.1.0/sm_resolver/__init__.py +39 -0
- sm_resolver-0.1.0/sm_resolver/corroborate.py +77 -0
- sm_resolver-0.1.0/sm_resolver/diff.py +71 -0
- sm_resolver-0.1.0/sm_resolver/models.py +38 -0
- sm_resolver-0.1.0/sm_resolver/py.typed +0 -0
- sm_resolver-0.1.0/sm_resolver/resolver.py +37 -0
- sm_resolver-0.1.0/sm_resolver/view.py +28 -0
- sm_resolver-0.1.0/tests/_helpers.py +36 -0
- sm_resolver-0.1.0/tests/test_corroborate.py +98 -0
- sm_resolver-0.1.0/tests/test_diff.py +62 -0
- sm_resolver-0.1.0/tests/test_models.py +22 -0
- sm_resolver-0.1.0/uv.lock +322 -0
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
- run: uv sync --extra dev
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run mypy sm_resolver
|
|
24
|
+
- run: uv run pytest -v
|
|
25
|
+
- name: Examples run
|
|
26
|
+
run: |
|
|
27
|
+
uv run python examples/corroborate_sources.py
|
|
28
|
+
uv run python examples/custom_layer.py
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Publish sm-resolver 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 and the exact values to type into PyPI's "Add a publisher" form.
|
|
7
|
+
#
|
|
8
|
+
# Fires only when a tag like `v0.1.0` is pushed — never on normal commits.
|
|
9
|
+
name: release
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
push:
|
|
13
|
+
tags:
|
|
14
|
+
- "v*"
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
publish:
|
|
21
|
+
name: Build + publish to PyPI
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
# A job-level permissions block REPLACES the workflow-level one (it does not
|
|
25
|
+
# merge), so contents:read must be restated here or actions/checkout 404s on
|
|
26
|
+
# its own repo. Both lines are required.
|
|
27
|
+
contents: read # for actions/checkout
|
|
28
|
+
id-token: write # REQUIRED for Trusted Publishing — this is what replaces the token.
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- uses: actions/setup-python@v5
|
|
33
|
+
with:
|
|
34
|
+
python-version: "3.12"
|
|
35
|
+
|
|
36
|
+
- name: Build sdist + wheel
|
|
37
|
+
run: |
|
|
38
|
+
python -m pip install --upgrade build
|
|
39
|
+
python -m build
|
|
40
|
+
|
|
41
|
+
- name: Check the built distributions
|
|
42
|
+
run: |
|
|
43
|
+
python -m pip install --upgrade twine
|
|
44
|
+
python -m twine check dist/*
|
|
45
|
+
|
|
46
|
+
- name: Publish to PyPI
|
|
47
|
+
# No `with: password:` — the action uses the OIDC token automatically.
|
|
48
|
+
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,23 @@
|
|
|
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.1.0] — 2026-07-04
|
|
8
|
+
|
|
9
|
+
Initial release — the corroboration kernel, extracted from `sm-divergence` once a
|
|
10
|
+
second layer (identity) joined the first (discovery) as a consumer.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`View`** — the comparable-claim contract (`comparable() → named fields`).
|
|
14
|
+
- **`Resolver[T]`** — the per-source adapter protocol + `Status`.
|
|
15
|
+
- **`diff_views`** — the pure, generic diff (`omission` + per-field divergence),
|
|
16
|
+
with the uniform `{field, values}` finding detail.
|
|
17
|
+
- **`Corroborator`** — resolve-all + diff + dedupe-emit orchestration.
|
|
18
|
+
- **`Finding`** — the finding type with a stable dedup fingerprint.
|
|
19
|
+
|
|
20
|
+
Zero runtime dependencies. `make ci-local` gate (ruff / format / mypy --strict /
|
|
21
|
+
pytest + runnable examples) on Python 3.11 and 3.12. Full `sm-*` doc set —
|
|
22
|
+
README, WHITEPAPER, SPEC, GOVERNANCE, CONTRIBUTING, PUBLISHING — plus
|
|
23
|
+
`examples/` and a PyPI trusted-publishing `release.yml`.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Contributing to sm-resolver
|
|
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 updating the **spec (`SPEC.md`), the tests,
|
|
10
|
+
and the code together**. The `tests/` suite is the authoritative behavioural
|
|
11
|
+
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 green.
|
|
14
|
+
|
|
15
|
+
## House rules
|
|
16
|
+
|
|
17
|
+
- **A timeout is not a claim.** An unreachable or erroring source MUST be
|
|
18
|
+
excluded from the diff, never counted as an omission. A change here needs an
|
|
19
|
+
RFC-style PR to `SPEC.md` first.
|
|
20
|
+
- **The diff stays pure and generic.** `diff_views` is deterministic, does no
|
|
21
|
+
I/O, never raises, and never learns its layer. All I/O lives in resolvers.
|
|
22
|
+
- **Zero runtime dependencies.** The kernel is stdlib-only; keep it that way.
|
|
23
|
+
- **No expansion of the classification (SPEC §2), the view contract (SPEC §3), or
|
|
24
|
+
the diff (SPEC §4) without an RFC-style PR to `SPEC.md` first.**
|
|
@@ -0,0 +1,22 @@
|
|
|
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 pure diff (`omission` + per-field), and the `Corroborator` orchestration | any specific source format or transport (a consumer's resolver owns that); the view type's fields (the consumer defines them); signing / verification (a consumer's resolver applies it before returning a view); a transparency log; and remediation policy on a finding |
|
|
8
|
+
|
|
9
|
+
The kernel owns one thing — answering *"do these sources disagree about this
|
|
10
|
+
subject, and how?"* — for any view type and any layer. Anything outside the table
|
|
11
|
+
belongs to a consumer (e.g. `sm-divergence`) or the caller.
|
|
12
|
+
|
|
13
|
+
## Versioning
|
|
14
|
+
|
|
15
|
+
Semantic Versioning 2.0.0. The claim classification (SPEC §2), the view contract
|
|
16
|
+
(SPEC §3), and the diff (SPEC §4) are frozen within a major; a change requires an
|
|
17
|
+
RFC-style PR to `SPEC.md` before code.
|
|
18
|
+
|
|
19
|
+
## Conformance
|
|
20
|
+
|
|
21
|
+
An implementation is conforming iff it reproduces the finding set in SPEC §6. The
|
|
22
|
+
diff is pure and deterministic by construction, so conformance is mechanical.
|
|
@@ -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,10 @@
|
|
|
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_resolver
|
|
8
|
+
uv run pytest -v
|
|
9
|
+
uv run python examples/corroborate_sources.py
|
|
10
|
+
uv run python examples/custom_layer.py
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sm-resolver
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The source-agnostic corroboration kernel — Resolver[T], the View contract, a pure diff, and the Corroborator. Zero runtime dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sharathvc23/sm-resolver
|
|
6
|
+
Project-URL: Spec, https://github.com/Sharathvc23/sm-resolver/blob/main/SPEC.md
|
|
7
|
+
Project-URL: Whitepaper, https://github.com/Sharathvc23/sm-resolver/blob/main/WHITEPAPER.md
|
|
8
|
+
Author: Sharath (Stellarminds)
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: accountability,ai-agents,corroboration,divergence,nanda,resolver
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# sm-resolver — the corroboration kernel
|
|
21
|
+
|
|
22
|
+
**A tiny, dependency-free kernel for detecting when independent sources disagree
|
|
23
|
+
about the same subject.** Point it at two or more sources, ask them the same
|
|
24
|
+
question, and it reports any disagreement. It is the machinery under
|
|
25
|
+
cross-registry / cross-method divergence detection, factored out so any layer can
|
|
26
|
+
reuse it.
|
|
27
|
+
|
|
28
|
+
Four pieces, and only the resolvers know a wire format:
|
|
29
|
+
|
|
30
|
+
- **`View`** — the contract a claim implements: `comparable() → {field: value}`.
|
|
31
|
+
Those fields are what gets compared; a `None` value never participates.
|
|
32
|
+
- **`Resolver[T]`** — a per-source adapter: a canonical id → `(Status, View)`.
|
|
33
|
+
It hides one source's format (an HTTP GET, a DID resolve, a DNS lookup) and
|
|
34
|
+
MUST NOT raise — an unreachable source is `error` (no claim), never a false
|
|
35
|
+
`absent`.
|
|
36
|
+
- **`diff_views`** — the pure diff: per-source claims → `Finding` list. It emits
|
|
37
|
+
`omission` (present on one source, positively absent on another) and one
|
|
38
|
+
finding per view field whose values disagree. It never learns its layer.
|
|
39
|
+
- **`Corroborator`** — resolve every source, diff, and emit new findings once
|
|
40
|
+
(deduped). Fewer than two sources → no-op.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install sm-resolver # zero runtime dependencies
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Use
|
|
49
|
+
|
|
50
|
+
Supply a view (its fields are what you compare) and a thin resolver per source:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
from collections.abc import Mapping
|
|
55
|
+
from dataclasses import dataclass
|
|
56
|
+
from sm_resolver import Corroborator, Status
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class RecordView: # your layer's view
|
|
60
|
+
endpoint: str | None = None
|
|
61
|
+
def comparable(self) -> Mapping[str, str | None]:
|
|
62
|
+
return {"endpoint": self.endpoint}
|
|
63
|
+
|
|
64
|
+
class MyResolver: # your thin per-source adapter
|
|
65
|
+
def __init__(self, label): self.label = label
|
|
66
|
+
async def resolve(self, agent_id) -> tuple[Status, RecordView | None]:
|
|
67
|
+
... # query this source; normalize to RecordView
|
|
68
|
+
|
|
69
|
+
findings = asyncio.run(
|
|
70
|
+
Corroborator([MyResolver("a"), MyResolver("b")]).check(["agent-1"])
|
|
71
|
+
)
|
|
72
|
+
for f in findings:
|
|
73
|
+
print(f.kind, f.agent_id, f.detail)
|
|
74
|
+
# endpoint agent-1 {'field': 'endpoint', 'values': {'a': 'https://real', 'b': 'https://evil'}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Long-running? Pass `on_finding=` — it fires once per distinct finding across the
|
|
78
|
+
corroborator's lifetime.
|
|
79
|
+
|
|
80
|
+
## Who uses it
|
|
81
|
+
|
|
82
|
+
[`sm-divergence`](https://github.com/Sharathvc23/sm-divergence) builds the
|
|
83
|
+
reference layers on this kernel: a **discovery** layer (registries — NEST, the
|
|
84
|
+
NANDA Index) and an **identity** layer (DID methods — `did:key`, `did:web`,
|
|
85
|
+
Universal Resolver), each supplying its own view and thin resolvers. Capability
|
|
86
|
+
and evidence layers are the same shape.
|
|
87
|
+
|
|
88
|
+
## Design rules
|
|
89
|
+
|
|
90
|
+
- **A timeout is not a claim.** An unreachable or erroring source is excluded,
|
|
91
|
+
never counted as an omission.
|
|
92
|
+
- **Only present, comparable values disagree.** A `None` field contributes
|
|
93
|
+
nothing — an unverifiable value is not a disagreement.
|
|
94
|
+
- **The diff stays pure and generic** — deterministic, no I/O, never raises,
|
|
95
|
+
never learns its layer. All I/O lives in the resolvers.
|
|
96
|
+
|
|
97
|
+
## Develop
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
make ci-local # uv: sync → ruff → format → mypy --strict → pytest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
[MIT](LICENSE)
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
*First published: 2026-07-04 | Last modified: 2026-07-04*
|
|
110
|
+
|
|
111
|
+
*Personal research contributions aligned with [Project NANDA](https://projectnanda.org) standards. [Stellarminds.ai](https://stellarminds.ai)*
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Publishing sm-resolver to PyPI
|
|
2
|
+
|
|
3
|
+
`sm-resolver` publishes via **PyPI Trusted Publishing** — no API tokens. You tell
|
|
4
|
+
PyPI once that this repo's `release.yml` workflow may publish; after that, pushing
|
|
5
|
+
a version tag builds and uploads automatically.
|
|
6
|
+
|
|
7
|
+
## One-time setup (≈5 minutes — this is the part only you can do)
|
|
8
|
+
|
|
9
|
+
1. Sign in to PyPI (the same account that publishes the rest of the `sm-*` family).
|
|
10
|
+
2. Go to **https://pypi.org/manage/account/publishing/** → "Add a pending
|
|
11
|
+
publisher" (use *pending*, because the `sm-resolver` project doesn't exist on
|
|
12
|
+
PyPI yet — the first publish creates it).
|
|
13
|
+
3. Fill the form with **exactly** these values:
|
|
14
|
+
|
|
15
|
+
| Field | Value |
|
|
16
|
+
|-------|-------|
|
|
17
|
+
| PyPI Project Name | `sm-resolver` |
|
|
18
|
+
| Owner | `Sharathvc23` |
|
|
19
|
+
| Repository name | `sm-resolver` |
|
|
20
|
+
| **Workflow name** | `release.yml` ← just the filename |
|
|
21
|
+
| Environment name | *(leave blank)* |
|
|
22
|
+
|
|
23
|
+
That's it. No secret is created or stored anywhere.
|
|
24
|
+
|
|
25
|
+
## Releasing (every time, after setup)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# bump `version` in pyproject.toml + update CHANGELOG, commit, then:
|
|
29
|
+
git tag v0.1.0
|
|
30
|
+
git push origin v0.1.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The `release` workflow builds the sdist + wheel, runs `twine check`, and uploads
|
|
34
|
+
to PyPI over OIDC. Watch it under the repo's **Actions** tab. Within a minute,
|
|
35
|
+
`pip install sm-resolver` works for everyone.
|
|
36
|
+
|
|
37
|
+
## Notes
|
|
38
|
+
|
|
39
|
+
- The tag (`v0.1.0`) and `version` in `pyproject.toml` must match.
|
|
40
|
+
- Dry run anytime, no upload: `python -m build && python -m twine check dist/*`.
|
|
41
|
+
- Making this public also unblocks `sm-divergence`'s CI, which depends on this
|
|
42
|
+
package (a private dependency otherwise needs a PAT to clone in Actions).
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# sm-resolver — the corroboration kernel
|
|
2
|
+
|
|
3
|
+
**A tiny, dependency-free kernel for detecting when independent sources disagree
|
|
4
|
+
about the same subject.** Point it at two or more sources, ask them the same
|
|
5
|
+
question, and it reports any disagreement. It is the machinery under
|
|
6
|
+
cross-registry / cross-method divergence detection, factored out so any layer can
|
|
7
|
+
reuse it.
|
|
8
|
+
|
|
9
|
+
Four pieces, and only the resolvers know a wire format:
|
|
10
|
+
|
|
11
|
+
- **`View`** — the contract a claim implements: `comparable() → {field: value}`.
|
|
12
|
+
Those fields are what gets compared; a `None` value never participates.
|
|
13
|
+
- **`Resolver[T]`** — a per-source adapter: a canonical id → `(Status, View)`.
|
|
14
|
+
It hides one source's format (an HTTP GET, a DID resolve, a DNS lookup) and
|
|
15
|
+
MUST NOT raise — an unreachable source is `error` (no claim), never a false
|
|
16
|
+
`absent`.
|
|
17
|
+
- **`diff_views`** — the pure diff: per-source claims → `Finding` list. It emits
|
|
18
|
+
`omission` (present on one source, positively absent on another) and one
|
|
19
|
+
finding per view field whose values disagree. It never learns its layer.
|
|
20
|
+
- **`Corroborator`** — resolve every source, diff, and emit new findings once
|
|
21
|
+
(deduped). Fewer than two sources → no-op.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install sm-resolver # zero runtime dependencies
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Use
|
|
30
|
+
|
|
31
|
+
Supply a view (its fields are what you compare) and a thin resolver per source:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import asyncio
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from sm_resolver import Corroborator, Status
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class RecordView: # your layer's view
|
|
41
|
+
endpoint: str | None = None
|
|
42
|
+
def comparable(self) -> Mapping[str, str | None]:
|
|
43
|
+
return {"endpoint": self.endpoint}
|
|
44
|
+
|
|
45
|
+
class MyResolver: # your thin per-source adapter
|
|
46
|
+
def __init__(self, label): self.label = label
|
|
47
|
+
async def resolve(self, agent_id) -> tuple[Status, RecordView | None]:
|
|
48
|
+
... # query this source; normalize to RecordView
|
|
49
|
+
|
|
50
|
+
findings = asyncio.run(
|
|
51
|
+
Corroborator([MyResolver("a"), MyResolver("b")]).check(["agent-1"])
|
|
52
|
+
)
|
|
53
|
+
for f in findings:
|
|
54
|
+
print(f.kind, f.agent_id, f.detail)
|
|
55
|
+
# endpoint agent-1 {'field': 'endpoint', 'values': {'a': 'https://real', 'b': 'https://evil'}}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Long-running? Pass `on_finding=` — it fires once per distinct finding across the
|
|
59
|
+
corroborator's lifetime.
|
|
60
|
+
|
|
61
|
+
## Who uses it
|
|
62
|
+
|
|
63
|
+
[`sm-divergence`](https://github.com/Sharathvc23/sm-divergence) builds the
|
|
64
|
+
reference layers on this kernel: a **discovery** layer (registries — NEST, the
|
|
65
|
+
NANDA Index) and an **identity** layer (DID methods — `did:key`, `did:web`,
|
|
66
|
+
Universal Resolver), each supplying its own view and thin resolvers. Capability
|
|
67
|
+
and evidence layers are the same shape.
|
|
68
|
+
|
|
69
|
+
## Design rules
|
|
70
|
+
|
|
71
|
+
- **A timeout is not a claim.** An unreachable or erroring source is excluded,
|
|
72
|
+
never counted as an omission.
|
|
73
|
+
- **Only present, comparable values disagree.** A `None` field contributes
|
|
74
|
+
nothing — an unverifiable value is not a disagreement.
|
|
75
|
+
- **The diff stays pure and generic** — deterministic, no I/O, never raises,
|
|
76
|
+
never learns its layer. All I/O lives in the resolvers.
|
|
77
|
+
|
|
78
|
+
## Develop
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
make ci-local # uv: sync → ruff → format → mypy --strict → pytest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
[MIT](LICENSE)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
*First published: 2026-07-04 | Last modified: 2026-07-04*
|
|
91
|
+
|
|
92
|
+
*Personal research contributions aligned with [Project NANDA](https://projectnanda.org) standards. [Stellarminds.ai](https://stellarminds.ai)*
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Corroboration Kernel — Procedure
|
|
2
|
+
|
|
3
|
+
**Spec version:** `resolver/0.1-draft`
|
|
4
|
+
**Status:** DRAFT / Working Draft
|
|
5
|
+
**Last edited:** 2026-07-04
|
|
6
|
+
|
|
7
|
+
The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, MAY are to be interpreted as in RFC 2119.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 1. Introduction
|
|
12
|
+
|
|
13
|
+
Given several **sources** that each answer "what do you have for subject *X*?",
|
|
14
|
+
the kernel classifies each source's claim, reduces a present claim to a
|
|
15
|
+
comparable **view**, and diffs the views into findings. It is source-, format-,
|
|
16
|
+
and layer-agnostic; a consumer supplies a view type and a resolver per source.
|
|
17
|
+
|
|
18
|
+
## 2. Claim classification
|
|
19
|
+
|
|
20
|
+
For a source *S* and subject *A*, a resolver MUST resolve exactly one of:
|
|
21
|
+
|
|
22
|
+
- **`present`** — *S* served a claim, reduced to a view (§3).
|
|
23
|
+
- **`absent`** — *S* positively asserts *A* is unknown.
|
|
24
|
+
- **`error`** — any other outcome (unreachable, timeout, unparseable, or a claim
|
|
25
|
+
the resolver cannot map to a view).
|
|
26
|
+
|
|
27
|
+
An `error` claim MUST be excluded from the diff entirely. It is NOT `absent`: **a
|
|
28
|
+
source that failed to answer has asserted nothing, and MUST NOT be treated as
|
|
29
|
+
claiming the subject is absent.** A resolver MUST NOT raise; a failure is `error`.
|
|
30
|
+
|
|
31
|
+
## 3. The view contract
|
|
32
|
+
|
|
33
|
+
A `present` claim MUST be reduced to a **view**: an object exposing
|
|
34
|
+
`comparable() → Mapping[field_name, str | None]`. The keys are the fields that
|
|
35
|
+
participate in the diff; a value of `None` never participates (an unverifiable or
|
|
36
|
+
missing field is not a disagreement). The view type is the consumer's choice.
|
|
37
|
+
|
|
38
|
+
## 4. The diff
|
|
39
|
+
|
|
40
|
+
Input is `views[source][subject]`: a view (present), `null` (absent), or *absent
|
|
41
|
+
from the mapping* (the `error` case — no claim). Per subject:
|
|
42
|
+
|
|
43
|
+
- **`omission`** — emit iff present on ≥1 source AND absent on ≥1 other (both
|
|
44
|
+
positive claims). Detail: `{present_on, missing_from}` (sorted source lists).
|
|
45
|
+
- **field divergence** — for each field name in any present view's
|
|
46
|
+
`comparable()`, collect the non-`None` values keyed by source; emit a finding
|
|
47
|
+
of `kind = <field name>` iff more than one distinct value appears. Detail:
|
|
48
|
+
`{"field": <name>, "values": {source: value}}`.
|
|
49
|
+
|
|
50
|
+
The diff MUST be pure: deterministic, no I/O, no raising. A subject MAY produce
|
|
51
|
+
multiple findings.
|
|
52
|
+
|
|
53
|
+
## 5. Corroborator
|
|
54
|
+
|
|
55
|
+
A `Corroborator` takes two or more resolvers, resolves each subject against each,
|
|
56
|
+
builds the `views` mapping (excluding `error` claims), and applies §4. Fewer than
|
|
57
|
+
two sources → no-op. It SHOULD deduplicate emitted findings by a stable
|
|
58
|
+
fingerprint over `{kind, agent_id, detail}`.
|
|
59
|
+
|
|
60
|
+
## 6. Conformance
|
|
61
|
+
|
|
62
|
+
A conforming implementation MUST reproduce, for a fixed set of per-source claim
|
|
63
|
+
inputs, the §4 finding set: agreement → none; present + absent → `omission`;
|
|
64
|
+
distinct field values → a finding of that field's kind; single/identical value or
|
|
65
|
+
one-value-beside-`None` → none; `error`/unreachable → excluded (no `omission`).
|
|
66
|
+
The diff is pure and deterministic, so conformance is mechanical.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# sm-resolver: A Corroboration Kernel for the Internet of AI Agents
|
|
2
|
+
|
|
3
|
+
*Personal research contribution by [Stellarminds.ai](https://stellarminds.ai), aligned with [Project NANDA](https://projectnanda.org) standards.*
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Abstract
|
|
8
|
+
|
|
9
|
+
Accountability for a federated system where agents reach each other through
|
|
10
|
+
middlemen — registries, name services, DID methods, catalogs — reduces to one
|
|
11
|
+
question asked over and over: *do independent sources agree about the same
|
|
12
|
+
subject, and if not, how do they differ?* This paper describes the minimal,
|
|
13
|
+
source-agnostic machinery that answers it: a per-source **resolver** that
|
|
14
|
+
normalizes any format to one comparable **view**, and a pure **diff** that turns
|
|
15
|
+
the views from all sources into findings. `sm-resolver` is that machinery and
|
|
16
|
+
nothing else — a dependency-free kernel other packages build layers on.
|
|
17
|
+
|
|
18
|
+
## 1. Problem
|
|
19
|
+
|
|
20
|
+
A middleman can lie: it can tamper with what it serves, omit a subject it holds,
|
|
21
|
+
or equivocate between clients. Signing an artifact defeats tampering, but no
|
|
22
|
+
signature proves what a source *withheld* or whether it showed everyone the same
|
|
23
|
+
thing. The robust defense is corroboration — ask two or more sources and treat
|
|
24
|
+
disagreement as a signal — and it recurs at every layer: a registry about an
|
|
25
|
+
agent's endpoint, a DID method about its key, a catalog about its capabilities.
|
|
26
|
+
The comparison logic is identical each time; only the wire format differs. Left
|
|
27
|
+
un-factored, every layer reimplements the same diff and the same "a timeout is
|
|
28
|
+
not an absence" subtleties, and drifts.
|
|
29
|
+
|
|
30
|
+
## 2. The corroboration primitive
|
|
31
|
+
|
|
32
|
+
The operation factors into three pieces, and only the first knows a format:
|
|
33
|
+
|
|
34
|
+
- **Resolve** — a per-source *resolver* performs that source's query, applies its
|
|
35
|
+
native verification, and returns a *view*. It is the only format-aware piece;
|
|
36
|
+
a thin adapter lives here.
|
|
37
|
+
- **View** — the comparable reduction of a claim to named string fields
|
|
38
|
+
(`comparable() → {field: value}`). A `None` value never participates.
|
|
39
|
+
- **Diff** — a pure function over the views from all sources. It emits `omission`
|
|
40
|
+
(present on one, positively absent on another) and one finding per view field
|
|
41
|
+
whose values disagree. It never learns what layer it serves.
|
|
42
|
+
|
|
43
|
+
Because the diff operates on normalized views, it is untouched by format
|
|
44
|
+
heterogeneity — the same code compares a registry claim and a DID-document claim
|
|
45
|
+
once each is a view. A `Corroborator` wires it together: resolve every source,
|
|
46
|
+
diff, emit new findings once.
|
|
47
|
+
|
|
48
|
+
## 3. Why it is a separate package
|
|
49
|
+
|
|
50
|
+
The kernel is small (three types and one pure function) but it is the part every
|
|
51
|
+
accountability layer shares, so it earns its own package the moment a second
|
|
52
|
+
layer consumes it. Extracting it keeps each layer a *thin adapter* — a view type
|
|
53
|
+
and per-source resolvers — with no generic machinery of its own, and lets a layer
|
|
54
|
+
depend on the kernel without dragging any sibling's wire code along. The kernel
|
|
55
|
+
carries **zero runtime dependencies**, so adopting it commits a consumer to
|
|
56
|
+
nothing but the standard library.
|
|
57
|
+
|
|
58
|
+
## 4. Design axioms
|
|
59
|
+
|
|
60
|
+
Consequences of the threat model, not preferences.
|
|
61
|
+
|
|
62
|
+
1. **Corroborate, don't trust.** One artifact proves its own contents, not that
|
|
63
|
+
the source served it honestly or to everyone. The minimum unit of
|
|
64
|
+
accountability is two independent sources; with one, the kernel is a no-op
|
|
65
|
+
rather than false assurance.
|
|
66
|
+
2. **A timeout is not a claim.** An unreachable source has asserted nothing and
|
|
67
|
+
is excluded — never counted as an omission — so a transient fault cannot
|
|
68
|
+
manufacture a false finding.
|
|
69
|
+
3. **Only present, comparable values disagree.** A `None` field contributes
|
|
70
|
+
nothing; an unverifiable value is not a disagreement and cannot be laundered
|
|
71
|
+
into one that discredits an honest source.
|
|
72
|
+
4. **Detect; do not remediate.** The kernel reports *that* sources disagree and
|
|
73
|
+
*how*. What to do about it is caller policy.
|
|
74
|
+
|
|
75
|
+
## 5. Composition
|
|
76
|
+
|
|
77
|
+
`sm-resolver` is the base; layers supply a view and resolvers:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
sm-resolver (View · Resolver[T] · diff · Corroborator)
|
|
81
|
+
▲ ▲ ▲
|
|
82
|
+
discovery layer identity layer capability / evidence
|
|
83
|
+
RecordView + DidView + (a view + resolvers)
|
|
84
|
+
registry resolvers DID-method resolvers
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
[`sm-divergence`](https://github.com/Sharathvc23/sm-divergence) is the reference
|
|
88
|
+
consumer: a discovery layer (registries — NEST, the NANDA Index) and an identity
|
|
89
|
+
layer (DID methods — `did:key`, `did:web`, a Universal Resolver), each a thin set
|
|
90
|
+
of resolvers over this kernel.
|
|
91
|
+
|
|
92
|
+
## 6. NANDA alignment
|
|
93
|
+
|
|
94
|
+
Project NANDA's discovery is multi-source by design — a lean Index delegating to
|
|
95
|
+
a quilt of registries. Corroboration is only *possible* because of that
|
|
96
|
+
decentralization: the redundancy built for resilience doubles as an integrity
|
|
97
|
+
check. `sm-resolver` is the mechanism that turns that latent property into an
|
|
98
|
+
active one, underpinning **accountable discovery** across the DNS and CA pillars.
|
|
99
|
+
|
|
100
|
+
## 7. Future work
|
|
101
|
+
|
|
102
|
+
- A quorum reducer over findings (*k*-of-*n* agreement) for more than two sources.
|
|
103
|
+
- Durable per-source divergence history (source reputation).
|
|
104
|
+
- A normative wire schema for a shareable `Finding` (cross-tool interchange).
|
|
105
|
+
|
|
106
|
+
## 8. Related packages
|
|
107
|
+
|
|
108
|
+
| Package | Role |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| [`sm-divergence`](https://github.com/Sharathvc23/sm-divergence) | The reference discovery + identity layers built on this kernel. |
|
|
111
|
+
| [`sm-bridge`](https://github.com/Sharathvc23/sm-bridge) | NANDA-compatible registry endpoints — a source a resolver corroborates across. |
|
|
112
|
+
| [`sm-conformance`](https://github.com/Sharathvc23/sm-conformance) | The shared Ed25519 / JCS signing convention a resolver's verification uses. |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
[MIT](LICENSE).
|
|
File without changes
|