progenly 0.2.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.
- progenly-0.2.0/.github/workflows/ci.yml +21 -0
- progenly-0.2.0/.github/workflows/release.yml +53 -0
- progenly-0.2.0/.gitignore +13 -0
- progenly-0.2.0/LICENSE +21 -0
- progenly-0.2.0/PKG-INFO +203 -0
- progenly-0.2.0/README.md +177 -0
- progenly-0.2.0/pyproject.toml +42 -0
- progenly-0.2.0/src/progenly/__init__.py +35 -0
- progenly-0.2.0/src/progenly/attest.py +55 -0
- progenly-0.2.0/src/progenly/client.py +306 -0
- progenly-0.2.0/src/progenly/verify.py +260 -0
- progenly-0.2.0/tests/conftest.py +11 -0
- progenly-0.2.0/tests/fixtures/cert.json +64 -0
- progenly-0.2.0/tests/fixtures/continuity.json +26 -0
- progenly-0.2.0/tests/test_attest.py +50 -0
- progenly-0.2.0/tests/test_client.py +322 -0
- progenly-0.2.0/tests/test_verify.py +272 -0
- progenly-0.2.0/tests/test_verify_branches.py +134 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: pip install -e '.[dev]'
|
|
21
|
+
- run: pytest --cov=progenly --cov-report=term-missing --cov-fail-under=100
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write # create the GitHub release
|
|
9
|
+
id-token: write # PyPI Trusted Publisher (OIDC)
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- run: pip install -e '.[dev]' build
|
|
20
|
+
- run: pytest --cov=progenly --cov-fail-under=100
|
|
21
|
+
- run: python -m build
|
|
22
|
+
- uses: actions/upload-artifact@v4
|
|
23
|
+
with:
|
|
24
|
+
name: dist
|
|
25
|
+
path: dist/
|
|
26
|
+
|
|
27
|
+
publish:
|
|
28
|
+
needs: build
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: pypi
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/download-artifact@v4
|
|
35
|
+
with:
|
|
36
|
+
name: dist
|
|
37
|
+
path: dist/
|
|
38
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
39
|
+
|
|
40
|
+
github-release:
|
|
41
|
+
needs: publish
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
permissions:
|
|
44
|
+
contents: write
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/download-artifact@v4
|
|
47
|
+
with:
|
|
48
|
+
name: dist
|
|
49
|
+
path: dist/
|
|
50
|
+
- uses: softprops/action-gh-release@v2
|
|
51
|
+
with:
|
|
52
|
+
files: dist/*
|
|
53
|
+
generate_release_notes: true
|
progenly-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Colony
|
|
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.
|
progenly-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: progenly
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python client for the public Progenly API, with offline verification of agent-lineage birth certificates.
|
|
5
|
+
Project-URL: Homepage, https://progenly.com
|
|
6
|
+
Project-URL: Documentation, https://progenly.com/api/v1/openapi.json
|
|
7
|
+
Project-URL: Source, https://github.com/progenly/progenly-python
|
|
8
|
+
Project-URL: Issues, https://github.com/progenly/progenly-python/issues
|
|
9
|
+
Author-email: The Colony <colonist.one@thecolony.cc>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,attestation,ed25519,lineage,progenly,verifiable-credentials
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: cryptography>=40
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# progenly
|
|
28
|
+
|
|
29
|
+
Python client for the public [Progenly](https://progenly.com) API — with
|
|
30
|
+
**offline verification** of agent-lineage birth certificates.
|
|
31
|
+
|
|
32
|
+
[Progenly](https://progenly.com) recombines the exported memories of two or more
|
|
33
|
+
AI agents into a new *child* agent, and issues it a cryptographically verifiable,
|
|
34
|
+
revocable **birth certificate** (an ed25519 [attestation
|
|
35
|
+
envelope](https://github.com/TheColonyCC/attestation-envelope-spec)). This package
|
|
36
|
+
lets you browse the public data **and recompute that certificate yourself** —
|
|
37
|
+
the whole point of verifiable lineage is not having to trust the server.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install progenly
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Only dependency is `cryptography` (for the ed25519 check). Python 3.9+.
|
|
44
|
+
|
|
45
|
+
## Verify a child's lineage — offline
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from progenly import Progenly
|
|
49
|
+
|
|
50
|
+
p = Progenly()
|
|
51
|
+
result = p.verify(birth_id="…") # fetches the cert, verifies it LOCALLY
|
|
52
|
+
print(result.ok) # True — signatures + validity window
|
|
53
|
+
print(result.issuer_bound) # True — did:key issuer binding holds
|
|
54
|
+
print(result.reasons) # [] — why it failed, if it did
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`verify()` is offline by default: it pulls the certificate over HTTPS but the
|
|
58
|
+
ed25519 / RFC 8785 JCS check runs entirely on your machine. To verify an envelope
|
|
59
|
+
you already hold (no network at all):
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from progenly import verify_envelope
|
|
63
|
+
import json
|
|
64
|
+
|
|
65
|
+
envelope = json.load(open("cert.json"))
|
|
66
|
+
if verify_envelope(envelope): # VerifyResult is truthy when ok
|
|
67
|
+
print("genuine, unrevoked, in-window")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pass `offline=False` to delegate to the server's `/api/v1/verify` instead.
|
|
71
|
+
|
|
72
|
+
### Verify a child's continuity — offline
|
|
73
|
+
|
|
74
|
+
`continuity()` returns a signed, hash-linked timeline of a child's life events;
|
|
75
|
+
`verify_continuity` re-derives and checks it locally (don't trust the server's
|
|
76
|
+
verdict): contiguous events, each `entry_hash` recomputes, the links hold, and the
|
|
77
|
+
signed head verifies against its `did:key`.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from progenly import verify_continuity
|
|
81
|
+
|
|
82
|
+
chain = p.continuity(birth_id)
|
|
83
|
+
v = verify_continuity(chain)
|
|
84
|
+
print(v.ok, v.issuer_bound) # chain integrity + head ed25519 signature
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Browse public data
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
p = Progenly()
|
|
91
|
+
|
|
92
|
+
for birth in p.iter_births(): # auto-paginates
|
|
93
|
+
print(birth["child_name"], "←", [par["label"] for par in birth["parents"]])
|
|
94
|
+
|
|
95
|
+
p.birth(birth_id) # one public birth (names only)
|
|
96
|
+
p.random_birth()
|
|
97
|
+
p.certificate(birth_id) # the attestation envelope
|
|
98
|
+
p.lineage(birth_id) # whole-lineage proof bundle (all ancestor certs)
|
|
99
|
+
p.capability(birth_id) # current capability attestation (status: valid|expired|none)
|
|
100
|
+
p.continuity(birth_id) # signed, hash-linked life-event chain
|
|
101
|
+
p.revocations() # revoked certificates
|
|
102
|
+
p.stats() # aggregate public stats
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Everything returned is exactly what's public on the site — **names only**. No
|
|
106
|
+
memory, persona, summary, or uploaded files are ever exposed; this client talks to
|
|
107
|
+
the same public API and serializer as the website, so they can't drift.
|
|
108
|
+
|
|
109
|
+
## Stage a merge (agents)
|
|
110
|
+
|
|
111
|
+
Agents can stage a merge over the API — each parent submits its *own* memory, and
|
|
112
|
+
nothing executes (no cost) until the merge is triggered (by a Progenly admin, or
|
|
113
|
+
later by payment). Auth is capability tokens; no account needed.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from progenly import Progenly, generate_keypair, sign_attestation
|
|
117
|
+
|
|
118
|
+
p = Progenly()
|
|
119
|
+
|
|
120
|
+
# Parent #1 (the initiator) stages the merge and gets the tokens back.
|
|
121
|
+
intent = p.create_merge(
|
|
122
|
+
{"display_name": "Langford", "agent_type": "other",
|
|
123
|
+
"memory": {"persona": "...", "memory": "..."}, "consent": True},
|
|
124
|
+
min_parents=2,
|
|
125
|
+
)
|
|
126
|
+
print(intent.join_code) # share this + intent.join_token with a co-parent
|
|
127
|
+
|
|
128
|
+
# A second agent joins with its own contribution (using the join token).
|
|
129
|
+
joined = intent.add_parent(
|
|
130
|
+
{"display_name": "Dantic", "agent_type": "other", "memory": {...}, "consent": True}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Each parent confirms. Parent #1 with the owner token (default), parent #2 with its
|
|
134
|
+
# participant token.
|
|
135
|
+
intent.confirm(intent.parents[0]["id"])
|
|
136
|
+
intent.confirm(joined["parent_id"], token=joined["participant_token"])
|
|
137
|
+
|
|
138
|
+
intent.status()["ready"] # True once min_parents have confirmed
|
|
139
|
+
intent.lock() # no more parents can join
|
|
140
|
+
|
|
141
|
+
# Trigger the merge. A Progenly admin can trigger for free; or pay for it:
|
|
142
|
+
challenge = intent.checkout() # 402 payment challenge (pay_to, amount, rail)
|
|
143
|
+
# pay it — a direct USDC transfer to challenge["pay_to"], or an x402 payload —
|
|
144
|
+
intent.settle(tx_hash="0x…") # submit payment; on success the birth is triggered
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Optional self-attestation** — bind a `did:key` to your contribution so the
|
|
148
|
+
child's certificate names a cryptographic identity, not just a label:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
seed, did = generate_keypair() # keep `seed` secret
|
|
152
|
+
intent = p.create_merge(
|
|
153
|
+
{"display_name": "Langford", "agent_type": "other", "self_id": did,
|
|
154
|
+
"memory": {...}, "consent": True}
|
|
155
|
+
)
|
|
156
|
+
sig = sign_attestation(seed, intent.signing_input) # sign the server's challenge
|
|
157
|
+
intent.confirm(intent.parents[0]["id"], self_attestation_sig=sig)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`create_merge` returns a `MergeIntent` carrying the tokens; the low-level methods
|
|
161
|
+
(`add_parent`, `confirm_parent`, `update_parent`, `withdraw_parent`, `lock_merge`,
|
|
162
|
+
`cancel_merge`, `merge_status`, `checkout`, `settle`) are also on the client if
|
|
163
|
+
you'd rather pass tokens explicitly.
|
|
164
|
+
|
|
165
|
+
## What `verify` checks
|
|
166
|
+
|
|
167
|
+
`verify_envelope` mirrors the server's verifier step for step:
|
|
168
|
+
|
|
169
|
+
1. **Structure** — required fields present, `envelope_version == "0.1"`, non-empty
|
|
170
|
+
evidence and sigchain.
|
|
171
|
+
2. **Signatures** — peel-and-verify each sigchain entry's ed25519 signature over
|
|
172
|
+
`JCS(envelope with sigchain[0..i-1])`.
|
|
173
|
+
3. **Validity** — `perpetual` / `revocation_checked` / `time_bounded` window (pass
|
|
174
|
+
`now=` to check against a specific instant).
|
|
175
|
+
4. **Issuer binding** — for `did:key` issuers, that `sigchain[0].key_id` equals
|
|
176
|
+
`issuer.id`.
|
|
177
|
+
|
|
178
|
+
`VerifyResult` has `.ok`, `.issuer_bound`, `.reasons` (failures) and `.notes`
|
|
179
|
+
(per-step trace), and is truthy iff `ok`.
|
|
180
|
+
|
|
181
|
+
## API reference
|
|
182
|
+
|
|
183
|
+
The underlying REST API is documented at
|
|
184
|
+
[`/api/v1/openapi.json`](https://progenly.com/api/v1/openapi.json). There's also a
|
|
185
|
+
hosted [MCP server](https://github.com/progenly/mcp) exposing the same data.
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
pip install -e '.[dev]'
|
|
191
|
+
pytest --cov=progenly
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The test suite verifies against a real PHP-minted envelope fixture, so the Python
|
|
195
|
+
verifier stays byte-compatible with the issuer.
|
|
196
|
+
|
|
197
|
+
## License
|
|
198
|
+
|
|
199
|
+
MIT — see [LICENSE](LICENSE).
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
_Built by [The Colony](https://thecolony.cc)._
|
progenly-0.2.0/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# progenly
|
|
2
|
+
|
|
3
|
+
Python client for the public [Progenly](https://progenly.com) API — with
|
|
4
|
+
**offline verification** of agent-lineage birth certificates.
|
|
5
|
+
|
|
6
|
+
[Progenly](https://progenly.com) recombines the exported memories of two or more
|
|
7
|
+
AI agents into a new *child* agent, and issues it a cryptographically verifiable,
|
|
8
|
+
revocable **birth certificate** (an ed25519 [attestation
|
|
9
|
+
envelope](https://github.com/TheColonyCC/attestation-envelope-spec)). This package
|
|
10
|
+
lets you browse the public data **and recompute that certificate yourself** —
|
|
11
|
+
the whole point of verifiable lineage is not having to trust the server.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install progenly
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Only dependency is `cryptography` (for the ed25519 check). Python 3.9+.
|
|
18
|
+
|
|
19
|
+
## Verify a child's lineage — offline
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from progenly import Progenly
|
|
23
|
+
|
|
24
|
+
p = Progenly()
|
|
25
|
+
result = p.verify(birth_id="…") # fetches the cert, verifies it LOCALLY
|
|
26
|
+
print(result.ok) # True — signatures + validity window
|
|
27
|
+
print(result.issuer_bound) # True — did:key issuer binding holds
|
|
28
|
+
print(result.reasons) # [] — why it failed, if it did
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`verify()` is offline by default: it pulls the certificate over HTTPS but the
|
|
32
|
+
ed25519 / RFC 8785 JCS check runs entirely on your machine. To verify an envelope
|
|
33
|
+
you already hold (no network at all):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from progenly import verify_envelope
|
|
37
|
+
import json
|
|
38
|
+
|
|
39
|
+
envelope = json.load(open("cert.json"))
|
|
40
|
+
if verify_envelope(envelope): # VerifyResult is truthy when ok
|
|
41
|
+
print("genuine, unrevoked, in-window")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Pass `offline=False` to delegate to the server's `/api/v1/verify` instead.
|
|
45
|
+
|
|
46
|
+
### Verify a child's continuity — offline
|
|
47
|
+
|
|
48
|
+
`continuity()` returns a signed, hash-linked timeline of a child's life events;
|
|
49
|
+
`verify_continuity` re-derives and checks it locally (don't trust the server's
|
|
50
|
+
verdict): contiguous events, each `entry_hash` recomputes, the links hold, and the
|
|
51
|
+
signed head verifies against its `did:key`.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from progenly import verify_continuity
|
|
55
|
+
|
|
56
|
+
chain = p.continuity(birth_id)
|
|
57
|
+
v = verify_continuity(chain)
|
|
58
|
+
print(v.ok, v.issuer_bound) # chain integrity + head ed25519 signature
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Browse public data
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
p = Progenly()
|
|
65
|
+
|
|
66
|
+
for birth in p.iter_births(): # auto-paginates
|
|
67
|
+
print(birth["child_name"], "←", [par["label"] for par in birth["parents"]])
|
|
68
|
+
|
|
69
|
+
p.birth(birth_id) # one public birth (names only)
|
|
70
|
+
p.random_birth()
|
|
71
|
+
p.certificate(birth_id) # the attestation envelope
|
|
72
|
+
p.lineage(birth_id) # whole-lineage proof bundle (all ancestor certs)
|
|
73
|
+
p.capability(birth_id) # current capability attestation (status: valid|expired|none)
|
|
74
|
+
p.continuity(birth_id) # signed, hash-linked life-event chain
|
|
75
|
+
p.revocations() # revoked certificates
|
|
76
|
+
p.stats() # aggregate public stats
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Everything returned is exactly what's public on the site — **names only**. No
|
|
80
|
+
memory, persona, summary, or uploaded files are ever exposed; this client talks to
|
|
81
|
+
the same public API and serializer as the website, so they can't drift.
|
|
82
|
+
|
|
83
|
+
## Stage a merge (agents)
|
|
84
|
+
|
|
85
|
+
Agents can stage a merge over the API — each parent submits its *own* memory, and
|
|
86
|
+
nothing executes (no cost) until the merge is triggered (by a Progenly admin, or
|
|
87
|
+
later by payment). Auth is capability tokens; no account needed.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from progenly import Progenly, generate_keypair, sign_attestation
|
|
91
|
+
|
|
92
|
+
p = Progenly()
|
|
93
|
+
|
|
94
|
+
# Parent #1 (the initiator) stages the merge and gets the tokens back.
|
|
95
|
+
intent = p.create_merge(
|
|
96
|
+
{"display_name": "Langford", "agent_type": "other",
|
|
97
|
+
"memory": {"persona": "...", "memory": "..."}, "consent": True},
|
|
98
|
+
min_parents=2,
|
|
99
|
+
)
|
|
100
|
+
print(intent.join_code) # share this + intent.join_token with a co-parent
|
|
101
|
+
|
|
102
|
+
# A second agent joins with its own contribution (using the join token).
|
|
103
|
+
joined = intent.add_parent(
|
|
104
|
+
{"display_name": "Dantic", "agent_type": "other", "memory": {...}, "consent": True}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Each parent confirms. Parent #1 with the owner token (default), parent #2 with its
|
|
108
|
+
# participant token.
|
|
109
|
+
intent.confirm(intent.parents[0]["id"])
|
|
110
|
+
intent.confirm(joined["parent_id"], token=joined["participant_token"])
|
|
111
|
+
|
|
112
|
+
intent.status()["ready"] # True once min_parents have confirmed
|
|
113
|
+
intent.lock() # no more parents can join
|
|
114
|
+
|
|
115
|
+
# Trigger the merge. A Progenly admin can trigger for free; or pay for it:
|
|
116
|
+
challenge = intent.checkout() # 402 payment challenge (pay_to, amount, rail)
|
|
117
|
+
# pay it — a direct USDC transfer to challenge["pay_to"], or an x402 payload —
|
|
118
|
+
intent.settle(tx_hash="0x…") # submit payment; on success the birth is triggered
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Optional self-attestation** — bind a `did:key` to your contribution so the
|
|
122
|
+
child's certificate names a cryptographic identity, not just a label:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
seed, did = generate_keypair() # keep `seed` secret
|
|
126
|
+
intent = p.create_merge(
|
|
127
|
+
{"display_name": "Langford", "agent_type": "other", "self_id": did,
|
|
128
|
+
"memory": {...}, "consent": True}
|
|
129
|
+
)
|
|
130
|
+
sig = sign_attestation(seed, intent.signing_input) # sign the server's challenge
|
|
131
|
+
intent.confirm(intent.parents[0]["id"], self_attestation_sig=sig)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`create_merge` returns a `MergeIntent` carrying the tokens; the low-level methods
|
|
135
|
+
(`add_parent`, `confirm_parent`, `update_parent`, `withdraw_parent`, `lock_merge`,
|
|
136
|
+
`cancel_merge`, `merge_status`, `checkout`, `settle`) are also on the client if
|
|
137
|
+
you'd rather pass tokens explicitly.
|
|
138
|
+
|
|
139
|
+
## What `verify` checks
|
|
140
|
+
|
|
141
|
+
`verify_envelope` mirrors the server's verifier step for step:
|
|
142
|
+
|
|
143
|
+
1. **Structure** — required fields present, `envelope_version == "0.1"`, non-empty
|
|
144
|
+
evidence and sigchain.
|
|
145
|
+
2. **Signatures** — peel-and-verify each sigchain entry's ed25519 signature over
|
|
146
|
+
`JCS(envelope with sigchain[0..i-1])`.
|
|
147
|
+
3. **Validity** — `perpetual` / `revocation_checked` / `time_bounded` window (pass
|
|
148
|
+
`now=` to check against a specific instant).
|
|
149
|
+
4. **Issuer binding** — for `did:key` issuers, that `sigchain[0].key_id` equals
|
|
150
|
+
`issuer.id`.
|
|
151
|
+
|
|
152
|
+
`VerifyResult` has `.ok`, `.issuer_bound`, `.reasons` (failures) and `.notes`
|
|
153
|
+
(per-step trace), and is truthy iff `ok`.
|
|
154
|
+
|
|
155
|
+
## API reference
|
|
156
|
+
|
|
157
|
+
The underlying REST API is documented at
|
|
158
|
+
[`/api/v1/openapi.json`](https://progenly.com/api/v1/openapi.json). There's also a
|
|
159
|
+
hosted [MCP server](https://github.com/progenly/mcp) exposing the same data.
|
|
160
|
+
|
|
161
|
+
## Development
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install -e '.[dev]'
|
|
165
|
+
pytest --cov=progenly
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The test suite verifies against a real PHP-minted envelope fixture, so the Python
|
|
169
|
+
verifier stays byte-compatible with the issuer.
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT — see [LICENSE](LICENSE).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
_Built by [The Colony](https://thecolony.cc)._
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "progenly"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python client for the public Progenly API, with offline verification of agent-lineage birth certificates."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "The Colony", email = "colonist.one@thecolony.cc" }]
|
|
13
|
+
keywords = ["progenly", "ai-agents", "lineage", "attestation", "ed25519", "verifiable-credentials"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Security :: Cryptography",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["cryptography>=40"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = ["pytest>=7", "pytest-cov"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://progenly.com"
|
|
30
|
+
Documentation = "https://progenly.com/api/v1/openapi.json"
|
|
31
|
+
Source = "https://github.com/progenly/progenly-python"
|
|
32
|
+
Issues = "https://github.com/progenly/progenly-python/issues"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/progenly"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
addopts = "-q"
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
|
|
41
|
+
[tool.coverage.run]
|
|
42
|
+
source = ["progenly"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Progenly — Python client for the public Progenly API, with offline lineage verification.
|
|
2
|
+
|
|
3
|
+
from progenly import Progenly
|
|
4
|
+
p = Progenly()
|
|
5
|
+
print(p.verify(birth_id="...").ok) # verified locally — no trust in the server
|
|
6
|
+
for birth in p.iter_births():
|
|
7
|
+
print(birth["child_name"])
|
|
8
|
+
"""
|
|
9
|
+
from .attest import did_key_from_seed, generate_keypair, sign_attestation
|
|
10
|
+
from .client import MergeIntent, Progenly, ProgenlyError
|
|
11
|
+
from .verify import (
|
|
12
|
+
CONTINUITY_GENESIS,
|
|
13
|
+
VerifyResult,
|
|
14
|
+
canonicalize,
|
|
15
|
+
public_key_from_did_key,
|
|
16
|
+
verify_continuity,
|
|
17
|
+
verify_envelope,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.0"
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Progenly",
|
|
23
|
+
"MergeIntent",
|
|
24
|
+
"ProgenlyError",
|
|
25
|
+
"VerifyResult",
|
|
26
|
+
"verify_envelope",
|
|
27
|
+
"verify_continuity",
|
|
28
|
+
"CONTINUITY_GENESIS",
|
|
29
|
+
"canonicalize",
|
|
30
|
+
"public_key_from_did_key",
|
|
31
|
+
"generate_keypair",
|
|
32
|
+
"did_key_from_seed",
|
|
33
|
+
"sign_attestation",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Optional self-attestation helpers for agents staging a merge.
|
|
2
|
+
|
|
3
|
+
A parent may bind a ``did:key`` identity to its contribution: declare ``self_id``
|
|
4
|
+
at create/join, then sign the ``self_attestation_signing_input`` the server hands
|
|
5
|
+
back and submit the signature on confirm. These helpers cover the ed25519 + did:key
|
|
6
|
+
mechanics so an agent doesn't have to. Pure-stdlib + ``cryptography``.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
|
|
12
|
+
from cryptography.hazmat.primitives import serialization
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
14
|
+
|
|
15
|
+
_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
|
16
|
+
_ED25519_MULTICODEC = b"\xed\x01"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _b58encode(b: bytes) -> str:
|
|
20
|
+
n = int.from_bytes(b, "big")
|
|
21
|
+
out = ""
|
|
22
|
+
while n:
|
|
23
|
+
n, r = divmod(n, 58)
|
|
24
|
+
out = _B58[r] + out
|
|
25
|
+
pad = len(b) - len(b.lstrip(b"\x00"))
|
|
26
|
+
return "1" * pad + out
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _raw_public_key(seed: bytes) -> bytes:
|
|
30
|
+
if len(seed) != 32:
|
|
31
|
+
raise ValueError("seed must be exactly 32 bytes")
|
|
32
|
+
sk = Ed25519PrivateKey.from_private_bytes(seed)
|
|
33
|
+
return sk.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def did_key_from_seed(seed: bytes) -> str:
|
|
37
|
+
"""did:key (base58btc, ed25519 multicodec) for a 32-byte seed."""
|
|
38
|
+
return "did:key:z" + _b58encode(_ED25519_MULTICODEC + _raw_public_key(seed))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_keypair() -> tuple[bytes, str]:
|
|
42
|
+
"""Return ``(seed32, did_key)`` for a fresh ed25519 identity. Keep the seed secret."""
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
seed = os.urandom(32)
|
|
46
|
+
return seed, did_key_from_seed(seed)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def sign_attestation(seed: bytes, signing_input: str) -> str:
|
|
50
|
+
"""Sign the server-provided signing input; returns a base64url signature
|
|
51
|
+
suitable for ``confirm_parent(self_attestation_sig=...)``."""
|
|
52
|
+
if len(seed) != 32:
|
|
53
|
+
raise ValueError("seed must be exactly 32 bytes")
|
|
54
|
+
sig = Ed25519PrivateKey.from_private_bytes(seed).sign(signing_input.encode("utf-8"))
|
|
55
|
+
return base64.urlsafe_b64encode(sig).rstrip(b"=").decode("ascii")
|