avow 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.
Files changed (59) hide show
  1. avow-0.1.0/.gitignore +30 -0
  2. avow-0.1.0/CHANGELOG.md +30 -0
  3. avow-0.1.0/LICENSE +21 -0
  4. avow-0.1.0/PKG-INFO +195 -0
  5. avow-0.1.0/QUICKSTART.md +32 -0
  6. avow-0.1.0/README.md +163 -0
  7. avow-0.1.0/demo/run_demo.py +141 -0
  8. avow-0.1.0/demo/unification_demo.py +121 -0
  9. avow-0.1.0/pyproject.toml +163 -0
  10. avow-0.1.0/src/assay/__init__.py +17 -0
  11. avow-0.1.0/src/assay/api.py +169 -0
  12. avow-0.1.0/src/assay/calibration.py +70 -0
  13. avow-0.1.0/src/assay/cli.py +97 -0
  14. avow-0.1.0/src/assay/composite.py +101 -0
  15. avow-0.1.0/src/assay/errors.py +61 -0
  16. avow-0.1.0/src/assay/metrics.py +78 -0
  17. avow-0.1.0/src/assay/models.py +42 -0
  18. avow-0.1.0/src/assay/py.typed +0 -0
  19. avow-0.1.0/src/assay/receipt.py +112 -0
  20. avow-0.1.0/src/assay/settings.py +21 -0
  21. avow-0.1.0/src/assay/uncertainty.py +82 -0
  22. avow-0.1.0/src/avow/__init__.py +43 -0
  23. avow-0.1.0/src/avow/_version.py +5 -0
  24. avow-0.1.0/src/avow/canonical.py +33 -0
  25. avow-0.1.0/src/avow/envelope.py +90 -0
  26. avow-0.1.0/src/avow/errors.py +38 -0
  27. avow-0.1.0/src/avow/keys.py +56 -0
  28. avow-0.1.0/src/avow/ledger.py +46 -0
  29. avow-0.1.0/src/avow/py.typed +0 -0
  30. avow-0.1.0/src/avow/verify.py +22 -0
  31. avow-0.1.0/src/writ/__init__.py +35 -0
  32. avow-0.1.0/src/writ/gate.py +144 -0
  33. avow-0.1.0/src/writ/py.typed +0 -0
  34. avow-0.1.0/testdata/vectors/canonical.json +102 -0
  35. avow-0.1.0/testdata/vectors/receipts.json +38 -0
  36. avow-0.1.0/tests/gen_vectors.py +102 -0
  37. avow-0.1.0/tests/test_api.py +156 -0
  38. avow-0.1.0/tests/test_boundaries.py +131 -0
  39. avow-0.1.0/tests/test_calibration.py +39 -0
  40. avow-0.1.0/tests/test_canonical.py +34 -0
  41. avow-0.1.0/tests/test_cli.py +85 -0
  42. avow-0.1.0/tests/test_composite.py +70 -0
  43. avow-0.1.0/tests/test_demo_runs.py +24 -0
  44. avow-0.1.0/tests/test_e2e_acceptance.py +141 -0
  45. avow-0.1.0/tests/test_envelope_split.py +73 -0
  46. avow-0.1.0/tests/test_errors.py +56 -0
  47. avow-0.1.0/tests/test_keys.py +38 -0
  48. avow-0.1.0/tests/test_ledger.py +55 -0
  49. avow-0.1.0/tests/test_metrics.py +47 -0
  50. avow-0.1.0/tests/test_models.py +47 -0
  51. avow-0.1.0/tests/test_packaging.py +44 -0
  52. avow-0.1.0/tests/test_receipt.py +111 -0
  53. avow-0.1.0/tests/test_settings.py +24 -0
  54. avow-0.1.0/tests/test_smoke.py +19 -0
  55. avow-0.1.0/tests/test_uncertainty.py +41 -0
  56. avow-0.1.0/tests/test_unification_demo_runs.py +24 -0
  57. avow-0.1.0/tests/test_vectors.py +48 -0
  58. avow-0.1.0/tests/test_verify.py +85 -0
  59. avow-0.1.0/tests/test_writ.py +125 -0
avow-0.1.0/.gitignore ADDED
@@ -0,0 +1,30 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .coverage
9
+ coverage.xml
10
+ coverage.json
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+ .DS_Store
15
+ .env
16
+
17
+ # Signing key material — defense-in-depth. `assay keygen` writes a real
18
+ # private signing key; never let one be committed. Public verify keys shipped
19
+ # for demos live under demo/ and are added explicitly.
20
+ private.key
21
+ signing.key
22
+ *.key
23
+ *.pem
24
+ keys/
25
+
26
+ # Generated by the weekly security-audit workflow (uv export target)
27
+ requirements-audit.txt
28
+
29
+ # Serena MCP tool scratch
30
+ .serena/
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-07-19
4
+ ### Changed
5
+ - **Repackaged as distribution `avow`** exposing three top-level import packages:
6
+ `avow` (the shared trust envelope), `assay` (scoring face, `import avow`), and
7
+ `writ` (effect face, `import avow`). The dependency edges `assay → avow` and
8
+ `writ → avow` are import edges inside one wheel; `avow` imports neither.
9
+ - **Extras split:** base install is the envelope only (pydantic, pydantic-settings,
10
+ pynacl, rfc8785); the scoring science stack moved behind `avow[assay]`
11
+ (scikit-learn, scipy, numpy) and the Typer CLI behind `avow[cli]`. `import assay`
12
+ without the extra raises coded `ScoringExtraMissing`.
13
+ - **Envelope error catalog** moved to `avow.errors` with `avow.*` codes under
14
+ `AvowError`; scoring codes stay `assay.*` under `AssayError` (both re-exported from
15
+ `assay.errors` for a single import site). `avow.ledger` is now subject-generic.
16
+
17
+ ### Added
18
+ - Cross-language golden vectors (`testdata/vectors/`) generated by
19
+ `tests/gen_vectors.py`, replayed by `tests/test_vectors.py`, to pin RFC 8785 byte
20
+ identity for a future `@edgeproc/avow` TypeScript Lego.
21
+
22
+ ## [0.0.1] - 2026-07-19
23
+ ### Added
24
+ - Deterministic v0 scoring engine: metrics, calibration (ECE/Brier/reliability),
25
+ bootstrap uncertainty with abstention floor, weighted multi-scale composite.
26
+ - Signed, offline-verifiable `ScoreReceipt` (RFC 8785 JCS + SHA-256 + Ed25519),
27
+ built on a payload-agnostic trust envelope that a future effect-face can reuse.
28
+ - Append-only content-addressed ledger with integrity check.
29
+ - Typer CLI (`keygen`, `score`, `composite`, `verify`) and a demo proving all six
30
+ acceptance cases.
avow-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Harish Seshadri
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.
avow-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: avow
3
+ Version: 0.1.0
4
+ Summary: The trust kernel: one signed, offline-verifiable receipt envelope (avow) with a scoring face (assay) and an effect-gate face (writ).
5
+ Project-URL: Homepage, https://github.com/hseshadr/assay
6
+ Project-URL: Repository, https://github.com/hseshadr/assay
7
+ Project-URL: Issues, https://github.com/hseshadr/assay/issues
8
+ Author-email: Harish Seshadri <harish.seshadri@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.13
18
+ Requires-Dist: pydantic-settings>=2.11
19
+ Requires-Dist: pydantic>=2.11
20
+ Requires-Dist: pynacl>=1.6
21
+ Requires-Dist: rfc8785>=0.1.4
22
+ Provides-Extra: assay
23
+ Requires-Dist: numpy>=2.5; extra == 'assay'
24
+ Requires-Dist: scikit-learn>=1.9; extra == 'assay'
25
+ Requires-Dist: scipy>=1.18; extra == 'assay'
26
+ Provides-Extra: cli
27
+ Requires-Dist: numpy>=2.5; extra == 'cli'
28
+ Requires-Dist: scikit-learn>=1.9; extra == 'cli'
29
+ Requires-Dist: scipy>=1.18; extra == 'cli'
30
+ Requires-Dist: typer>=0.27; extra == 'cli'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # Avow
34
+
35
+ **The trust kernel: one signed receipt, three Legos.**
36
+
37
+ Avow turns a claim into a **receipt you can check** — a small, signed record that
38
+ proves *exactly what was claimed* and fails loudly if a single byte is changed. It
39
+ ships as one installable distribution (`avow`) that exposes three import packages you
40
+ can pick and choose:
41
+
42
+ | Package | It is… | Depends on | Install |
43
+ |---|---|---|---|
44
+ | **`avow`** | the shared **envelope** — sign, hash, verify a receipt | pydantic, pynacl, rfc8785 | `pip install avow` |
45
+ | **`assay`** | the **scoring** face — an honest number, wrapped in a receipt | `avow` + scikit-learn/scipy/numpy | `pip install 'avow[assay]'` |
46
+ | **`writ`** | the **effect** face — a policy-gated action, sealed as a receipt | `avow` only | `pip install avow` |
47
+
48
+ The command-line tool lives behind `pip install 'avow[cli]'`.
49
+
50
+ > **One rule you can hold in your head:** the envelope signs the *canonical bytes of a
51
+ > frozen subject* and never looks inside. So the exact same sign/verify code carries a
52
+ > score for the scoring face and an action for the effect face. One envelope, many
53
+ > subjects. The dependency arrows only ever point **into** `avow` — `assay → avow` and
54
+ > `writ → avow`; `avow` imports neither, which is why installing the envelope alone
55
+ > never pulls the heavy science stack.
56
+
57
+ ## Why it exists
58
+
59
+ A raw claim — `f1 = 0.83`, or "I deleted account 7" — is a statement with no receipt.
60
+ You cannot tell whether the score ran on 12 examples or 12,000, or whether the action
61
+ was actually allowed. Avow wraps the claim in a **signed receipt** carrying the inputs'
62
+ content hash and an Ed25519 signature. Anyone holding the receipt can re-check it
63
+ offline — and verification fails if anything was tampered.
64
+
65
+ **The trust boundary, stated honestly.** A receipt proves *payload integrity under some
66
+ signer*. It does **not**, on its own, prove *authenticity* — the signer's public key
67
+ rides in the receipt outside the signed bytes, so an attacker can re-sign a forged
68
+ payload with their own key. Authenticity therefore requires **pinning the signer's
69
+ public key out-of-band**: you pass the key you already trust (the `.pub` from `keygen`)
70
+ to `verify`, and Avow rejects any receipt not signed by exactly that key. Never trust
71
+ the key embedded in the receipt.
72
+
73
+ ## Quickstart (copy-paste)
74
+
75
+ ```bash
76
+ # dev setup (this repo installs all faces + tooling):
77
+ uv sync --all-extras
78
+ uv run poe gate # ruff · ruff-format · mypy --strict · xenon A · pytest (100%)
79
+ uv run python demo/run_demo.py # scoring face: 6 honesty acceptance cases
80
+ uv run python demo/unification_demo.py # ONE envelope + ONE verifier, BOTH faces
81
+ ```
82
+
83
+ **Envelope only** — sign and verify any frozen subject (base install, no science stack):
84
+
85
+ ```python
86
+ from pydantic import BaseModel, ConfigDict
87
+ from avow import generate_signing_key, public_key_hex, sign_payload, verify_signature
88
+
89
+ class Claim(BaseModel):
90
+ model_config = ConfigDict(frozen=True, extra="forbid")
91
+ what: str
92
+ value: float
93
+
94
+ key = generate_signing_key()
95
+ pinned = public_key_hex(key) # pin this out-of-band
96
+ receipt = sign_payload(Claim(what="f1", value=0.83), key)
97
+ verify_signature(receipt, expected_public_key=pinned) # raises on any tamper
98
+ ```
99
+
100
+ **Scoring face** (`pip install 'avow[assay]'`) — an honest number in a receipt:
101
+
102
+ ```python
103
+ from avow import generate_signing_key, public_key_hex
104
+ from assay import score, verify
105
+ from assay.models import ScoreRequest
106
+ from assay.settings import AssaySettings
107
+
108
+ key = generate_signing_key()
109
+ request = ScoreRequest(metric="binary", metric_version="1",
110
+ y_true=(0, 1, 0, 1), y_score=(0.2, 0.8, 0.3, 0.7))
111
+ receipt = score(request, signing_key=key, settings=AssaySettings())
112
+ print(receipt.payload.score) # calibrated point — or None when the sample is too thin
113
+ assert verify(receipt, expected_public_key=public_key_hex(key)) is True
114
+ ```
115
+
116
+ **Effect face** (`writ`, base install) — a policy-gated, sealed action:
117
+
118
+ ```python
119
+ from avow import generate_signing_key, public_key_hex, verify_signature
120
+ from writ import Allowlist, EffectRequest, KeyholderEffector, governed_gate
121
+
122
+ key = generate_signing_key()
123
+ def do_it(_req: EffectRequest) -> None: ... # the privileged effect
124
+ gate = governed_gate(Allowlist(frozenset({"read"})),
125
+ KeyholderEffector(effect=do_it, signing_key=key))
126
+ receipt = gate(EffectRequest(action="read", target="ledger-7", args_digest="sha256:abc"))
127
+ verify_signature(receipt, expected_public_key=public_key_hex(key))
128
+ print(receipt.payload.decision) # "allow" — or "deny", still sealed
129
+ ```
130
+
131
+ **CLI** (`pip install 'avow[cli]'`):
132
+
133
+ ```bash
134
+ uv run assay keygen --out signing.key # also writes signing.key.pub
135
+ echo '{"metric":"binary","metric_version":"1","y_true":[0,1,0,1],"y_score":[0.2,0.8,0.3,0.7]}' > req.json
136
+ uv run assay score --request req.json --key signing.key --out receipt.json --ledger ledger.jsonl
137
+ uv run assay verify --receipt receipt.json --public-key signing.key.pub # -> OK: receipt verified
138
+ ```
139
+
140
+ ## Under the hood (for developers)
141
+
142
+ **`avow` — the envelope.** `sign_payload` / `verify_signature` / `payload_digest`
143
+ operate only on the canonical JSON of a frozen `SignedReceipt[SubjectT]` subject.
144
+ Canonicalization is **RFC 8785 (JCS)** — a byte-stable JSON form — hashed with SHA-256
145
+ and signed with **Ed25519** (`PyNaCl`). Payloads carry **no timestamp**, so identical
146
+ inputs yield an identical, reproducible, offline-verifiable receipt. `avow.ledger` is an
147
+ append-only, content-addressed JSONL log with a fail-closed integrity check, generic
148
+ over the subject. Coded failures live in `avow.errors` (`avow.*` codes under `AvowError`).
149
+
150
+ **`assay` — the scoring face.** A thin trust + honesty + composition layer over reused
151
+ libraries (it computes no metric math itself): scikit-learn for precision/recall/F1,
152
+ PR-AUC, ROC-AUC and Brier; `scipy.stats.bootstrap` for percentile intervals with a
153
+ **sample-size floor** (below it, Assay abstains rather than invent a point estimate);
154
+ population-weighted **ECE** for calibration; a positive-weighted composite with a
155
+ propagated interval. `assay.receipt` defines the score subjects; the envelope signs
156
+ them. Its errors are `assay.*` under `AssayError`. Import `assay` without the
157
+ `[assay]` extra and you get a coded `ScoringExtraMissing`, never a raw traceback.
158
+
159
+ **`writ` — the effect face.** `writ.gate(request, policy, effector)` evaluates a typed
160
+ policy: on **deny** it seals a signed denial receipt and never runs the effect; on
161
+ **allow** the effector runs the effect, then seals a signed effect receipt — both
162
+ verifiable through the shared envelope. The v0 policy decider is a Python predicate
163
+ (`Allowlist`); **OPA/Rego is the v1 decider**.
164
+
165
+ **Un-bypassable seam — stated honestly.** The effect credential and the privileged
166
+ effect live only inside the effector, which `governed_gate` captures in the single
167
+ closure the agent receives; the effector itself is never handed over, so the only path
168
+ to the effect is through the guard. This is the **v0 capability-holding approximation**:
169
+ the credential is still in-process, so same-process reflection could reach it. TRUE
170
+ un-bypassable enforcement (a separate-process broker or a WASM guest, where the agent's
171
+ address space cannot reach the credential) is the **v1 hardening**. We claim no more.
172
+
173
+ **Signing-key custody.** `assay keygen` (and `avow.keys`) write a 32-byte Ed25519 seed
174
+ to a `0600` file and the public key to a companion `.pub`. Keys are never logged and
175
+ never committed (`*.key` is gitignored). The public key also travels inside each receipt
176
+ for convenience, but that embedded copy is **not** the trust anchor — a verifier pins
177
+ the out-of-band key and passes it to `verify`.
178
+
179
+ **Cross-language byte identity.** `testdata/vectors/` holds golden vectors generated by
180
+ `tests/gen_vectors.py` (canonical bytes + hashes, and receipts signed with a fixed
181
+ non-secret test seed). The Python suite replays them in `tests/test_vectors.py`; a
182
+ future TypeScript `@edgeproc/avow` replays the *same files* byte-for-byte, so any RFC
183
+ 8785 number-serialization divergence fails in CI, not in production.
184
+
185
+ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the data-flow diagram, the
186
+ import edges, and the native-vs-browser story.
187
+
188
+ ## Status
189
+
190
+ v0 shipped — deterministic (no LLM). Full pyramid green under `uv run poe gate`
191
+ (ruff, ruff-format, mypy `--strict`, xenon A, 100% coverage). CI mirrors the gate.
192
+
193
+ ## License
194
+
195
+ MIT © Harish Seshadri
@@ -0,0 +1,32 @@
1
+ # QUICKSTART
2
+
3
+ One distribution `avow`, three import packages: `avow` (envelope), `assay` (scoring),
4
+ `writ` (effect). `assay` and `writ` import `avow`; `avow` imports neither.
5
+
6
+ 1. `uv sync --all-extras` — installs all three faces + the CLI + tooling for dev.
7
+ 2. `uv run python demo/run_demo.py` — scoring face: six honesty acceptance cases.
8
+ 3. `uv run python demo/unification_demo.py` — one envelope + one verifier, both faces.
9
+ 4. `uv run poe gate` — ruff, ruff-format, mypy `--strict`, xenon A, tests (100% coverage).
10
+
11
+ Install matrix (for consumers):
12
+
13
+ | Command | Gives you |
14
+ |---|---|
15
+ | `pip install avow` | `import avow`, `import writ` |
16
+ | `pip install 'avow[assay]'` | `+ import assay` (scoring) |
17
+ | `pip install 'avow[cli]'` | `+ the assay CLI` |
18
+
19
+ CLI: `uv run assay --help`.
20
+
21
+ ```bash
22
+ uv run assay keygen --out signing.key # also writes signing.key.pub
23
+ echo '{"metric":"binary","metric_version":"1","y_true":[0,1,0,1],"y_score":[0.2,0.8,0.3,0.7]}' > req.json
24
+ uv run assay score --request req.json --key signing.key --out receipt.json --ledger ledger.jsonl
25
+ uv run assay verify --receipt receipt.json --public-key signing.key.pub # -> OK: receipt verified
26
+ ```
27
+
28
+ Regenerate the cross-language golden vectors after any canonicalization change:
29
+
30
+ ```bash
31
+ uv run python tests/gen_vectors.py # writes testdata/vectors/{canonical,receipts}.json
32
+ ```
avow-0.1.0/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # Avow
2
+
3
+ **The trust kernel: one signed receipt, three Legos.**
4
+
5
+ Avow turns a claim into a **receipt you can check** — a small, signed record that
6
+ proves *exactly what was claimed* and fails loudly if a single byte is changed. It
7
+ ships as one installable distribution (`avow`) that exposes three import packages you
8
+ can pick and choose:
9
+
10
+ | Package | It is… | Depends on | Install |
11
+ |---|---|---|---|
12
+ | **`avow`** | the shared **envelope** — sign, hash, verify a receipt | pydantic, pynacl, rfc8785 | `pip install avow` |
13
+ | **`assay`** | the **scoring** face — an honest number, wrapped in a receipt | `avow` + scikit-learn/scipy/numpy | `pip install 'avow[assay]'` |
14
+ | **`writ`** | the **effect** face — a policy-gated action, sealed as a receipt | `avow` only | `pip install avow` |
15
+
16
+ The command-line tool lives behind `pip install 'avow[cli]'`.
17
+
18
+ > **One rule you can hold in your head:** the envelope signs the *canonical bytes of a
19
+ > frozen subject* and never looks inside. So the exact same sign/verify code carries a
20
+ > score for the scoring face and an action for the effect face. One envelope, many
21
+ > subjects. The dependency arrows only ever point **into** `avow` — `assay → avow` and
22
+ > `writ → avow`; `avow` imports neither, which is why installing the envelope alone
23
+ > never pulls the heavy science stack.
24
+
25
+ ## Why it exists
26
+
27
+ A raw claim — `f1 = 0.83`, or "I deleted account 7" — is a statement with no receipt.
28
+ You cannot tell whether the score ran on 12 examples or 12,000, or whether the action
29
+ was actually allowed. Avow wraps the claim in a **signed receipt** carrying the inputs'
30
+ content hash and an Ed25519 signature. Anyone holding the receipt can re-check it
31
+ offline — and verification fails if anything was tampered.
32
+
33
+ **The trust boundary, stated honestly.** A receipt proves *payload integrity under some
34
+ signer*. It does **not**, on its own, prove *authenticity* — the signer's public key
35
+ rides in the receipt outside the signed bytes, so an attacker can re-sign a forged
36
+ payload with their own key. Authenticity therefore requires **pinning the signer's
37
+ public key out-of-band**: you pass the key you already trust (the `.pub` from `keygen`)
38
+ to `verify`, and Avow rejects any receipt not signed by exactly that key. Never trust
39
+ the key embedded in the receipt.
40
+
41
+ ## Quickstart (copy-paste)
42
+
43
+ ```bash
44
+ # dev setup (this repo installs all faces + tooling):
45
+ uv sync --all-extras
46
+ uv run poe gate # ruff · ruff-format · mypy --strict · xenon A · pytest (100%)
47
+ uv run python demo/run_demo.py # scoring face: 6 honesty acceptance cases
48
+ uv run python demo/unification_demo.py # ONE envelope + ONE verifier, BOTH faces
49
+ ```
50
+
51
+ **Envelope only** — sign and verify any frozen subject (base install, no science stack):
52
+
53
+ ```python
54
+ from pydantic import BaseModel, ConfigDict
55
+ from avow import generate_signing_key, public_key_hex, sign_payload, verify_signature
56
+
57
+ class Claim(BaseModel):
58
+ model_config = ConfigDict(frozen=True, extra="forbid")
59
+ what: str
60
+ value: float
61
+
62
+ key = generate_signing_key()
63
+ pinned = public_key_hex(key) # pin this out-of-band
64
+ receipt = sign_payload(Claim(what="f1", value=0.83), key)
65
+ verify_signature(receipt, expected_public_key=pinned) # raises on any tamper
66
+ ```
67
+
68
+ **Scoring face** (`pip install 'avow[assay]'`) — an honest number in a receipt:
69
+
70
+ ```python
71
+ from avow import generate_signing_key, public_key_hex
72
+ from assay import score, verify
73
+ from assay.models import ScoreRequest
74
+ from assay.settings import AssaySettings
75
+
76
+ key = generate_signing_key()
77
+ request = ScoreRequest(metric="binary", metric_version="1",
78
+ y_true=(0, 1, 0, 1), y_score=(0.2, 0.8, 0.3, 0.7))
79
+ receipt = score(request, signing_key=key, settings=AssaySettings())
80
+ print(receipt.payload.score) # calibrated point — or None when the sample is too thin
81
+ assert verify(receipt, expected_public_key=public_key_hex(key)) is True
82
+ ```
83
+
84
+ **Effect face** (`writ`, base install) — a policy-gated, sealed action:
85
+
86
+ ```python
87
+ from avow import generate_signing_key, public_key_hex, verify_signature
88
+ from writ import Allowlist, EffectRequest, KeyholderEffector, governed_gate
89
+
90
+ key = generate_signing_key()
91
+ def do_it(_req: EffectRequest) -> None: ... # the privileged effect
92
+ gate = governed_gate(Allowlist(frozenset({"read"})),
93
+ KeyholderEffector(effect=do_it, signing_key=key))
94
+ receipt = gate(EffectRequest(action="read", target="ledger-7", args_digest="sha256:abc"))
95
+ verify_signature(receipt, expected_public_key=public_key_hex(key))
96
+ print(receipt.payload.decision) # "allow" — or "deny", still sealed
97
+ ```
98
+
99
+ **CLI** (`pip install 'avow[cli]'`):
100
+
101
+ ```bash
102
+ uv run assay keygen --out signing.key # also writes signing.key.pub
103
+ echo '{"metric":"binary","metric_version":"1","y_true":[0,1,0,1],"y_score":[0.2,0.8,0.3,0.7]}' > req.json
104
+ uv run assay score --request req.json --key signing.key --out receipt.json --ledger ledger.jsonl
105
+ uv run assay verify --receipt receipt.json --public-key signing.key.pub # -> OK: receipt verified
106
+ ```
107
+
108
+ ## Under the hood (for developers)
109
+
110
+ **`avow` — the envelope.** `sign_payload` / `verify_signature` / `payload_digest`
111
+ operate only on the canonical JSON of a frozen `SignedReceipt[SubjectT]` subject.
112
+ Canonicalization is **RFC 8785 (JCS)** — a byte-stable JSON form — hashed with SHA-256
113
+ and signed with **Ed25519** (`PyNaCl`). Payloads carry **no timestamp**, so identical
114
+ inputs yield an identical, reproducible, offline-verifiable receipt. `avow.ledger` is an
115
+ append-only, content-addressed JSONL log with a fail-closed integrity check, generic
116
+ over the subject. Coded failures live in `avow.errors` (`avow.*` codes under `AvowError`).
117
+
118
+ **`assay` — the scoring face.** A thin trust + honesty + composition layer over reused
119
+ libraries (it computes no metric math itself): scikit-learn for precision/recall/F1,
120
+ PR-AUC, ROC-AUC and Brier; `scipy.stats.bootstrap` for percentile intervals with a
121
+ **sample-size floor** (below it, Assay abstains rather than invent a point estimate);
122
+ population-weighted **ECE** for calibration; a positive-weighted composite with a
123
+ propagated interval. `assay.receipt` defines the score subjects; the envelope signs
124
+ them. Its errors are `assay.*` under `AssayError`. Import `assay` without the
125
+ `[assay]` extra and you get a coded `ScoringExtraMissing`, never a raw traceback.
126
+
127
+ **`writ` — the effect face.** `writ.gate(request, policy, effector)` evaluates a typed
128
+ policy: on **deny** it seals a signed denial receipt and never runs the effect; on
129
+ **allow** the effector runs the effect, then seals a signed effect receipt — both
130
+ verifiable through the shared envelope. The v0 policy decider is a Python predicate
131
+ (`Allowlist`); **OPA/Rego is the v1 decider**.
132
+
133
+ **Un-bypassable seam — stated honestly.** The effect credential and the privileged
134
+ effect live only inside the effector, which `governed_gate` captures in the single
135
+ closure the agent receives; the effector itself is never handed over, so the only path
136
+ to the effect is through the guard. This is the **v0 capability-holding approximation**:
137
+ the credential is still in-process, so same-process reflection could reach it. TRUE
138
+ un-bypassable enforcement (a separate-process broker or a WASM guest, where the agent's
139
+ address space cannot reach the credential) is the **v1 hardening**. We claim no more.
140
+
141
+ **Signing-key custody.** `assay keygen` (and `avow.keys`) write a 32-byte Ed25519 seed
142
+ to a `0600` file and the public key to a companion `.pub`. Keys are never logged and
143
+ never committed (`*.key` is gitignored). The public key also travels inside each receipt
144
+ for convenience, but that embedded copy is **not** the trust anchor — a verifier pins
145
+ the out-of-band key and passes it to `verify`.
146
+
147
+ **Cross-language byte identity.** `testdata/vectors/` holds golden vectors generated by
148
+ `tests/gen_vectors.py` (canonical bytes + hashes, and receipts signed with a fixed
149
+ non-secret test seed). The Python suite replays them in `tests/test_vectors.py`; a
150
+ future TypeScript `@edgeproc/avow` replays the *same files* byte-for-byte, so any RFC
151
+ 8785 number-serialization divergence fails in CI, not in production.
152
+
153
+ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the data-flow diagram, the
154
+ import edges, and the native-vs-browser story.
155
+
156
+ ## Status
157
+
158
+ v0 shipped — deterministic (no LLM). Full pyramid green under `uv run poe gate`
159
+ (ruff, ruff-format, mypy `--strict`, xenon A, 100% coverage). CI mirrors the gate.
160
+
161
+ ## License
162
+
163
+ MIT © Harish Seshadri
@@ -0,0 +1,141 @@
1
+ """Assay demo: proves all six v0 acceptance cases end-to-end.
2
+
3
+ Run it: uv run python demo/run_demo.py
4
+ Every case computes a real score, signs a real receipt, and asserts the honesty
5
+ property that case guarantees."""
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+
11
+ from nacl.signing import SigningKey
12
+
13
+ from assay.api import composite_score, replay, score, verify
14
+ from assay.models import CompositeRequest, ScoreRequest, SubScoreInput
15
+ from assay.receipt import sign_payload
16
+ from assay.settings import AssaySettings
17
+
18
+ _SEED = bytes(range(32))
19
+
20
+
21
+ def _pubkey(key: SigningKey) -> str:
22
+ """The signer's public key, pinned by the verifier out-of-band."""
23
+ return bytes(key.verify_key).hex()
24
+
25
+
26
+ def _big_request() -> ScoreRequest:
27
+ return ScoreRequest(
28
+ metric="binary",
29
+ metric_version="1",
30
+ y_true=tuple([0, 1] * 20),
31
+ y_score=tuple([0.2, 0.8] * 20),
32
+ )
33
+
34
+
35
+ def _composite_request() -> CompositeRequest:
36
+ subs = (
37
+ SubScoreInput(
38
+ name="accuracy",
39
+ value=0.9,
40
+ low=0.85,
41
+ high=0.95,
42
+ scale_min=0.0,
43
+ scale_max=1.0,
44
+ weight=1.0,
45
+ ),
46
+ SubScoreInput(
47
+ name="latency",
48
+ value=80.0,
49
+ low=70.0,
50
+ high=90.0,
51
+ scale_min=0.0,
52
+ scale_max=100.0,
53
+ weight=1.0,
54
+ ),
55
+ SubScoreInput(
56
+ name="rating",
57
+ value=4.0,
58
+ low=3.5,
59
+ high=4.5,
60
+ scale_min=1.0,
61
+ scale_max=5.0,
62
+ weight=2.0,
63
+ ),
64
+ )
65
+ return CompositeRequest(metric_version="1", subscores=subs)
66
+
67
+
68
+ def _case_reproducible(key: SigningKey, settings: AssaySettings) -> None:
69
+ a = score(_big_request(), signing_key=key, settings=settings)
70
+ b = score(_big_request(), signing_key=key, settings=settings)
71
+ assert a.payload_hash == b.payload_hash and a == b
72
+ print(f"[1] reproducible: identical hash {a.payload_hash[:23]}...")
73
+
74
+
75
+ def _case_offline_verify(key: SigningKey, settings: AssaySettings) -> None:
76
+ receipt = score(_big_request(), signing_key=key, settings=settings)
77
+ assert verify(receipt, expected_public_key=_pubkey(key)) is True
78
+ assert replay(_big_request(), receipt, settings=settings) is True
79
+ print("[2] offline verify + replay: signature valid, score recomputes")
80
+
81
+
82
+ def _case_tamper(key: SigningKey, settings: AssaySettings) -> None:
83
+ receipt = score(_big_request(), signing_key=key, settings=settings)
84
+ expected = _pubkey(key)
85
+ # A blanked signature fails against the pinned signer.
86
+ forged = receipt.model_copy(update={"signature": "00" * 64})
87
+ assert verify(forged, expected_public_key=expected) is False
88
+ # A re-signed forgery (attacker flips the score, signs with their OWN key and
89
+ # swaps in their pubkey) is also rejected — authenticity is pinned, not trusted.
90
+ attacker = SigningKey(bytes(range(1, 33)))
91
+ resigned = sign_payload(receipt.payload.model_copy(update={"score": 0.999}), attacker)
92
+ assert verify(resigned, expected_public_key=expected) is False
93
+ print("[3] tamper + forgery detected: neither a flipped sig nor a re-signed key passes")
94
+
95
+
96
+ def _case_abstain(key: SigningKey, settings: AssaySettings) -> None:
97
+ thin = ScoreRequest(
98
+ metric="binary",
99
+ metric_version="1",
100
+ y_true=(0, 1, 0, 1, 0),
101
+ y_score=(0.2, 0.8, 0.3, 0.7, 0.4),
102
+ )
103
+ receipt = score(thin, signing_key=key, settings=settings)
104
+ assert receipt.payload.abstained is True and receipt.payload.score is None
105
+ print("[4] low sample: abstained, no fake point number emitted")
106
+
107
+
108
+ def _case_calibration(key: SigningKey, settings: AssaySettings) -> None:
109
+ receipt = score(_big_request(), signing_key=key, settings=settings)
110
+ calibration = receipt.payload.calibration
111
+ assert calibration is not None
112
+ assert math.isclose(calibration.ece, 0.2) # float aggregate: compare with tolerance
113
+ print(f"[5] calibration shipped: ECE={calibration.ece}, Brier={calibration.brier}")
114
+
115
+
116
+ def _case_composite(key: SigningKey) -> None:
117
+ receipt = composite_score(_composite_request(), signing_key=key)
118
+ payload = receipt.payload
119
+ assert payload.score == 0.8 # noqa: PLR2004
120
+ assert payload.interval_low == 0.7 # noqa: PLR2004
121
+ assert payload.interval_high == 0.9 # noqa: PLR2004
122
+ assert verify(receipt, expected_public_key=_pubkey(key)) is True
123
+ print(f"[6] composite: {payload.score} in [{payload.interval_low}, {payload.interval_high}]")
124
+
125
+
126
+ def main() -> int:
127
+ """Run all six acceptance cases; return 0 on success."""
128
+ key = SigningKey(_SEED)
129
+ settings = AssaySettings()
130
+ _case_reproducible(key, settings)
131
+ _case_offline_verify(key, settings)
132
+ _case_tamper(key, settings)
133
+ _case_abstain(key, settings)
134
+ _case_calibration(key, settings)
135
+ _case_composite(key)
136
+ print("all six acceptance cases passed")
137
+ return 0
138
+
139
+
140
+ if __name__ == "__main__":
141
+ raise SystemExit(main())