signafaide 0.2.1__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.
- signafaide-0.2.1/.gitignore +5 -0
- signafaide-0.2.1/PKG-INFO +238 -0
- signafaide-0.2.1/README.md +217 -0
- signafaide-0.2.1/pyproject.toml +42 -0
- signafaide-0.2.1/src/signafaide/__init__.py +263 -0
- signafaide-0.2.1/src/signafaide/__main__.py +5 -0
- signafaide-0.2.1/src/signafaide/_plural.py +6 -0
- signafaide-0.2.1/src/signafaide/_version.py +1 -0
- signafaide-0.2.1/src/signafaide/bitcoin.py +102 -0
- signafaide-0.2.1/src/signafaide/cli.py +449 -0
- signafaide-0.2.1/src/signafaide/crypto.py +196 -0
- signafaide-0.2.1/src/signafaide/decorator.py +318 -0
- signafaide-0.2.1/src/signafaide/drand.py +75 -0
- signafaide-0.2.1/src/signafaide/experiments.py +210 -0
- signafaide-0.2.1/src/signafaide/http.py +148 -0
- signafaide-0.2.1/src/signafaide/jcs.py +125 -0
- signafaide-0.2.1/src/signafaide/ots.py +256 -0
- signafaide-0.2.1/src/signafaide/records.py +379 -0
- signafaide-0.2.1/src/signafaide/replay.py +302 -0
- signafaide-0.2.1/src/signafaide/runtime.py +1047 -0
- signafaide-0.2.1/src/signafaide/state.py +993 -0
- signafaide-0.2.1/src/signafaide/telemetry.py +65 -0
- signafaide-0.2.1/src/signafaide/verifier.py +1330 -0
- signafaide-0.2.1/tests/conftest.py +74 -0
- signafaide-0.2.1/tests/fake_service.py +275 -0
- signafaide-0.2.1/tests/helpers.py +23 -0
- signafaide-0.2.1/tests/test_adapters.py +143 -0
- signafaide-0.2.1/tests/test_cli.py +232 -0
- signafaide-0.2.1/tests/test_core.py +236 -0
- signafaide-0.2.1/tests/test_record.py +431 -0
- signafaide-0.2.1/tests/test_replay_compare.py +132 -0
- signafaide-0.2.1/tests/test_round3.py +318 -0
- signafaide-0.2.1/tests/test_round4.py +205 -0
- signafaide-0.2.1/tests/test_round5.py +120 -0
- signafaide-0.2.1/tests/test_round6.py +147 -0
- signafaide-0.2.1/tests/test_state.py +246 -0
- signafaide-0.2.1/tests/test_telemetry.py +149 -0
- signafaide-0.2.1/tests/test_vectors.py +256 -0
- signafaide-0.2.1/tests/test_verify.py +421 -0
- signafaide-0.2.1/uv.lock +4185 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: signafaide
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Signed, chained records of what your AI agents decided. Hash-only by default.
|
|
5
|
+
Project-URL: Homepage, https://signafaide.co.uk
|
|
6
|
+
Project-URL: Documentation, https://signafaide.co.uk/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/AIME-TECH-UK-LIMITED/signafaide-sdk
|
|
8
|
+
Project-URL: Changelog, https://github.com/AIME-TECH-UK-LIMITED/signafaide-sdk/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/AIME-TECH-UK-LIMITED/signafaide-sdk/issues
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: ai-agents,audit-trail,eu-ai-act,provenance,tamper-evident,verifiable
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Security :: Cryptography
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: cryptography>=41
|
|
18
|
+
Requires-Dist: py-ecc>=7
|
|
19
|
+
Provides-Extra: bls
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# signafaide
|
|
23
|
+
|
|
24
|
+
Signed, chained records of what your AI agents decided. Each record is signed with the agent's own key and chained on your machine; once you opt in, a witness (the signafAIde service, or one you host) co-signs and publicly timestamps it, so neither side can forge or edit a record alone.
|
|
25
|
+
|
|
26
|
+
- Nothing is sent until a person runs `signafaide init` or sets `SIGNAFAIDE_API_KEY`. After that each record sends its content hash with its tool name, agent name and time (and its chain links), never its inputs or outputs unless you turn on stored content. Tool and agent names are sent as you write them, so don't put personal data in them. Each content hash is salted with 16 random bytes that stay on your machine with the content, so the hash alone cannot be used to guess the content, even when it has only a few possible values.
|
|
27
|
+
- The sandbox `init` creates needs no account, email or card.
|
|
28
|
+
- To keep the evidence in your environment, self-host the witness and set `SIGNAFAIDE_URL` ([self-host](https://signafaide.co.uk/docs/self-host)).
|
|
29
|
+
- Apache-2.0, with a public protocol spec and an independent verifier.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add signafaide # a uv project
|
|
33
|
+
poetry add signafaide # a Poetry project
|
|
34
|
+
python3 -m venv .venv && .venv/bin/python -m pip install signafaide # otherwise, then add it to requirements.txt
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Not a bare `pip install`: most systems' Python refuses it outside a virtual environment (PEP 668, "externally-managed-environment").
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import signafaide
|
|
41
|
+
|
|
42
|
+
@signafaide.record
|
|
43
|
+
def approve_payout(claim_id: str, amount: float): ...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
signafaide verify # or .venv/bin/signafaide verify, uv run signafaide verify
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it. With no API key, records are signed, chained and kept in `.signafaide/`, and nothing is sent. `signafaide verify` checks every signature, chain link and content hash and ends with a result line that says how many of the 12 checks ran, for example `PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)`. A check that was not run is never counted as passed. It also says what still needs the witness:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Sending hashes to a witness
|
|
57
|
+
|
|
58
|
+
Co-signature and the public timestamp need a witness. A person opts in once:
|
|
59
|
+
|
|
60
|
+
- `signafaide init` creates a free sandbox (no account, email or card), saves `SIGNAFAIDE_API_KEY` to `.env`, adds `.env` to `.gitignore` and prints a link to claim it; or
|
|
61
|
+
- set `SIGNAFAIDE_API_KEY` (a live key from the console), with `SIGNAFAIDE_URL` for a self-hosted witness. `SIGNAFAIDE_URL` alone sends nothing.
|
|
62
|
+
|
|
63
|
+
Records written before the opt-in ship afterwards with their original times. The service accepts records up to 7 days old (an agent key's `valid_from` is at most 7 days before its registration); a local chain that starts earlier stays local, still verifies, and `signafaide flush` reports it as `too_old`. `signafaide.init()` in code only configures: it never creates a sandbox.
|
|
64
|
+
|
|
65
|
+
## What a call records
|
|
66
|
+
|
|
67
|
+
1. A `decision` record before the function runs: the tool name, plus `{"input": {<argument>: <value>}, "meta": {...}}`.
|
|
68
|
+
2. The function runs.
|
|
69
|
+
3. An `outcome` record after it: `{"output": ...}`, or `{"error": {"type", "message"}}`. The error is raised again unchanged.
|
|
70
|
+
|
|
71
|
+
Records are signed and chained locally in `.signafaide/` (synchronously, in a couple of milliseconds), then, once you opted in, shipped in the background. Sync and async functions both work.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
@signafaide.record(agent="claims-bot", name="approve_payout", meta={"model": "gpt-x"})
|
|
75
|
+
def approve(claim_id: str, amount: float): ...
|
|
76
|
+
|
|
77
|
+
signafaide.note("policy_loaded", {"version": 3}) # anything that is not a tool call
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Give each agent an explicit name, for example `@signafaide.record(agent="claims-bot")`; without one it is the project folder's name. It must match `^[a-z0-9][a-z0-9._-]{0,63}$`. In production, one agent name is one chain written by one running instance.
|
|
81
|
+
|
|
82
|
+
## Frameworks
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
tools = signafaide.wrap_tools({"lookup_order": lookup_order, "refund": refund}) # dict: key = name
|
|
86
|
+
tools = signafaide.wrap_tools([lookup_order, refund]) # list
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`wrap_tools` (and `wrap_tool` for one) also takes:
|
|
90
|
+
|
|
91
|
+
- **LangChain** tools (`@tool` or any `BaseTool`). The copy records each `invoke` / `ainvoke`, including tool calls, and still converts for `bind_tools`. The outcome holds the tool's own return value, not the `ToolMessage` LangChain wraps it in.
|
|
92
|
+
- **OpenAI Agents SDK** `FunctionTool`s (`@function_tool`). The copy records `on_invoke_tool`.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from agents import Agent, function_tool
|
|
96
|
+
|
|
97
|
+
@function_tool
|
|
98
|
+
def lookup_order(order_id: str) -> str: ...
|
|
99
|
+
|
|
100
|
+
agent = Agent(name="support", tools=signafaide.wrap_tools([lookup_order]))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Neither framework is a dependency: `wrap_tools` recognises the objects by their shape.
|
|
104
|
+
|
|
105
|
+
## Strict mode: no receipt, no action
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
@signafaide.record(strict=True)
|
|
109
|
+
def wire_funds(account: str, amount: float): ...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The decision is shipped first, and the function runs only after the service has returned its receipt. If the service can't be reached or refuses the record, or there is no API key (`no_key`), `signafaide.NotRecorded` is raised and the function does not run. Agent frameworks often turn a tool's error into a failed tool call and print nothing, so the first time a strict tool is refused in a process the SDK writes one warning line to stderr with the tool's name and the fix.
|
|
113
|
+
|
|
114
|
+
## Hash-only
|
|
115
|
+
|
|
116
|
+
By default the service receives only the envelope: the name, the time, the chain links and `content_hash`. The hash is `sha256(salt || JCS(content))` with a random 16-byte salt per record, kept next to the content in the local chain, so the service cannot confirm a guess of the content from the hash. When you store content with the service, the salt goes with it, so anyone holding the pack can check the content against the hash. Your inputs and outputs stay in `.signafaide/chain/<project>/<env>/<agent>.jsonl` (`.signafaide/chain/_pending/nokey/<agent>.jsonl` before the opt-in), which is your evidence store. To send the content too (encrypted at rest; live records need the Team plan):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
signafaide.init(store_content=True) # everything
|
|
120
|
+
@signafaide.record(store_content=True) # one tool
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Offline
|
|
124
|
+
|
|
125
|
+
Nothing waits on the network unless you use strict mode: writing a record never makes a request, and a wrong key or an unreachable witness is a warning, never an exception in your tool. A malformed `SIGNAFAIDE_API_KEY` is caught here and never sent. With an API key, records go to `.signafaide/outbox/` and are shipped in batches by a background thread: every 2 seconds, every 100 records, and once more on exit (at most 3 seconds). If the service can't be reached, they stay on disk and are retried with backoff; a refusal retrying cannot fix (quota used up, a revoked key) stops sending until the configuration changes. They ship on the next run or with `signafaide flush`. Records are never lost on exit. The delay shows as the gap between a record's `time` and its batch's `server_time`.
|
|
126
|
+
|
|
127
|
+
## Containers that restart
|
|
128
|
+
|
|
129
|
+
A container with no persistent disk continues its chain after the witness's head, and its first record is a signed start note. So that a routine restart is not read as a possible loss, end the instance with `signafaide.stop()`: it ships what is left and then writes a signed stop note. A start right after a stop note is a clean restart; a start with no stop note before it (a crash, a kill, an unreachable witness) is shown as a warning, because records the earlier instance had not sent are not known.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import signal, sys, signafaide
|
|
133
|
+
|
|
134
|
+
def on_term(signum, frame):
|
|
135
|
+
signafaide.stop() # {shipped, left, errors, stopped}
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
signal.signal(signal.SIGTERM, on_term)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A program that ends by itself with `SIGNAFAIDE_HEAD_FILE` set writes the stop note at exit, within the exit limit above. `SIGNAFAIDE_INSTANCE_STOP=0` turns that off; `=1` turns it on without a head file.
|
|
142
|
+
|
|
143
|
+
## Errors
|
|
144
|
+
|
|
145
|
+
Service errors raise or log `signafaide.SignafaideError`. Its message is the service's one-sentence fix, word for word, and `.code` is the error code. A `chain_conflict` (the service's chain for an agent differs from yours) is reported with the service's head and blocks shipping for that agent. The local chain is never rewritten. Run `signafaide verify` and `signafaide status` to see why.
|
|
146
|
+
|
|
147
|
+
## Verify
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
signafaide verify # the local .signafaide state
|
|
151
|
+
signafaide verify pack.json # a pack from `signafaide export`
|
|
152
|
+
signafaide verify pack.json --keys keys.json --json
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
There is one line per check (schema, hashes, signatures, chains, Merkle proofs, batch and head signatures, drand, anchors, OpenTimestamps, content), then the result line: `PASS` or `FAIL` and how many checks passed, are pending and were not checked, for example `PASS: 11 of 12 checks passed, 1 pending (public timestamp)`. Pending means co-signed, waiting for the public timestamp (an anchor is made every hour and usually confirms within a few hours); pending and not checked are never counted as passed. The exit code is 0 for PASS, 1 for FAIL and 2 when an input can't be read. Locally, a chain the witness refuses for good (or that an older SDK wrote with time going back) shows as `BLOCKED` with the reason and the next step, and always exits 1; `--fail-on-pending-send` also fails while records wait to be co-signed, for CI. Locally, before anything was co-signed, the witness's checks show `NOT CHECKED` ("nothing co-signed yet") and no request is made. Service keys come from `--keys <file|url>`, or by default from the service's `/.well-known/signafaide-keys.json`. `--trust-embedded-keys` uses the keys inside the pack and prints a warning. The drand BLS signature is checked too, through `py_ecc` (a dependency, as `@noble/curves` is for the TypeScript SDK), so both SDKs count the same checks. A pack's rounds are checked together, so it takes well under a second.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
report = signafaide.verify("pack.json", keys="keys.json")
|
|
159
|
+
print(report) # the same lines
|
|
160
|
+
report.ok # True / False
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
**For an auditor** the agent keys must come from you, not from the pack (otherwise a service could add a key of its own): `signafaide keys export -o agent-keys.json` writes every agent key, each signed by itself. The auditor runs `npx signafaide verify pack.json --keys <witness>/.well-known/signafaide-keys.json --agent-keys agent-keys.json --head head.json` (or the Python CLI with the same arguments; `head.json` is your `.signafaide/head.json`, which catches records cut off at the end). Without `--agent-keys` the client-signature check says `NOT INDEPENDENT` and the result is FAIL; `--trust-embedded-agent-keys` accepts the pack's keys and says so in the result line. On your own machine, `signafaide verify pack.json` uses this project's agent keys automatically. Public timestamps are checked online (`--timestamp-check <url>`, default `https://mempool.space/api`); with `--offline` they show `NOT CHECKED`, never confirmed. `signafaide keys rotate --agent X` gives an agent a new key; the SDK signs the registration of the new key with the old one.
|
|
165
|
+
|
|
166
|
+
## Replay
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
signafaide replay --agent claims-bot --from 2026-11-01 --to 2026-11-13 --html replay.html
|
|
170
|
+
signafaide replay --agent claims-bot --record 3fa2b1c9
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Shows the agent's decisions and outcomes in order, with their inputs and outputs from the local chain. Each record is checked against its `content_hash`, its signature and the service's receipt first, so an edited line shows as `FAIL`. `--html` also writes a self-contained page. In code: `r = signafaide.replay(agent="claims-bot", since="2026-11-01")`, then `print(r)`, `r.to_html()`, `r.ok`.
|
|
174
|
+
|
|
175
|
+
## Experiments: A/B on past decisions
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
def approve(claim_id, amount): # same arguments as the recorded tool
|
|
179
|
+
return {"approved": amount < 300}
|
|
180
|
+
|
|
181
|
+
report = signafaide.compare(approve, agent="claims-bot", name="approve_payout", since="2026-11-01", limit=500)
|
|
182
|
+
print(report) # n, agreed, changed, errors, then the changed decisions
|
|
183
|
+
report.upload() # optional: hash-only unless content is stored with the service
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The chain is verified first; then each recorded decision with a co-signed outcome is re-run with your candidate, on your machine, with your own models and keys, and compared with the recorded `output` (exact canonical-JSON equality, or `same=lambda recorded, candidate: ...`). A candidate that raises counts as an error. **Candidates must be side-effect free:** compare never writes records, and a recorded tool called from a candidate raises instead of running. CLI: `signafaide compare --module candidate.py:approve --name approve_payout --upload`.
|
|
187
|
+
|
|
188
|
+
## CLI
|
|
189
|
+
|
|
190
|
+
| command | does |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `signafaide init` | the opt-in: creates a free sandbox if there is no key (no account, email or card), prints the claim link |
|
|
193
|
+
| `signafaide status` | key and env, agents, last receipt, outbox size, quota; BLOCKED chains (exit 1) |
|
|
194
|
+
| `signafaide repair [--agent X] [--dry-run]` | sets aside records no witness can take (kept in `.signafaide/held/`, still verified), gives the agent a new key and starts a new segment |
|
|
195
|
+
| `signafaide verify [pack.json] [--keys ...] [--agent-keys ...] [--head ...] [--offline] [--fail-on-pending-send] [--json]` | verifies; exit 0 PASS, 1 FAIL or BLOCKED, 2 unreadable input |
|
|
196
|
+
| `signafaide keys export [-o agent-keys.json]` / `signafaide keys rotate [--agent X] [--env test\|live]` | the agent-key file for auditors / a new key for an agent |
|
|
197
|
+
| `signafaide keys create [--agent X] [--env test\|live] [--print]` | a new key, registered with the witness, for a secret store; writes no record (`--print`: prints the key file and saves nothing here) |
|
|
198
|
+
| `signafaide export [--agent X] [--from T] [--to T] [-o pack.json]` | downloads a verification pack of the whole project (every agent); `--agent` narrows it to one |
|
|
199
|
+
| `signafaide replay [--agent X] [--from T] [--to T] [--record H] [--html out.html] [--json]` | the verified timeline from the local chain; exit 1 on FAIL |
|
|
200
|
+
| `signafaide compare --module file.py:fn [--agent X] [--name N] [--from T] [--to T] [--limit N] [--upload [--values]]` | re-runs recorded decisions with a candidate and compares; `--values` sends the values too (stored-content decisions only) |
|
|
201
|
+
| `signafaide claim` | prints the claim link again |
|
|
202
|
+
| `signafaide flush` | ships the outbox now (with no key it sends nothing and says so) |
|
|
203
|
+
|
|
204
|
+
## What is sent besides records
|
|
205
|
+
|
|
206
|
+
Every request carries one header:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
X-Signafaide-Client: signafaide-python/0.2.1 (python 3.12.3; adapter=none; agent=claude-code; via=sdk)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
It holds the SDK and its version, the Python runtime and its version, and the framework adapter in use (`langchain`, `openai-agents` or `none`). It also holds the coding agent that set the project up (`claude-code`, `codex`, `cursor`, `gemini-cli`, `copilot`, `windsurf` or `unknown`) and how the call was made (`sdk`, `cli` or `mcp`). The agent is found from that tool's environment variables when the project is set up (`signafaide init`) and kept in `.signafaide/config.json`; it is never guessed per process. `signafaide verify` and `signafaide export` also send `POST /v1/events {"name": "verify"}` (or `"export"`) when there is an API key. If that fails, the command is not affected. With no API key nothing at all is sent.
|
|
213
|
+
|
|
214
|
+
The header and the events hold no content, file paths, hostnames or user names. `SIGNAFAIDE_TELEMETRY=0` removes the adapter, the coding agent and the strict-mode flag from the header, stops the coding agent being saved in `.signafaide/config.json`, and turns the events off. The SDK and its version, the runtime and its version, and how the call was made (`via`) are still sent.
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
| variable | default |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `SIGNAFAIDE_API_KEY` | read from the environment, then `.env`; created by `signafaide init`; none: local only |
|
|
221
|
+
| `SIGNAFAIDE_URL` | `https://api.signafaide.co.uk`; a self-hosted witness, or `http://127.0.0.1:7474` for a local dev server. Alone it sends nothing |
|
|
222
|
+
| `SIGNAFAIDE_HOME` | `.signafaide/` in the project root; a mounted volume or `/tmp/signafaide` in production |
|
|
223
|
+
| `SIGNAFAIDE_AGENT_KEY_<NAME>` | an agent's key file content from a secret store (or `key_provider=` in `signafaide.init`); live keys can use `SIGNAFAIDE_AGENT_KEY_LIVE_<NAME>`, made with `signafaide keys create --env live --print` |
|
|
224
|
+
| `SIGNAFAIDE_HEAD_FILE` | unset: a file outside `.signafaide/` (a persistent volume) that also keeps the newest signed heads, for containers whose disk does not last; or `on_head=` in `signafaide.init` |
|
|
225
|
+
| `SIGNAFAIDE_INSTANCE_STOP` | unset: a signed stop note is written at a clean exit when a head file is set; `1` always, `0` never (see Containers that restart) |
|
|
226
|
+
| `SIGNAFAIDE_EXIT_FLUSH_MS` | `3000`: the longest a finished program is kept open to send; `0` never |
|
|
227
|
+
| `SIGNAFAIDE_TEST` | test mode is on by itself under pytest (records in a temporary folder, nothing sent); `1` forces it, `0` turns it off |
|
|
228
|
+
| `SIGNAFAIDE_DISABLED` | `1`: wrapped functions run and nothing is recorded |
|
|
229
|
+
| `SIGNAFAIDE_SEGMENT_BYTES` | `67108864`: a shipped chain file larger than this rolls into a sealed segment |
|
|
230
|
+
| `SIGNAFAIDE_TELEMETRY` | `0` turns off the adapter, agent and strict fields and the events |
|
|
231
|
+
|
|
232
|
+
Running in containers, with replicas, on read-only or ephemeral disks, on serverless or in CI: see the [production guide](https://signafaide.co.uk/docs/production). A record's time never goes back along its chain: if the clock steps back, the record keeps the previous record's time and the clock's reading goes in `meta.wall_time`.
|
|
233
|
+
|
|
234
|
+
Each project and env has its own chain per agent. A new key, for example a live key replacing the sandbox key, starts a fresh chain at seq 1. The old chain stays on disk, and `signafaide status` lists every chain.
|
|
235
|
+
|
|
236
|
+
The project root is the nearest folder with `.signafaide/`, `.git`, `pyproject.toml` or `package.json`. `signafaide init` adds `.signafaide/` to `.gitignore` (never a running program). Agent keys live in `.signafaide/agents/<agent>.key` (sandbox) and `.signafaide/agents/live/<agent>.key` (live), mode 0600, and never leave the machine. Back them up.
|
|
237
|
+
|
|
238
|
+
Python 3.9+. Dependencies: `cryptography` and `py_ecc` (drand BLS).
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# signafaide
|
|
2
|
+
|
|
3
|
+
Signed, chained records of what your AI agents decided. Each record is signed with the agent's own key and chained on your machine; once you opt in, a witness (the signafAIde service, or one you host) co-signs and publicly timestamps it, so neither side can forge or edit a record alone.
|
|
4
|
+
|
|
5
|
+
- Nothing is sent until a person runs `signafaide init` or sets `SIGNAFAIDE_API_KEY`. After that each record sends its content hash with its tool name, agent name and time (and its chain links), never its inputs or outputs unless you turn on stored content. Tool and agent names are sent as you write them, so don't put personal data in them. Each content hash is salted with 16 random bytes that stay on your machine with the content, so the hash alone cannot be used to guess the content, even when it has only a few possible values.
|
|
6
|
+
- The sandbox `init` creates needs no account, email or card.
|
|
7
|
+
- To keep the evidence in your environment, self-host the witness and set `SIGNAFAIDE_URL` ([self-host](https://signafaide.co.uk/docs/self-host)).
|
|
8
|
+
- Apache-2.0, with a public protocol spec and an independent verifier.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv add signafaide # a uv project
|
|
12
|
+
poetry add signafaide # a Poetry project
|
|
13
|
+
python3 -m venv .venv && .venv/bin/python -m pip install signafaide # otherwise, then add it to requirements.txt
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Not a bare `pip install`: most systems' Python refuses it outside a virtual environment (PEP 668, "externally-managed-environment").
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import signafaide
|
|
20
|
+
|
|
21
|
+
@signafaide.record
|
|
22
|
+
def approve_payout(claim_id: str, amount: float): ...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
signafaide verify # or .venv/bin/signafaide verify, uv run signafaide verify
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That's it. With no API key, records are signed, chained and kept in `.signafaide/`, and nothing is sent. `signafaide verify` checks every signature, chain link and content hash and ends with a result line that says how many of the 12 checks ran, for example `PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)`. A check that was not run is never counted as passed. It also says what still needs the witness:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Sending hashes to a witness
|
|
36
|
+
|
|
37
|
+
Co-signature and the public timestamp need a witness. A person opts in once:
|
|
38
|
+
|
|
39
|
+
- `signafaide init` creates a free sandbox (no account, email or card), saves `SIGNAFAIDE_API_KEY` to `.env`, adds `.env` to `.gitignore` and prints a link to claim it; or
|
|
40
|
+
- set `SIGNAFAIDE_API_KEY` (a live key from the console), with `SIGNAFAIDE_URL` for a self-hosted witness. `SIGNAFAIDE_URL` alone sends nothing.
|
|
41
|
+
|
|
42
|
+
Records written before the opt-in ship afterwards with their original times. The service accepts records up to 7 days old (an agent key's `valid_from` is at most 7 days before its registration); a local chain that starts earlier stays local, still verifies, and `signafaide flush` reports it as `too_old`. `signafaide.init()` in code only configures: it never creates a sandbox.
|
|
43
|
+
|
|
44
|
+
## What a call records
|
|
45
|
+
|
|
46
|
+
1. A `decision` record before the function runs: the tool name, plus `{"input": {<argument>: <value>}, "meta": {...}}`.
|
|
47
|
+
2. The function runs.
|
|
48
|
+
3. An `outcome` record after it: `{"output": ...}`, or `{"error": {"type", "message"}}`. The error is raised again unchanged.
|
|
49
|
+
|
|
50
|
+
Records are signed and chained locally in `.signafaide/` (synchronously, in a couple of milliseconds), then, once you opted in, shipped in the background. Sync and async functions both work.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
@signafaide.record(agent="claims-bot", name="approve_payout", meta={"model": "gpt-x"})
|
|
54
|
+
def approve(claim_id: str, amount: float): ...
|
|
55
|
+
|
|
56
|
+
signafaide.note("policy_loaded", {"version": 3}) # anything that is not a tool call
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Give each agent an explicit name, for example `@signafaide.record(agent="claims-bot")`; without one it is the project folder's name. It must match `^[a-z0-9][a-z0-9._-]{0,63}$`. In production, one agent name is one chain written by one running instance.
|
|
60
|
+
|
|
61
|
+
## Frameworks
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
tools = signafaide.wrap_tools({"lookup_order": lookup_order, "refund": refund}) # dict: key = name
|
|
65
|
+
tools = signafaide.wrap_tools([lookup_order, refund]) # list
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`wrap_tools` (and `wrap_tool` for one) also takes:
|
|
69
|
+
|
|
70
|
+
- **LangChain** tools (`@tool` or any `BaseTool`). The copy records each `invoke` / `ainvoke`, including tool calls, and still converts for `bind_tools`. The outcome holds the tool's own return value, not the `ToolMessage` LangChain wraps it in.
|
|
71
|
+
- **OpenAI Agents SDK** `FunctionTool`s (`@function_tool`). The copy records `on_invoke_tool`.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from agents import Agent, function_tool
|
|
75
|
+
|
|
76
|
+
@function_tool
|
|
77
|
+
def lookup_order(order_id: str) -> str: ...
|
|
78
|
+
|
|
79
|
+
agent = Agent(name="support", tools=signafaide.wrap_tools([lookup_order]))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Neither framework is a dependency: `wrap_tools` recognises the objects by their shape.
|
|
83
|
+
|
|
84
|
+
## Strict mode: no receipt, no action
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
@signafaide.record(strict=True)
|
|
88
|
+
def wire_funds(account: str, amount: float): ...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The decision is shipped first, and the function runs only after the service has returned its receipt. If the service can't be reached or refuses the record, or there is no API key (`no_key`), `signafaide.NotRecorded` is raised and the function does not run. Agent frameworks often turn a tool's error into a failed tool call and print nothing, so the first time a strict tool is refused in a process the SDK writes one warning line to stderr with the tool's name and the fix.
|
|
92
|
+
|
|
93
|
+
## Hash-only
|
|
94
|
+
|
|
95
|
+
By default the service receives only the envelope: the name, the time, the chain links and `content_hash`. The hash is `sha256(salt || JCS(content))` with a random 16-byte salt per record, kept next to the content in the local chain, so the service cannot confirm a guess of the content from the hash. When you store content with the service, the salt goes with it, so anyone holding the pack can check the content against the hash. Your inputs and outputs stay in `.signafaide/chain/<project>/<env>/<agent>.jsonl` (`.signafaide/chain/_pending/nokey/<agent>.jsonl` before the opt-in), which is your evidence store. To send the content too (encrypted at rest; live records need the Team plan):
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
signafaide.init(store_content=True) # everything
|
|
99
|
+
@signafaide.record(store_content=True) # one tool
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Offline
|
|
103
|
+
|
|
104
|
+
Nothing waits on the network unless you use strict mode: writing a record never makes a request, and a wrong key or an unreachable witness is a warning, never an exception in your tool. A malformed `SIGNAFAIDE_API_KEY` is caught here and never sent. With an API key, records go to `.signafaide/outbox/` and are shipped in batches by a background thread: every 2 seconds, every 100 records, and once more on exit (at most 3 seconds). If the service can't be reached, they stay on disk and are retried with backoff; a refusal retrying cannot fix (quota used up, a revoked key) stops sending until the configuration changes. They ship on the next run or with `signafaide flush`. Records are never lost on exit. The delay shows as the gap between a record's `time` and its batch's `server_time`.
|
|
105
|
+
|
|
106
|
+
## Containers that restart
|
|
107
|
+
|
|
108
|
+
A container with no persistent disk continues its chain after the witness's head, and its first record is a signed start note. So that a routine restart is not read as a possible loss, end the instance with `signafaide.stop()`: it ships what is left and then writes a signed stop note. A start right after a stop note is a clean restart; a start with no stop note before it (a crash, a kill, an unreachable witness) is shown as a warning, because records the earlier instance had not sent are not known.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import signal, sys, signafaide
|
|
112
|
+
|
|
113
|
+
def on_term(signum, frame):
|
|
114
|
+
signafaide.stop() # {shipped, left, errors, stopped}
|
|
115
|
+
sys.exit(0)
|
|
116
|
+
|
|
117
|
+
signal.signal(signal.SIGTERM, on_term)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A program that ends by itself with `SIGNAFAIDE_HEAD_FILE` set writes the stop note at exit, within the exit limit above. `SIGNAFAIDE_INSTANCE_STOP=0` turns that off; `=1` turns it on without a head file.
|
|
121
|
+
|
|
122
|
+
## Errors
|
|
123
|
+
|
|
124
|
+
Service errors raise or log `signafaide.SignafaideError`. Its message is the service's one-sentence fix, word for word, and `.code` is the error code. A `chain_conflict` (the service's chain for an agent differs from yours) is reported with the service's head and blocks shipping for that agent. The local chain is never rewritten. Run `signafaide verify` and `signafaide status` to see why.
|
|
125
|
+
|
|
126
|
+
## Verify
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
signafaide verify # the local .signafaide state
|
|
130
|
+
signafaide verify pack.json # a pack from `signafaide export`
|
|
131
|
+
signafaide verify pack.json --keys keys.json --json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
There is one line per check (schema, hashes, signatures, chains, Merkle proofs, batch and head signatures, drand, anchors, OpenTimestamps, content), then the result line: `PASS` or `FAIL` and how many checks passed, are pending and were not checked, for example `PASS: 11 of 12 checks passed, 1 pending (public timestamp)`. Pending means co-signed, waiting for the public timestamp (an anchor is made every hour and usually confirms within a few hours); pending and not checked are never counted as passed. The exit code is 0 for PASS, 1 for FAIL and 2 when an input can't be read. Locally, a chain the witness refuses for good (or that an older SDK wrote with time going back) shows as `BLOCKED` with the reason and the next step, and always exits 1; `--fail-on-pending-send` also fails while records wait to be co-signed, for CI. Locally, before anything was co-signed, the witness's checks show `NOT CHECKED` ("nothing co-signed yet") and no request is made. Service keys come from `--keys <file|url>`, or by default from the service's `/.well-known/signafaide-keys.json`. `--trust-embedded-keys` uses the keys inside the pack and prints a warning. The drand BLS signature is checked too, through `py_ecc` (a dependency, as `@noble/curves` is for the TypeScript SDK), so both SDKs count the same checks. A pack's rounds are checked together, so it takes well under a second.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
report = signafaide.verify("pack.json", keys="keys.json")
|
|
138
|
+
print(report) # the same lines
|
|
139
|
+
report.ok # True / False
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
**For an auditor** the agent keys must come from you, not from the pack (otherwise a service could add a key of its own): `signafaide keys export -o agent-keys.json` writes every agent key, each signed by itself. The auditor runs `npx signafaide verify pack.json --keys <witness>/.well-known/signafaide-keys.json --agent-keys agent-keys.json --head head.json` (or the Python CLI with the same arguments; `head.json` is your `.signafaide/head.json`, which catches records cut off at the end). Without `--agent-keys` the client-signature check says `NOT INDEPENDENT` and the result is FAIL; `--trust-embedded-agent-keys` accepts the pack's keys and says so in the result line. On your own machine, `signafaide verify pack.json` uses this project's agent keys automatically. Public timestamps are checked online (`--timestamp-check <url>`, default `https://mempool.space/api`); with `--offline` they show `NOT CHECKED`, never confirmed. `signafaide keys rotate --agent X` gives an agent a new key; the SDK signs the registration of the new key with the old one.
|
|
144
|
+
|
|
145
|
+
## Replay
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
signafaide replay --agent claims-bot --from 2026-11-01 --to 2026-11-13 --html replay.html
|
|
149
|
+
signafaide replay --agent claims-bot --record 3fa2b1c9
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Shows the agent's decisions and outcomes in order, with their inputs and outputs from the local chain. Each record is checked against its `content_hash`, its signature and the service's receipt first, so an edited line shows as `FAIL`. `--html` also writes a self-contained page. In code: `r = signafaide.replay(agent="claims-bot", since="2026-11-01")`, then `print(r)`, `r.to_html()`, `r.ok`.
|
|
153
|
+
|
|
154
|
+
## Experiments: A/B on past decisions
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
def approve(claim_id, amount): # same arguments as the recorded tool
|
|
158
|
+
return {"approved": amount < 300}
|
|
159
|
+
|
|
160
|
+
report = signafaide.compare(approve, agent="claims-bot", name="approve_payout", since="2026-11-01", limit=500)
|
|
161
|
+
print(report) # n, agreed, changed, errors, then the changed decisions
|
|
162
|
+
report.upload() # optional: hash-only unless content is stored with the service
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The chain is verified first; then each recorded decision with a co-signed outcome is re-run with your candidate, on your machine, with your own models and keys, and compared with the recorded `output` (exact canonical-JSON equality, or `same=lambda recorded, candidate: ...`). A candidate that raises counts as an error. **Candidates must be side-effect free:** compare never writes records, and a recorded tool called from a candidate raises instead of running. CLI: `signafaide compare --module candidate.py:approve --name approve_payout --upload`.
|
|
166
|
+
|
|
167
|
+
## CLI
|
|
168
|
+
|
|
169
|
+
| command | does |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `signafaide init` | the opt-in: creates a free sandbox if there is no key (no account, email or card), prints the claim link |
|
|
172
|
+
| `signafaide status` | key and env, agents, last receipt, outbox size, quota; BLOCKED chains (exit 1) |
|
|
173
|
+
| `signafaide repair [--agent X] [--dry-run]` | sets aside records no witness can take (kept in `.signafaide/held/`, still verified), gives the agent a new key and starts a new segment |
|
|
174
|
+
| `signafaide verify [pack.json] [--keys ...] [--agent-keys ...] [--head ...] [--offline] [--fail-on-pending-send] [--json]` | verifies; exit 0 PASS, 1 FAIL or BLOCKED, 2 unreadable input |
|
|
175
|
+
| `signafaide keys export [-o agent-keys.json]` / `signafaide keys rotate [--agent X] [--env test\|live]` | the agent-key file for auditors / a new key for an agent |
|
|
176
|
+
| `signafaide keys create [--agent X] [--env test\|live] [--print]` | a new key, registered with the witness, for a secret store; writes no record (`--print`: prints the key file and saves nothing here) |
|
|
177
|
+
| `signafaide export [--agent X] [--from T] [--to T] [-o pack.json]` | downloads a verification pack of the whole project (every agent); `--agent` narrows it to one |
|
|
178
|
+
| `signafaide replay [--agent X] [--from T] [--to T] [--record H] [--html out.html] [--json]` | the verified timeline from the local chain; exit 1 on FAIL |
|
|
179
|
+
| `signafaide compare --module file.py:fn [--agent X] [--name N] [--from T] [--to T] [--limit N] [--upload [--values]]` | re-runs recorded decisions with a candidate and compares; `--values` sends the values too (stored-content decisions only) |
|
|
180
|
+
| `signafaide claim` | prints the claim link again |
|
|
181
|
+
| `signafaide flush` | ships the outbox now (with no key it sends nothing and says so) |
|
|
182
|
+
|
|
183
|
+
## What is sent besides records
|
|
184
|
+
|
|
185
|
+
Every request carries one header:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
X-Signafaide-Client: signafaide-python/0.2.1 (python 3.12.3; adapter=none; agent=claude-code; via=sdk)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
It holds the SDK and its version, the Python runtime and its version, and the framework adapter in use (`langchain`, `openai-agents` or `none`). It also holds the coding agent that set the project up (`claude-code`, `codex`, `cursor`, `gemini-cli`, `copilot`, `windsurf` or `unknown`) and how the call was made (`sdk`, `cli` or `mcp`). The agent is found from that tool's environment variables when the project is set up (`signafaide init`) and kept in `.signafaide/config.json`; it is never guessed per process. `signafaide verify` and `signafaide export` also send `POST /v1/events {"name": "verify"}` (or `"export"`) when there is an API key. If that fails, the command is not affected. With no API key nothing at all is sent.
|
|
192
|
+
|
|
193
|
+
The header and the events hold no content, file paths, hostnames or user names. `SIGNAFAIDE_TELEMETRY=0` removes the adapter, the coding agent and the strict-mode flag from the header, stops the coding agent being saved in `.signafaide/config.json`, and turns the events off. The SDK and its version, the runtime and its version, and how the call was made (`via`) are still sent.
|
|
194
|
+
|
|
195
|
+
## Configuration
|
|
196
|
+
|
|
197
|
+
| variable | default |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `SIGNAFAIDE_API_KEY` | read from the environment, then `.env`; created by `signafaide init`; none: local only |
|
|
200
|
+
| `SIGNAFAIDE_URL` | `https://api.signafaide.co.uk`; a self-hosted witness, or `http://127.0.0.1:7474` for a local dev server. Alone it sends nothing |
|
|
201
|
+
| `SIGNAFAIDE_HOME` | `.signafaide/` in the project root; a mounted volume or `/tmp/signafaide` in production |
|
|
202
|
+
| `SIGNAFAIDE_AGENT_KEY_<NAME>` | an agent's key file content from a secret store (or `key_provider=` in `signafaide.init`); live keys can use `SIGNAFAIDE_AGENT_KEY_LIVE_<NAME>`, made with `signafaide keys create --env live --print` |
|
|
203
|
+
| `SIGNAFAIDE_HEAD_FILE` | unset: a file outside `.signafaide/` (a persistent volume) that also keeps the newest signed heads, for containers whose disk does not last; or `on_head=` in `signafaide.init` |
|
|
204
|
+
| `SIGNAFAIDE_INSTANCE_STOP` | unset: a signed stop note is written at a clean exit when a head file is set; `1` always, `0` never (see Containers that restart) |
|
|
205
|
+
| `SIGNAFAIDE_EXIT_FLUSH_MS` | `3000`: the longest a finished program is kept open to send; `0` never |
|
|
206
|
+
| `SIGNAFAIDE_TEST` | test mode is on by itself under pytest (records in a temporary folder, nothing sent); `1` forces it, `0` turns it off |
|
|
207
|
+
| `SIGNAFAIDE_DISABLED` | `1`: wrapped functions run and nothing is recorded |
|
|
208
|
+
| `SIGNAFAIDE_SEGMENT_BYTES` | `67108864`: a shipped chain file larger than this rolls into a sealed segment |
|
|
209
|
+
| `SIGNAFAIDE_TELEMETRY` | `0` turns off the adapter, agent and strict fields and the events |
|
|
210
|
+
|
|
211
|
+
Running in containers, with replicas, on read-only or ephemeral disks, on serverless or in CI: see the [production guide](https://signafaide.co.uk/docs/production). A record's time never goes back along its chain: if the clock steps back, the record keeps the previous record's time and the clock's reading goes in `meta.wall_time`.
|
|
212
|
+
|
|
213
|
+
Each project and env has its own chain per agent. A new key, for example a live key replacing the sandbox key, starts a fresh chain at seq 1. The old chain stays on disk, and `signafaide status` lists every chain.
|
|
214
|
+
|
|
215
|
+
The project root is the nearest folder with `.signafaide/`, `.git`, `pyproject.toml` or `package.json`. `signafaide init` adds `.signafaide/` to `.gitignore` (never a running program). Agent keys live in `.signafaide/agents/<agent>.key` (sandbox) and `.signafaide/agents/live/<agent>.key` (live), mode 0600, and never leave the machine. Back them up.
|
|
216
|
+
|
|
217
|
+
Python 3.9+. Dependencies: `cryptography` and `py_ecc` (drand BLS).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.18"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "signafaide"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Signed, chained records of what your AI agents decided. Hash-only by default."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
dependencies = ["cryptography>=41", "py_ecc>=7"]
|
|
13
|
+
keywords = ["ai-agents", "audit-trail", "tamper-evident", "verifiable", "eu-ai-act", "provenance"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Topic :: Security :: Cryptography",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://signafaide.co.uk"
|
|
23
|
+
Documentation = "https://signafaide.co.uk/docs"
|
|
24
|
+
Repository = "https://github.com/AIME-TECH-UK-LIMITED/signafaide-sdk"
|
|
25
|
+
Changelog = "https://github.com/AIME-TECH-UK-LIMITED/signafaide-sdk/blob/main/CHANGELOG.md"
|
|
26
|
+
Issues = "https://github.com/AIME-TECH-UK-LIMITED/signafaide-sdk/issues"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
# Kept so `pip install "signafaide[bls]"` still works; BLS is now a regular dependency.
|
|
30
|
+
bls = []
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
signafaide = "signafaide.cli:main"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = ["pytest>=7", "langchain-core>=0.3", "openai-agents>=0.2", "py_ecc>=7"]
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/signafaide"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|