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.
- avow-0.1.0/.gitignore +30 -0
- avow-0.1.0/CHANGELOG.md +30 -0
- avow-0.1.0/LICENSE +21 -0
- avow-0.1.0/PKG-INFO +195 -0
- avow-0.1.0/QUICKSTART.md +32 -0
- avow-0.1.0/README.md +163 -0
- avow-0.1.0/demo/run_demo.py +141 -0
- avow-0.1.0/demo/unification_demo.py +121 -0
- avow-0.1.0/pyproject.toml +163 -0
- avow-0.1.0/src/assay/__init__.py +17 -0
- avow-0.1.0/src/assay/api.py +169 -0
- avow-0.1.0/src/assay/calibration.py +70 -0
- avow-0.1.0/src/assay/cli.py +97 -0
- avow-0.1.0/src/assay/composite.py +101 -0
- avow-0.1.0/src/assay/errors.py +61 -0
- avow-0.1.0/src/assay/metrics.py +78 -0
- avow-0.1.0/src/assay/models.py +42 -0
- avow-0.1.0/src/assay/py.typed +0 -0
- avow-0.1.0/src/assay/receipt.py +112 -0
- avow-0.1.0/src/assay/settings.py +21 -0
- avow-0.1.0/src/assay/uncertainty.py +82 -0
- avow-0.1.0/src/avow/__init__.py +43 -0
- avow-0.1.0/src/avow/_version.py +5 -0
- avow-0.1.0/src/avow/canonical.py +33 -0
- avow-0.1.0/src/avow/envelope.py +90 -0
- avow-0.1.0/src/avow/errors.py +38 -0
- avow-0.1.0/src/avow/keys.py +56 -0
- avow-0.1.0/src/avow/ledger.py +46 -0
- avow-0.1.0/src/avow/py.typed +0 -0
- avow-0.1.0/src/avow/verify.py +22 -0
- avow-0.1.0/src/writ/__init__.py +35 -0
- avow-0.1.0/src/writ/gate.py +144 -0
- avow-0.1.0/src/writ/py.typed +0 -0
- avow-0.1.0/testdata/vectors/canonical.json +102 -0
- avow-0.1.0/testdata/vectors/receipts.json +38 -0
- avow-0.1.0/tests/gen_vectors.py +102 -0
- avow-0.1.0/tests/test_api.py +156 -0
- avow-0.1.0/tests/test_boundaries.py +131 -0
- avow-0.1.0/tests/test_calibration.py +39 -0
- avow-0.1.0/tests/test_canonical.py +34 -0
- avow-0.1.0/tests/test_cli.py +85 -0
- avow-0.1.0/tests/test_composite.py +70 -0
- avow-0.1.0/tests/test_demo_runs.py +24 -0
- avow-0.1.0/tests/test_e2e_acceptance.py +141 -0
- avow-0.1.0/tests/test_envelope_split.py +73 -0
- avow-0.1.0/tests/test_errors.py +56 -0
- avow-0.1.0/tests/test_keys.py +38 -0
- avow-0.1.0/tests/test_ledger.py +55 -0
- avow-0.1.0/tests/test_metrics.py +47 -0
- avow-0.1.0/tests/test_models.py +47 -0
- avow-0.1.0/tests/test_packaging.py +44 -0
- avow-0.1.0/tests/test_receipt.py +111 -0
- avow-0.1.0/tests/test_settings.py +24 -0
- avow-0.1.0/tests/test_smoke.py +19 -0
- avow-0.1.0/tests/test_uncertainty.py +41 -0
- avow-0.1.0/tests/test_unification_demo_runs.py +24 -0
- avow-0.1.0/tests/test_vectors.py +48 -0
- avow-0.1.0/tests/test_verify.py +85 -0
- 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/
|
avow-0.1.0/CHANGELOG.md
ADDED
|
@@ -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
|
avow-0.1.0/QUICKSTART.md
ADDED
|
@@ -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())
|