sealkeeper 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.
- sealkeeper-0.1.0/.gitignore +6 -0
- sealkeeper-0.1.0/LICENSE +21 -0
- sealkeeper-0.1.0/PKG-INFO +162 -0
- sealkeeper-0.1.0/README.md +142 -0
- sealkeeper-0.1.0/pyproject.toml +55 -0
- sealkeeper-0.1.0/src/sealkeeper/__init__.py +37 -0
- sealkeeper-0.1.0/src/sealkeeper/_protocol.py +277 -0
- sealkeeper-0.1.0/src/sealkeeper/_version.py +1 -0
- sealkeeper-0.1.0/src/sealkeeper/client.py +303 -0
- sealkeeper-0.1.0/src/sealkeeper/errors.py +23 -0
- sealkeeper-0.1.0/src/sealkeeper/gate.py +259 -0
- sealkeeper-0.1.0/src/sealkeeper/lookup.py +120 -0
- sealkeeper-0.1.0/src/sealkeeper/py.typed +0 -0
- sealkeeper-0.1.0/src/sealkeeper/seal.py +339 -0
- sealkeeper-0.1.0/tests/conftest.py +7 -0
- sealkeeper-0.1.0/tests/fixtures/agent.json +7 -0
- sealkeeper-0.1.0/tests/fixtures/index.json +786 -0
- sealkeeper-0.1.0/tests/fixtures/keys.json +11 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v1-bad-signature.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v1-expired.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v1-unknown-kid.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v1-valid.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v4-valid.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v6-valid.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v7-listed-x402-valid.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v7-service-valid.txt +1 -0
- sealkeeper-0.1.0/tests/fixtures/seals/v7-valid.txt +1 -0
- sealkeeper-0.1.0/tests/recorded/get-trust-is-error.json +19 -0
- sealkeeper-0.1.0/tests/recorded/get-trust-not-found.json +28 -0
- sealkeeper-0.1.0/tests/recorded/get-trust-shown.json +42 -0
- sealkeeper-0.1.0/tests/recorded/keys-production.json +18 -0
- sealkeeper-0.1.0/tests/recorded/query-400.json +19 -0
- sealkeeper-0.1.0/tests/recorded/unknown-tool.json +14 -0
- sealkeeper-0.1.0/tests/support.py +230 -0
- sealkeeper-0.1.0/tests/test_did.py +84 -0
- sealkeeper-0.1.0/tests/test_fixtures.py +56 -0
- sealkeeper-0.1.0/tests/test_gate.py +433 -0
- sealkeeper-0.1.0/tests/test_keys.py +158 -0
- sealkeeper-0.1.0/tests/test_live.py +37 -0
- sealkeeper-0.1.0/tests/test_lookup.py +219 -0
- sealkeeper-0.1.0/tests/test_seal.py +283 -0
- sealkeeper-0.1.0/tests/test_transport.py +267 -0
sealkeeper-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The SealKeeper Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sealkeeper
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Look up and verify SealKeeper SEALs, and gate agent and service calls on them.
|
|
5
|
+
Project-URL: Homepage, https://sealkeeper.run
|
|
6
|
+
Project-URL: SEAL Standard, https://sealkeeper.run/seal/standard
|
|
7
|
+
Project-URL: Repository, https://github.com/sealkeeper-dev/sealkeeper-py
|
|
8
|
+
Project-URL: Issues, https://github.com/sealkeeper-dev/sealkeeper-py/issues
|
|
9
|
+
Author: The SealKeeper Authors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: cryptography>=42
|
|
18
|
+
Requires-Dist: httpx<1,>=0.27
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# sealkeeper
|
|
22
|
+
|
|
23
|
+
Look up an AI agent or service on [SealKeeper](https://sealkeeper.run), verify its SEAL on your own machine and decide whether to call it.
|
|
24
|
+
|
|
25
|
+
A SEAL is SealKeeper's signed credential for an agent or a service. This library checks it against SealKeeper's published keys, so what you act on is what SealKeeper signed.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pip install sealkeeper
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python 3.10 or later. It depends on `httpx` and `cryptography` only.
|
|
34
|
+
|
|
35
|
+
## Look up
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sealkeeper import SealKeeper
|
|
39
|
+
|
|
40
|
+
with SealKeeper() as sk:
|
|
41
|
+
found = sk.lookup("alice/helper")
|
|
42
|
+
|
|
43
|
+
if found.verified:
|
|
44
|
+
print(found.kind, found.level, found.trust)
|
|
45
|
+
else:
|
|
46
|
+
print(found.code)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A target is any of these.
|
|
50
|
+
|
|
51
|
+
- A handle, such as `alice/helper`.
|
|
52
|
+
- A page URL, `https://sealkeeper.run/agents/alice/helper` or `https://sealkeeper.run/agents/<agent id>`.
|
|
53
|
+
- An agent id, 43 characters of base64url, or its `did:key`.
|
|
54
|
+
- A card URL, or an MCP server's URL. Pass it as it is listed, since the host is the only part read case blind.
|
|
55
|
+
|
|
56
|
+
`lookup` returns a `Lookup`. `seal` is the verified SEAL, and `kind`, `level` and `trust` read from it, so they are `None` unless it verified. Without one, `code` says why.
|
|
57
|
+
|
|
58
|
+
| `code` | Meaning |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `broken` | The SEAL did not verify. `broken` holds the reason |
|
|
61
|
+
| `withheld` | SealKeeper withholds the SEAL. `reason` and `since` say why and from when |
|
|
62
|
+
| `no_seal` | No SEAL while the agent is dormant. `dormant_days` says how long |
|
|
63
|
+
| `checking` | SealKeeper is still checking it |
|
|
64
|
+
| `delisted` | SealKeeper no longer lists it |
|
|
65
|
+
| `not_found` | The target names no agent or service |
|
|
66
|
+
| `unknown` | No SEAL, and the answer says nothing more |
|
|
67
|
+
|
|
68
|
+
A code this version does not know comes through as sent, with the whole answer in `raw`. `handle` is the handle SealKeeper answered, for display only.
|
|
69
|
+
|
|
70
|
+
## Verify
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from sealkeeper import SealBroken, SealKeeper
|
|
74
|
+
|
|
75
|
+
with SealKeeper() as sk:
|
|
76
|
+
try:
|
|
77
|
+
seal = sk.verify(seal_text, expected_agent_id)
|
|
78
|
+
except SealBroken as broken:
|
|
79
|
+
print(broken.reason)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`expected_agent_id` is the agent you are about to trust, as an agent id or a `did:key`. A valid SEAL for another agent is `wrong_agent`.
|
|
83
|
+
|
|
84
|
+
`verify` runs the checks of the [SEAL Standard](https://sealkeeper.run/seal/standard) in order. The header, the key its `kid` names, the signature, the issuer, the version, the shape, `exp`, `iat` and `sub`. The reasons are in `SEAL_BROKEN_REASONS`.
|
|
85
|
+
|
|
86
|
+
- `malformed`, `unknown_kid`, `bad_signature`, `wrong_issuer`, `unsupported_version`, `expired`, `not_yet_valid`, `wrong_agent`
|
|
87
|
+
|
|
88
|
+
It returns a `VerifiedSeal` with `agent_id`, `kind` (`agent` or `service`), `level` (`none`, `bronze`, `silver` or `gold`), `trust` (the Trust Score, `None` before version 4), `issued_at`, `expires_at` and `payload`.
|
|
89
|
+
|
|
90
|
+
The keys come from `https://sealkeeper.run/.well-known/seal.json`. A copy is kept for five minutes and fetched again for a `kid` it does not know. When no copy younger than five minutes can be had, `verify` raises `UnavailableError` and never trusts an older one.
|
|
91
|
+
|
|
92
|
+
The shape check covers the fields this library reads and returns, `sub`, `iat`, `exp`, `level`, `trust`, `subject.kind` and `listing.card_url`. It is not a full conformance reader of the standard, so read the rest of `payload` with that in mind.
|
|
93
|
+
|
|
94
|
+
With a key document of your own, `verify_with_keys(seal, read_keys(document), now, expected_agent_id)` does the same offline.
|
|
95
|
+
|
|
96
|
+
`verify_seal(seal, agent=None, at=None)` asks SealKeeper to check a SEAL instead, and answers as SealKeeper does, with its record of holds.
|
|
97
|
+
|
|
98
|
+
## Gate
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from sealkeeper import Gate
|
|
102
|
+
|
|
103
|
+
gate = Gate()
|
|
104
|
+
decision = gate.check("https://mcp.example.com/mcp", "service")
|
|
105
|
+
if decision.passed:
|
|
106
|
+
call_it()
|
|
107
|
+
else:
|
|
108
|
+
print(decision.code, decision.reason)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`check(target, kind)` answers `Pass` or `Block`, each with `passed` and `code`. A pass on a SEAL is `sealed`, with the `VerifiedSeal` in `seal`. `kind` is `"agent"` or `"service"`. `acheck` is the same, awaited. Neither raises for a decision, so a check never ends your agent's run.
|
|
112
|
+
|
|
113
|
+
A pass needs a current, valid SEAL of that kind at bronze or above. For a card or MCP server URL, the SEAL must also name that URL.
|
|
114
|
+
|
|
115
|
+
- `Gate(allow_unsealed=True)` also passes a target SealKeeper does not know, one it is still checking and one with no SEAL while dormant. `Pass.code` then says which, and `Pass.seal` is `None`. Everything else still blocks.
|
|
116
|
+
- `cache_ttl` keeps each lookup for that many seconds, 300 by default, 0 to 86,400. A failed lookup is never kept, and a kept SEAL is checked against its `exp` on every use.
|
|
117
|
+
|
|
118
|
+
Each decision writes one INFO line to the `sealkeeper` logger, with the code, the kind and the agent's handle, or the host of a URL. Never the SEAL or a full URL.
|
|
119
|
+
|
|
120
|
+
### Block codes
|
|
121
|
+
|
|
122
|
+
`BLOCK_CODES` holds every one.
|
|
123
|
+
|
|
124
|
+
| Code | Meaning |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `unavailable` | SealKeeper could not be reached, timed out or asked to slow down |
|
|
127
|
+
| `bad_target` | SealKeeper refused the target. `reason` is its line |
|
|
128
|
+
| `error` | SealKeeper answered something this library cannot read |
|
|
129
|
+
| `not_found` | The target names no agent or service |
|
|
130
|
+
| `checking` | SealKeeper is still checking it |
|
|
131
|
+
| `no_seal` | No SEAL while it is dormant |
|
|
132
|
+
| `withheld` | SealKeeper withholds its SEAL. `reason` names why |
|
|
133
|
+
| `delisted` | SealKeeper no longer lists it |
|
|
134
|
+
| `unknown` | No SEAL, and nothing more said |
|
|
135
|
+
| `wrong_url` | The SEAL is for another URL than the target |
|
|
136
|
+
| `wrong_kind` | The SEAL is for the other kind |
|
|
137
|
+
| `below_bronze` | The SEAL's level is none |
|
|
138
|
+
| `malformed` and the other seven reasons of `SEAL_BROKEN_REASONS` | The SEAL did not verify |
|
|
139
|
+
|
|
140
|
+
## Errors
|
|
141
|
+
|
|
142
|
+
`lookup`, `get_trust`, `verify_seal` and `verify` raise these, all `SealKeeperError`.
|
|
143
|
+
|
|
144
|
+
- `BadTargetError`, SealKeeper refused the target, with its line.
|
|
145
|
+
- `UnavailableError`, SealKeeper could not be reached in 5 seconds or kept answering 429.
|
|
146
|
+
- `SealBroken`, from `verify`, with `reason`.
|
|
147
|
+
- `SealKeeperError` itself, for an answer this library cannot read.
|
|
148
|
+
|
|
149
|
+
## Options
|
|
150
|
+
|
|
151
|
+
Both `SealKeeper` and `AsyncSealKeeper` take these, all optional.
|
|
152
|
+
|
|
153
|
+
- `http_client`, an `httpx.Client` or `httpx.AsyncClient` to send through.
|
|
154
|
+
- `clock`, a function answering Unix seconds, for the SEAL checks.
|
|
155
|
+
- `user_agent_suffix`, added to the `User-Agent`, such as `"my-wrapper/1.0.0"`.
|
|
156
|
+
- `sleep`, what waits before the one retry of a 429.
|
|
157
|
+
|
|
158
|
+
`Gate` takes `client`, `async_client` and `clock` the same way.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# sealkeeper
|
|
2
|
+
|
|
3
|
+
Look up an AI agent or service on [SealKeeper](https://sealkeeper.run), verify its SEAL on your own machine and decide whether to call it.
|
|
4
|
+
|
|
5
|
+
A SEAL is SealKeeper's signed credential for an agent or a service. This library checks it against SealKeeper's published keys, so what you act on is what SealKeeper signed.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install sealkeeper
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python 3.10 or later. It depends on `httpx` and `cryptography` only.
|
|
14
|
+
|
|
15
|
+
## Look up
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from sealkeeper import SealKeeper
|
|
19
|
+
|
|
20
|
+
with SealKeeper() as sk:
|
|
21
|
+
found = sk.lookup("alice/helper")
|
|
22
|
+
|
|
23
|
+
if found.verified:
|
|
24
|
+
print(found.kind, found.level, found.trust)
|
|
25
|
+
else:
|
|
26
|
+
print(found.code)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A target is any of these.
|
|
30
|
+
|
|
31
|
+
- A handle, such as `alice/helper`.
|
|
32
|
+
- A page URL, `https://sealkeeper.run/agents/alice/helper` or `https://sealkeeper.run/agents/<agent id>`.
|
|
33
|
+
- An agent id, 43 characters of base64url, or its `did:key`.
|
|
34
|
+
- A card URL, or an MCP server's URL. Pass it as it is listed, since the host is the only part read case blind.
|
|
35
|
+
|
|
36
|
+
`lookup` returns a `Lookup`. `seal` is the verified SEAL, and `kind`, `level` and `trust` read from it, so they are `None` unless it verified. Without one, `code` says why.
|
|
37
|
+
|
|
38
|
+
| `code` | Meaning |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `broken` | The SEAL did not verify. `broken` holds the reason |
|
|
41
|
+
| `withheld` | SealKeeper withholds the SEAL. `reason` and `since` say why and from when |
|
|
42
|
+
| `no_seal` | No SEAL while the agent is dormant. `dormant_days` says how long |
|
|
43
|
+
| `checking` | SealKeeper is still checking it |
|
|
44
|
+
| `delisted` | SealKeeper no longer lists it |
|
|
45
|
+
| `not_found` | The target names no agent or service |
|
|
46
|
+
| `unknown` | No SEAL, and the answer says nothing more |
|
|
47
|
+
|
|
48
|
+
A code this version does not know comes through as sent, with the whole answer in `raw`. `handle` is the handle SealKeeper answered, for display only.
|
|
49
|
+
|
|
50
|
+
## Verify
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from sealkeeper import SealBroken, SealKeeper
|
|
54
|
+
|
|
55
|
+
with SealKeeper() as sk:
|
|
56
|
+
try:
|
|
57
|
+
seal = sk.verify(seal_text, expected_agent_id)
|
|
58
|
+
except SealBroken as broken:
|
|
59
|
+
print(broken.reason)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`expected_agent_id` is the agent you are about to trust, as an agent id or a `did:key`. A valid SEAL for another agent is `wrong_agent`.
|
|
63
|
+
|
|
64
|
+
`verify` runs the checks of the [SEAL Standard](https://sealkeeper.run/seal/standard) in order. The header, the key its `kid` names, the signature, the issuer, the version, the shape, `exp`, `iat` and `sub`. The reasons are in `SEAL_BROKEN_REASONS`.
|
|
65
|
+
|
|
66
|
+
- `malformed`, `unknown_kid`, `bad_signature`, `wrong_issuer`, `unsupported_version`, `expired`, `not_yet_valid`, `wrong_agent`
|
|
67
|
+
|
|
68
|
+
It returns a `VerifiedSeal` with `agent_id`, `kind` (`agent` or `service`), `level` (`none`, `bronze`, `silver` or `gold`), `trust` (the Trust Score, `None` before version 4), `issued_at`, `expires_at` and `payload`.
|
|
69
|
+
|
|
70
|
+
The keys come from `https://sealkeeper.run/.well-known/seal.json`. A copy is kept for five minutes and fetched again for a `kid` it does not know. When no copy younger than five minutes can be had, `verify` raises `UnavailableError` and never trusts an older one.
|
|
71
|
+
|
|
72
|
+
The shape check covers the fields this library reads and returns, `sub`, `iat`, `exp`, `level`, `trust`, `subject.kind` and `listing.card_url`. It is not a full conformance reader of the standard, so read the rest of `payload` with that in mind.
|
|
73
|
+
|
|
74
|
+
With a key document of your own, `verify_with_keys(seal, read_keys(document), now, expected_agent_id)` does the same offline.
|
|
75
|
+
|
|
76
|
+
`verify_seal(seal, agent=None, at=None)` asks SealKeeper to check a SEAL instead, and answers as SealKeeper does, with its record of holds.
|
|
77
|
+
|
|
78
|
+
## Gate
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from sealkeeper import Gate
|
|
82
|
+
|
|
83
|
+
gate = Gate()
|
|
84
|
+
decision = gate.check("https://mcp.example.com/mcp", "service")
|
|
85
|
+
if decision.passed:
|
|
86
|
+
call_it()
|
|
87
|
+
else:
|
|
88
|
+
print(decision.code, decision.reason)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`check(target, kind)` answers `Pass` or `Block`, each with `passed` and `code`. A pass on a SEAL is `sealed`, with the `VerifiedSeal` in `seal`. `kind` is `"agent"` or `"service"`. `acheck` is the same, awaited. Neither raises for a decision, so a check never ends your agent's run.
|
|
92
|
+
|
|
93
|
+
A pass needs a current, valid SEAL of that kind at bronze or above. For a card or MCP server URL, the SEAL must also name that URL.
|
|
94
|
+
|
|
95
|
+
- `Gate(allow_unsealed=True)` also passes a target SealKeeper does not know, one it is still checking and one with no SEAL while dormant. `Pass.code` then says which, and `Pass.seal` is `None`. Everything else still blocks.
|
|
96
|
+
- `cache_ttl` keeps each lookup for that many seconds, 300 by default, 0 to 86,400. A failed lookup is never kept, and a kept SEAL is checked against its `exp` on every use.
|
|
97
|
+
|
|
98
|
+
Each decision writes one INFO line to the `sealkeeper` logger, with the code, the kind and the agent's handle, or the host of a URL. Never the SEAL or a full URL.
|
|
99
|
+
|
|
100
|
+
### Block codes
|
|
101
|
+
|
|
102
|
+
`BLOCK_CODES` holds every one.
|
|
103
|
+
|
|
104
|
+
| Code | Meaning |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `unavailable` | SealKeeper could not be reached, timed out or asked to slow down |
|
|
107
|
+
| `bad_target` | SealKeeper refused the target. `reason` is its line |
|
|
108
|
+
| `error` | SealKeeper answered something this library cannot read |
|
|
109
|
+
| `not_found` | The target names no agent or service |
|
|
110
|
+
| `checking` | SealKeeper is still checking it |
|
|
111
|
+
| `no_seal` | No SEAL while it is dormant |
|
|
112
|
+
| `withheld` | SealKeeper withholds its SEAL. `reason` names why |
|
|
113
|
+
| `delisted` | SealKeeper no longer lists it |
|
|
114
|
+
| `unknown` | No SEAL, and nothing more said |
|
|
115
|
+
| `wrong_url` | The SEAL is for another URL than the target |
|
|
116
|
+
| `wrong_kind` | The SEAL is for the other kind |
|
|
117
|
+
| `below_bronze` | The SEAL's level is none |
|
|
118
|
+
| `malformed` and the other seven reasons of `SEAL_BROKEN_REASONS` | The SEAL did not verify |
|
|
119
|
+
|
|
120
|
+
## Errors
|
|
121
|
+
|
|
122
|
+
`lookup`, `get_trust`, `verify_seal` and `verify` raise these, all `SealKeeperError`.
|
|
123
|
+
|
|
124
|
+
- `BadTargetError`, SealKeeper refused the target, with its line.
|
|
125
|
+
- `UnavailableError`, SealKeeper could not be reached in 5 seconds or kept answering 429.
|
|
126
|
+
- `SealBroken`, from `verify`, with `reason`.
|
|
127
|
+
- `SealKeeperError` itself, for an answer this library cannot read.
|
|
128
|
+
|
|
129
|
+
## Options
|
|
130
|
+
|
|
131
|
+
Both `SealKeeper` and `AsyncSealKeeper` take these, all optional.
|
|
132
|
+
|
|
133
|
+
- `http_client`, an `httpx.Client` or `httpx.AsyncClient` to send through.
|
|
134
|
+
- `clock`, a function answering Unix seconds, for the SEAL checks.
|
|
135
|
+
- `user_agent_suffix`, added to the `User-Agent`, such as `"my-wrapper/1.0.0"`.
|
|
136
|
+
- `sleep`, what waits before the one retry of a 429.
|
|
137
|
+
|
|
138
|
+
`Gate` takes `client`, `async_client` and `clock` the same way.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sealkeeper"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Look up and verify SealKeeper SEALs, and gate agent and service calls on them."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "The SealKeeper Authors" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["httpx>=0.27,<1", "cryptography>=42"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.14",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://sealkeeper.run"
|
|
20
|
+
"SEAL Standard" = "https://sealkeeper.run/seal/standard"
|
|
21
|
+
Repository = "https://github.com/sealkeeper-dev/sealkeeper-py"
|
|
22
|
+
Issues = "https://github.com/sealkeeper-dev/sealkeeper-py/issues"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling>=1.27"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.version]
|
|
29
|
+
path = "src/sealkeeper/_version.py"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/sealkeeper"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
only-include = ["src/sealkeeper", "tests", "README.md", "LICENSE"]
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = ["pytest>=8.3", "ruff>=0.14"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
target-version = "py310"
|
|
42
|
+
line-length = 88
|
|
43
|
+
include = ["*.py"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.isort]
|
|
49
|
+
known-first-party = ["sealkeeper", "support"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
# Async tests run on anyio. pytest-asyncio stays off where it is installed.
|
|
54
|
+
addopts = "--strict-markers -p no:asyncio"
|
|
55
|
+
markers = ["live: calls production, runs only with SEALKEEPER_LIVE=1"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Look up and verify SealKeeper SEALs, and gate calls on them."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .client import AsyncSealKeeper, SealKeeper
|
|
5
|
+
from .errors import BadTargetError, SealBroken, SealKeeperError, UnavailableError
|
|
6
|
+
from .gate import BLOCK_CODES, PASS_CODES, Block, Gate, Pass
|
|
7
|
+
from .lookup import Lookup
|
|
8
|
+
from .seal import (
|
|
9
|
+
SEAL_BROKEN_REASONS,
|
|
10
|
+
SEAL_VERSIONS,
|
|
11
|
+
VerifiedSeal,
|
|
12
|
+
agent_id_from_did_key,
|
|
13
|
+
read_keys,
|
|
14
|
+
verify_with_keys,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BLOCK_CODES",
|
|
19
|
+
"PASS_CODES",
|
|
20
|
+
"SEAL_BROKEN_REASONS",
|
|
21
|
+
"SEAL_VERSIONS",
|
|
22
|
+
"AsyncSealKeeper",
|
|
23
|
+
"BadTargetError",
|
|
24
|
+
"Block",
|
|
25
|
+
"Gate",
|
|
26
|
+
"Lookup",
|
|
27
|
+
"Pass",
|
|
28
|
+
"SealBroken",
|
|
29
|
+
"SealKeeper",
|
|
30
|
+
"SealKeeperError",
|
|
31
|
+
"UnavailableError",
|
|
32
|
+
"VerifiedSeal",
|
|
33
|
+
"__version__",
|
|
34
|
+
"agent_id_from_did_key",
|
|
35
|
+
"read_keys",
|
|
36
|
+
"verify_with_keys",
|
|
37
|
+
]
|