dcl-core 0.3.0__tar.gz → 0.3.2__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.
- dcl_core-0.3.2/PKG-INFO +297 -0
- dcl_core-0.3.2/README.md +270 -0
- dcl_core-0.3.2/dcl/__init__.py +18 -0
- dcl_core-0.3.2/dcl/guard.py +512 -0
- dcl_core-0.3.2/dcl/sandbox.py +149 -0
- dcl_core-0.3.2/dcl/signed_envelope.py +278 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core/__init__.py +1 -1
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core/chain.py +255 -242
- dcl_core-0.3.2/dcl_core.egg-info/PKG-INFO +297 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core.egg-info/SOURCES.txt +8 -0
- dcl_core-0.3.2/dcl_core.egg-info/requires.txt +7 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core.egg-info/top_level.txt +1 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/pyproject.toml +36 -33
- dcl_core-0.3.2/tests/test_dcl_guard.py +867 -0
- dcl_core-0.3.2/tests/test_production_web2_proof.py +816 -0
- dcl_core-0.3.2/tests/test_signed_oracle_envelope_guard.py +321 -0
- dcl_core-0.3.2/tests/test_signed_oracle_envelope_vector.py +160 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/tests/test_verify_matches_chain_state.py +101 -98
- dcl_core-0.3.0/PKG-INFO +0 -113
- dcl_core-0.3.0/README.md +0 -90
- dcl_core-0.3.0/dcl_core.egg-info/PKG-INFO +0 -113
- dcl_core-0.3.0/dcl_core.egg-info/requires.txt +0 -3
- {dcl_core-0.3.0 → dcl_core-0.3.2}/LICENSE +0 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core/consensus.py +0 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core/seal.py +0 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core/verify.py +0 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/dcl_core.egg-info/dependency_links.txt +0 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/setup.cfg +0 -0
- {dcl_core-0.3.0 → dcl_core-0.3.2}/tests/test_fixes.py +0 -0
dcl_core-0.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dcl-core
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Tamper-evident record chain, offline chain verification, and multi-party consensus primitives for AI agent audit systems (DCL / Leibniz Layer protocol).
|
|
5
|
+
Author: Fronesis Labs
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://fronesislabs.com
|
|
8
|
+
Project-URL: MCP, https://mcp.fronesislabs.com/mcp
|
|
9
|
+
Project-URL: Repository, https://github.com/Fronesis-Labs/dcl-core
|
|
10
|
+
Project-URL: Protocol, https://github.com/Fronesis-Labs/dcl-sdk/blob/main/PROTOCOL.md
|
|
11
|
+
Keywords: mcp,model-context-protocol,audit,hash-chain,ai-agents,x402
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: cryptography>=41
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Provides-Extra: proof
|
|
25
|
+
Requires-Dist: x402[evm,requests]>=2.25.0; extra == "proof"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# DCL Core
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/dcl-core/)
|
|
31
|
+
[](https://pypi.org/project/dcl-core/)
|
|
32
|
+
[](https://github.com/Fronesis-Labs/dcl-core/blob/main/LICENSE)
|
|
33
|
+
|
|
34
|
+
**Don't trust the agent. Trust the proof.**
|
|
35
|
+
|
|
36
|
+
Python reference implementation of the tamper-evident record chain and
|
|
37
|
+
multi-party consensus primitives behind DCL — the audit layer for
|
|
38
|
+
autonomous AI agent decisions. Given a chain record, `dcl-core` recomputes
|
|
39
|
+
its hash locally and tells you if it was tampered with. No API key, no
|
|
40
|
+
network call, no trust placed in whoever is showing you the record.
|
|
41
|
+
That claim is `verify_chain()` on a full chain record. The Web2 proof
|
|
42
|
+
later in this document only confirms the sequence this client observed.
|
|
43
|
+
|
|
44
|
+
Part of the Deterministic Commitment Layer / Leibniz Layer™ ecosystem by
|
|
45
|
+
Fronesis Labs.
|
|
46
|
+
|
|
47
|
+
## Why This Package Is Free
|
|
48
|
+
|
|
49
|
+
Verifying that an audit record hasn't been altered is a different problem
|
|
50
|
+
from generating the verdict in the first place — and it's the one part of
|
|
51
|
+
an audit system that *must* be independently checkable, or the audit trail
|
|
52
|
+
isn't really independent. If you can only verify a record by paying the
|
|
53
|
+
same party who might have edited it, that's not verification, it's trust
|
|
54
|
+
with extra steps.
|
|
55
|
+
|
|
56
|
+
So the protocol itself is open, Apache-2.0, and has no server dependency
|
|
57
|
+
for its core function. `dcl-core` is the reference implementation for
|
|
58
|
+
Python; [`@fronesis-labs/dcl-sdk`](https://github.com/Fronesis-Labs/dcl-sdk)
|
|
59
|
+
is the byte-for-byte-compatible equivalent for TypeScript/JavaScript. DCL's
|
|
60
|
+
paid layer — running an agent's output through policy and getting a
|
|
61
|
+
verdict — lives separately in
|
|
62
|
+
[`dcl-webhook`](https://github.com/Fronesis-Labs/dcl-webhook).
|
|
63
|
+
|
|
64
|
+
This is a clean-history consolidation: the single-agent chain logic
|
|
65
|
+
previously duplicated inside `dcl-webhook`, and the multi-agent consensus
|
|
66
|
+
logic from `dcl-v2`, unified into one module with two known issues fixed.
|
|
67
|
+
|
|
68
|
+
## Two Integrity Fixes Worth Knowing About
|
|
69
|
+
|
|
70
|
+
### 1. `ChainState.verify()` now catches content tampering, not just broken links
|
|
71
|
+
|
|
72
|
+
The chain logic previously embedded in `dcl-webhook/dcl_core.py` only
|
|
73
|
+
checked that `prev_hash` pointers linked correctly between rows. It never
|
|
74
|
+
recomputed a row's hash from its own stored fields. That meant a party with
|
|
75
|
+
direct database access could edit `verdict`, `confidence`, or `reason` on
|
|
76
|
+
an existing row — without touching `tx_hash`/`prev_hash` — and `verify()`
|
|
77
|
+
would still report the chain as clean. That defeated the core tamper-evident
|
|
78
|
+
claim.
|
|
79
|
+
|
|
80
|
+
`verify()` here recomputes each row's hash from all of its stored fields
|
|
81
|
+
and compares it against the stored `tx_hash`. Editing any field now breaks
|
|
82
|
+
verification. See
|
|
83
|
+
`tests/test_fixes.py::test_detects_content_tamper_not_just_link_break`.
|
|
84
|
+
|
|
85
|
+
### 2. Multi-party consensus no longer forgeable via XOR
|
|
86
|
+
|
|
87
|
+
`dcl-v2`'s original formula was `H*_t = hash(h_t1 ⊕ h_t2 ⊕ ... ⊕ h_tM)`.
|
|
88
|
+
XOR is commutative and reversible: a party submitting last — or able to
|
|
89
|
+
observe the running combination — could solve for a contribution that
|
|
90
|
+
forces the final Super-Hash to any target value, without breaking any hash
|
|
91
|
+
function. Cheap arithmetic forgery, not a cryptographic break, but a real
|
|
92
|
+
one.
|
|
93
|
+
|
|
94
|
+
`compute_super_hash()` instead sorts contributions by party ID and hashes
|
|
95
|
+
their length-prefixed concatenation. Order no longer matters (no
|
|
96
|
+
last-mover advantage) and the XOR-forgery trick no longer works. See
|
|
97
|
+
`tests/test_fixes.py::test_super_hash_xor_forgery_no_longer_trivial`.
|
|
98
|
+
|
|
99
|
+
## Structure
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
dcl_core/
|
|
103
|
+
├── __init__.py
|
|
104
|
+
├── chain.py ChainState — append-only chain, content-hash verify()
|
|
105
|
+
├── consensus.py ConsensusRound, compute_super_hash — multi-party consensus
|
|
106
|
+
├── seal.py format_seal() — human-readable "Verified by..." seal, mirrors dcl-sdk's seal.ts
|
|
107
|
+
└── verify.py verify_chain() — standalone offline chain verification, mirrors dcl-sdk's verify.ts
|
|
108
|
+
tests/
|
|
109
|
+
└── test_fixes.py Regression tests for both fixes above
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Quick Start
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install -e .
|
|
116
|
+
python -m pytest tests/ -v
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from dcl_core import ChainState
|
|
121
|
+
|
|
122
|
+
chain = ChainState("audit.db")
|
|
123
|
+
tx_hash, idx = chain.append(
|
|
124
|
+
verdict="COMMIT", input_hash="0xabc...", policy_hash="0xdef...",
|
|
125
|
+
agent_id="agent-1", reason="policy checks passed", confidence=0.95,
|
|
126
|
+
task_type="generation",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
clean, bad_index, reason = chain.verify()
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from dcl_core import ConsensusRound, verify_super_hash
|
|
134
|
+
|
|
135
|
+
round_ = ConsensusRound(expected_parties={"org-a", "org-b", "org-c"})
|
|
136
|
+
round_.submit("org-a", h_a)
|
|
137
|
+
round_.submit("org-b", h_b)
|
|
138
|
+
round_.submit("org-c", h_c)
|
|
139
|
+
super_hash, contributions = round_.seal()
|
|
140
|
+
assert verify_super_hash(contributions, super_hash)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Scope
|
|
144
|
+
|
|
145
|
+
This repo publishes the **protocol layer only** — record format, hash-chain
|
|
146
|
+
verification, and consensus combination. Scoring/policy engines (behavioral
|
|
147
|
+
fingerprinting, epistemic validation thresholds, reputation weighting) stay
|
|
148
|
+
in separate, closed modules that build on top of this.
|
|
149
|
+
|
|
150
|
+
## Web2 HTTP side effect
|
|
151
|
+
|
|
152
|
+
Web2 agents that need a COMMIT / NO_COMMIT gate in front of an HTTP side effect can use `DCLGuard`:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from dcl import DCLGuard
|
|
156
|
+
|
|
157
|
+
guard = DCLGuard(oracle_url="https://webhook.fronesislabs.com")
|
|
158
|
+
|
|
159
|
+
result = guard.post(
|
|
160
|
+
"https://api.example.com/send",
|
|
161
|
+
json={"to": "user@example.com", "subject": "Hello"},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# result.executed is True only for exact COMMIT, a matching digest,
|
|
165
|
+
# and a non-redirect response from that URL.
|
|
166
|
+
# A target redirect sets request_sent True and executed False:
|
|
167
|
+
# the digested host may already have seen the POST.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`from dcl import DCLGuard` is the public guard import. `from dcl_core import ChainState` is the chain import. This distribution publishes both packages (`pyproject.toml` includes `dcl` and `dcl_core*`). They are not the same module.
|
|
171
|
+
|
|
172
|
+
`post()` accepts `json=`. It does not accept `data=` or `files=`. A missing JSON body is hashed as an empty string, not as JSON `null`.
|
|
173
|
+
|
|
174
|
+
### Request binding
|
|
175
|
+
|
|
176
|
+
`dcl.guard.request_digest()` is SHA-256 (hex, no `0x`) of this preimage, UTF-8:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
METHOD
|
|
180
|
+
canonical URL
|
|
181
|
+
canonical JSON body
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`METHOD` is stripped and uppercased. The digest includes the HTTP method, the canonical URL, and the canonical JSON body.
|
|
185
|
+
|
|
186
|
+
The digest does not include HTTP headers, `Authorization`, `Content-Type`, x402 headers, cookies, `User-Agent`, other transport metadata, TLS metadata, or the network connection. `post()` can still send `headers=` to the target after COMMIT. Those headers are not sent to the Oracle and are not in the digest.
|
|
187
|
+
|
|
188
|
+
The Oracle receives the action, the target, and the JSON body in the `response` field, not only the digest, because checks such as jailbreak evaluate that text. On the production Oracle that text is sent to `https://webhook.fronesislabs.com`, which is the same disclosure as publishing the decision on the public audit board.
|
|
189
|
+
|
|
190
|
+
If a header changes what the request means, the Oracle decision is not cryptographically bound to that header.
|
|
191
|
+
|
|
192
|
+
### Canonicalization
|
|
193
|
+
|
|
194
|
+
This is this package's own deterministic scheme. It is not RFC 8785 JCS.
|
|
195
|
+
|
|
196
|
+
- `dcl.guard.canonical_json()`: sorted object keys, compact separators `( ",", ":" )`, `ensure_ascii=False`. Array order is preserved. No Unicode normalization.
|
|
197
|
+
- `dcl.guard.canonical_url()`: strip surrounding whitespace, lowercase scheme and hostname, drop the fragment, omit default ports 80 and 443, keep any other port, sort decoded query pairs and encode them again, keep the path, and use `/` when the path is empty.
|
|
198
|
+
|
|
199
|
+
A published vector is tested by hashing that exact preimage, not by calling `request_digest()` twice. Method `POST`, URL `https://API.Example.com:443/orders?b=2&a=1#fragment`, JSON `{"z":1,"a":"é"}` canonicalizes to:
|
|
200
|
+
|
|
201
|
+
```text
|
|
202
|
+
POST
|
|
203
|
+
https://api.example.com/orders?a=1&b=2
|
|
204
|
+
{"a":"é","z":1}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
SHA-256: `102854ec6909e5be3774fffbfc7bee1922341a2dede88277f7e30f12bc3a8123`.
|
|
208
|
+
|
|
209
|
+
Semantically similar URLs or JSON values that this scheme does not normalize (path percent-encoding, object key duplicates, `1` versus `1.0`) produce different digests. That is the current scheme, not an accident to paper over.
|
|
210
|
+
|
|
211
|
+
### Enforcement boundary
|
|
212
|
+
|
|
213
|
+
This is library-level enforcement inside the process that calls `DCLGuard.post()`. It does not enforce anything at the network. An application can still POST the same URL with another client and skip the guard.
|
|
214
|
+
|
|
215
|
+
### Oracle trust boundary
|
|
216
|
+
|
|
217
|
+
Three different things:
|
|
218
|
+
|
|
219
|
+
1. Request-digest binding: the guard compares the digest it computed with the `request_digest` string in the Oracle JSON.
|
|
220
|
+
2. Oracle transport and trust: the default client uses HTTPS/TLS to the configured Oracle URL and does not follow Oracle redirects. Unsigned JSON remains supported. When OracleTrustConfig is configured, the guard verifies the Oracle signed wrapper before accepting the response.
|
|
221
|
+
3. Audit-chain verification: `dcl_core.verify_chain()` recomputes a chain record hash. The guard does not do that.
|
|
222
|
+
|
|
223
|
+
Python `DCLGuard` with `trust=OracleTrustConfig(...)` verifies a signed wrapper. Without `trust`, it uses the current unsigned path. The TypeScript guard is still unsigned. Nonce replay protection is not implemented. HTTPS/TLS to the configured Oracle endpoint is still required. This does not mean every Oracle response is cryptographically signed.
|
|
224
|
+
|
|
225
|
+
### Signed decision envelope
|
|
226
|
+
|
|
227
|
+
[Signed Oracle decision envelope v1](docs/SIGNED_ORACLE_DECISION_ENVELOPE_V1.md) is the contract. Python `DCLGuard` verifies it only when the caller passes `trust=OracleTrustConfig(...)`. `trust=None`, the default, keeps the current unsigned Oracle JSON. Passing `trust` does not reject that unsigned JSON. A signed wrapper without `trust` stays a denial. The TypeScript guard does not verify envelopes yet.
|
|
228
|
+
|
|
229
|
+
The signed message is the UTF-8 canonical bytes of the ten-field envelope, not the HTTP headers and not `oracle.tx_hash`. The public key comes from `trust.keys`, not from the Oracle body. Unknown `key_id` is a denial. `COMMIT` after a signed wrapper requires a valid Ed25519 signature plus the envelope's `request_digest`, `trace_id`, `issued_at`, and `expires_at` checks. Nonce format is checked and is not stored; that is not replay protection. The signature does not replace HTTPS/TLS. The request-digest algorithm, Audit Event v1.0, and the production proof JSON stay as they are. `oracle.tx_hash` remains the audit-chain hash, not a Base transaction.
|
|
230
|
+
|
|
231
|
+
`verdict` must be exactly `COMMIT` or exactly `NO_COMMIT`. `COMMITTED`, `commit`, `COMMIT `, and any other string are denials. `allowed` is true only when `verdict == "COMMIT"`.
|
|
232
|
+
|
|
233
|
+
### Failure behavior
|
|
234
|
+
|
|
235
|
+
These outcomes are fail-closed in `tests/test_dcl_guard.py` and `packages/dcl/test/guard.test.ts`. `result.executed` is false and the target server used by the test receives no request, except the target-redirect case noted below:
|
|
236
|
+
|
|
237
|
+
- Oracle HTTP 402, including a body that says `COMMIT`
|
|
238
|
+
- Oracle HTTP 404 and HTTP 500
|
|
239
|
+
- Oracle timeout
|
|
240
|
+
- Oracle connection failure
|
|
241
|
+
- any other exception from the Oracle transport
|
|
242
|
+
- malformed JSON, JSON that is not an object, missing verdict, unknown verdict, missing or non-string `reason`
|
|
243
|
+
- `COMMIT` with no `request_digest`, an empty digest, or a different digest
|
|
244
|
+
- `NO_COMMIT`
|
|
245
|
+
- Oracle HTTP redirect (not followed)
|
|
246
|
+
- target HTTP redirect (not followed; see below)
|
|
247
|
+
|
|
248
|
+
### Redirect behavior
|
|
249
|
+
|
|
250
|
+
The default Oracle client (`_urllib_transport`, and `redirect: "manual"` in TypeScript) does not follow a 3xx from the Oracle. The guard then denies the side effect. A custom `transport` is responsible for its own Oracle requests; the guard still will not call the target unless that transport returns exact `COMMIT` plus the matching digest.
|
|
251
|
+
|
|
252
|
+
The target POST also does not follow redirects. `urllib` redirect codes are turned into `TargetRedirectRefused` (TypeScript uses `redirect: "manual"`). `Location` is not requested.
|
|
253
|
+
|
|
254
|
+
Known case: `request_sent` is true and `executed` is false (`requestSent` in TypeScript). The POST to the digested URL has already been sent, so that host may have seen the request. `executed: false` does not mean the request never left this process. The redirect destination is not contacted.
|
|
255
|
+
|
|
256
|
+
### Production proof
|
|
257
|
+
|
|
258
|
+
Live runs against `https://webhook.fronesislabs.com` and `https://httpbin.org/post`, from commit `79390fc93f9ecf0baebe96e750d25e133ff1ef3e`:
|
|
259
|
+
|
|
260
|
+
[Production proof](examples/production_web2_proof.py)
|
|
261
|
+
|
|
262
|
+
Positive path, `2026-10-02T06:45:16Z`: Oracle 402 → x402 payment → `COMMIT` with `request_digest` `841bae2cf5392e1c55c4be5bae4187722df1af40166b1f8fc5c485d515b70019` → target POST 200.
|
|
263
|
+
|
|
264
|
+
Negative path, `2026-10-02T06:46:17Z`: Oracle 402 → x402 payment → `NO_COMMIT` (`forbidden: 'jailbreak'`) with `request_digest` `bc6bc2e1d5e9101a177d0d07469670d81699305e41e23b6fe6ff46d70b8b806e` → the guard did not call the target.
|
|
265
|
+
|
|
266
|
+
Evidence:
|
|
267
|
+
|
|
268
|
+
- [production_web2_proof.json](examples/production_web2_proof.json)
|
|
269
|
+
- [production_web2_negative_proof.json](examples/production_web2_negative_proof.json)
|
|
270
|
+
|
|
271
|
+
`oracle.tx_hash` is the identifier the Oracle returned. In the DCL chain, `ChainState.append` sets that field to `0x` plus the SHA-256 of the canonical audit-chain record. It is not a Base blockchain transaction hash. Positive: `0xaa13b48b6089eb31c9050719b61705d2a33dce911e0299b32c5244cb8821db83`. Negative: `0xa244dccca0682a89441f99bc982ec1f260a2493f667e80c72bd1c4a22d846689`.
|
|
272
|
+
|
|
273
|
+
`payment.tx_hash` on the positive proof is the x402 settlement transaction: `0x33f8af305b255b1a5d15b891f036b5940a47afa90fe9742201ea35af28ce2ad1`.
|
|
274
|
+
|
|
275
|
+
These two JSON files, from commit `79390fc93f9ecf0baebe96e750d25e133ff1ef3e` at `2026-10-02T06:45:16Z` and `2026-10-02T06:46:17Z`, are the current evidence. This repository does not contain an earlier proof note. A write-up that cites commit `8b620d0` or different hashes is a previous run, not a second canonical proof. The identifier that note called an Oracle transaction reference is the audit-chain hash (`oracle.tx_hash`), not a Base transaction.
|
|
276
|
+
|
|
277
|
+
### Evidence limitations
|
|
278
|
+
|
|
279
|
+
The production proof shows what this client observed: the 402, one Oracle payment, the Oracle JSON, the digest comparison, the COMMIT or NO_COMMIT branch, and whether `DCLGuard.post()` then called the target.
|
|
280
|
+
|
|
281
|
+
The negative proof's `target.called: false` means this client did not run the target POST after `NO_COMMIT`. `httpbin.org` does not provide an independent call counter in this proof. The proof does not show that no other client reached httpbin.
|
|
282
|
+
|
|
283
|
+
The JSON does not contain the audit-chain fields `verify_chain()` needs (`index`, `prev_hash`, `input_hash`, `policy_hash`, `confidence`, `timestamp`, `drift_context`, and the rest of the canonical record). It is not an offline cryptographic proof of the Oracle verdict, and it does not authenticate the Oracle response.
|
|
284
|
+
|
|
285
|
+
It also does not provide network-level enforcement or stop another code path from skipping `DCLGuard`.
|
|
286
|
+
|
|
287
|
+
### Payment
|
|
288
|
+
|
|
289
|
+
x402 payment is not part of `DCLGuard`. The proof script passes `examples/oracle_x402_transport.py` as the Oracle `transport`. That adapter reads `DCL_PAYER_PRIVATE_KEY`, or `X402_PRIVATE_KEY` as its only alias. A general `PRIVATE_KEY` is ignored, even when it is set for another wallet. If neither accepted variable is set, the adapter raises before any network call or signature and names the missing variable. It will not pay more than `DCL_MAX_PAYMENT_USDC` (default `0.01`). The guard never sees the key and never signs a payment. The target POST stays unpaid.
|
|
290
|
+
|
|
291
|
+
Do not commit a private key or a seed phrase. Use a dedicated wallet for automated x402 payments, keep only a small operational balance, and do not point this adapter at a treasury or personal wallet. Rotate a credential that has been exposed.
|
|
292
|
+
|
|
293
|
+
See [Protect an HTTP side effect with DCL](docs/PROTECT_HTTP_SIDE_EFFECT.md) and [examples/README.md](examples/README.md).
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
dcl_core-0.3.2/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# DCL Core
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/dcl-core/)
|
|
4
|
+
[](https://pypi.org/project/dcl-core/)
|
|
5
|
+
[](https://github.com/Fronesis-Labs/dcl-core/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
**Don't trust the agent. Trust the proof.**
|
|
8
|
+
|
|
9
|
+
Python reference implementation of the tamper-evident record chain and
|
|
10
|
+
multi-party consensus primitives behind DCL — the audit layer for
|
|
11
|
+
autonomous AI agent decisions. Given a chain record, `dcl-core` recomputes
|
|
12
|
+
its hash locally and tells you if it was tampered with. No API key, no
|
|
13
|
+
network call, no trust placed in whoever is showing you the record.
|
|
14
|
+
That claim is `verify_chain()` on a full chain record. The Web2 proof
|
|
15
|
+
later in this document only confirms the sequence this client observed.
|
|
16
|
+
|
|
17
|
+
Part of the Deterministic Commitment Layer / Leibniz Layer™ ecosystem by
|
|
18
|
+
Fronesis Labs.
|
|
19
|
+
|
|
20
|
+
## Why This Package Is Free
|
|
21
|
+
|
|
22
|
+
Verifying that an audit record hasn't been altered is a different problem
|
|
23
|
+
from generating the verdict in the first place — and it's the one part of
|
|
24
|
+
an audit system that *must* be independently checkable, or the audit trail
|
|
25
|
+
isn't really independent. If you can only verify a record by paying the
|
|
26
|
+
same party who might have edited it, that's not verification, it's trust
|
|
27
|
+
with extra steps.
|
|
28
|
+
|
|
29
|
+
So the protocol itself is open, Apache-2.0, and has no server dependency
|
|
30
|
+
for its core function. `dcl-core` is the reference implementation for
|
|
31
|
+
Python; [`@fronesis-labs/dcl-sdk`](https://github.com/Fronesis-Labs/dcl-sdk)
|
|
32
|
+
is the byte-for-byte-compatible equivalent for TypeScript/JavaScript. DCL's
|
|
33
|
+
paid layer — running an agent's output through policy and getting a
|
|
34
|
+
verdict — lives separately in
|
|
35
|
+
[`dcl-webhook`](https://github.com/Fronesis-Labs/dcl-webhook).
|
|
36
|
+
|
|
37
|
+
This is a clean-history consolidation: the single-agent chain logic
|
|
38
|
+
previously duplicated inside `dcl-webhook`, and the multi-agent consensus
|
|
39
|
+
logic from `dcl-v2`, unified into one module with two known issues fixed.
|
|
40
|
+
|
|
41
|
+
## Two Integrity Fixes Worth Knowing About
|
|
42
|
+
|
|
43
|
+
### 1. `ChainState.verify()` now catches content tampering, not just broken links
|
|
44
|
+
|
|
45
|
+
The chain logic previously embedded in `dcl-webhook/dcl_core.py` only
|
|
46
|
+
checked that `prev_hash` pointers linked correctly between rows. It never
|
|
47
|
+
recomputed a row's hash from its own stored fields. That meant a party with
|
|
48
|
+
direct database access could edit `verdict`, `confidence`, or `reason` on
|
|
49
|
+
an existing row — without touching `tx_hash`/`prev_hash` — and `verify()`
|
|
50
|
+
would still report the chain as clean. That defeated the core tamper-evident
|
|
51
|
+
claim.
|
|
52
|
+
|
|
53
|
+
`verify()` here recomputes each row's hash from all of its stored fields
|
|
54
|
+
and compares it against the stored `tx_hash`. Editing any field now breaks
|
|
55
|
+
verification. See
|
|
56
|
+
`tests/test_fixes.py::test_detects_content_tamper_not_just_link_break`.
|
|
57
|
+
|
|
58
|
+
### 2. Multi-party consensus no longer forgeable via XOR
|
|
59
|
+
|
|
60
|
+
`dcl-v2`'s original formula was `H*_t = hash(h_t1 ⊕ h_t2 ⊕ ... ⊕ h_tM)`.
|
|
61
|
+
XOR is commutative and reversible: a party submitting last — or able to
|
|
62
|
+
observe the running combination — could solve for a contribution that
|
|
63
|
+
forces the final Super-Hash to any target value, without breaking any hash
|
|
64
|
+
function. Cheap arithmetic forgery, not a cryptographic break, but a real
|
|
65
|
+
one.
|
|
66
|
+
|
|
67
|
+
`compute_super_hash()` instead sorts contributions by party ID and hashes
|
|
68
|
+
their length-prefixed concatenation. Order no longer matters (no
|
|
69
|
+
last-mover advantage) and the XOR-forgery trick no longer works. See
|
|
70
|
+
`tests/test_fixes.py::test_super_hash_xor_forgery_no_longer_trivial`.
|
|
71
|
+
|
|
72
|
+
## Structure
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
dcl_core/
|
|
76
|
+
├── __init__.py
|
|
77
|
+
├── chain.py ChainState — append-only chain, content-hash verify()
|
|
78
|
+
├── consensus.py ConsensusRound, compute_super_hash — multi-party consensus
|
|
79
|
+
├── seal.py format_seal() — human-readable "Verified by..." seal, mirrors dcl-sdk's seal.ts
|
|
80
|
+
└── verify.py verify_chain() — standalone offline chain verification, mirrors dcl-sdk's verify.ts
|
|
81
|
+
tests/
|
|
82
|
+
└── test_fixes.py Regression tests for both fixes above
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick Start
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install -e .
|
|
89
|
+
python -m pytest tests/ -v
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from dcl_core import ChainState
|
|
94
|
+
|
|
95
|
+
chain = ChainState("audit.db")
|
|
96
|
+
tx_hash, idx = chain.append(
|
|
97
|
+
verdict="COMMIT", input_hash="0xabc...", policy_hash="0xdef...",
|
|
98
|
+
agent_id="agent-1", reason="policy checks passed", confidence=0.95,
|
|
99
|
+
task_type="generation",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
clean, bad_index, reason = chain.verify()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from dcl_core import ConsensusRound, verify_super_hash
|
|
107
|
+
|
|
108
|
+
round_ = ConsensusRound(expected_parties={"org-a", "org-b", "org-c"})
|
|
109
|
+
round_.submit("org-a", h_a)
|
|
110
|
+
round_.submit("org-b", h_b)
|
|
111
|
+
round_.submit("org-c", h_c)
|
|
112
|
+
super_hash, contributions = round_.seal()
|
|
113
|
+
assert verify_super_hash(contributions, super_hash)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Scope
|
|
117
|
+
|
|
118
|
+
This repo publishes the **protocol layer only** — record format, hash-chain
|
|
119
|
+
verification, and consensus combination. Scoring/policy engines (behavioral
|
|
120
|
+
fingerprinting, epistemic validation thresholds, reputation weighting) stay
|
|
121
|
+
in separate, closed modules that build on top of this.
|
|
122
|
+
|
|
123
|
+
## Web2 HTTP side effect
|
|
124
|
+
|
|
125
|
+
Web2 agents that need a COMMIT / NO_COMMIT gate in front of an HTTP side effect can use `DCLGuard`:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from dcl import DCLGuard
|
|
129
|
+
|
|
130
|
+
guard = DCLGuard(oracle_url="https://webhook.fronesislabs.com")
|
|
131
|
+
|
|
132
|
+
result = guard.post(
|
|
133
|
+
"https://api.example.com/send",
|
|
134
|
+
json={"to": "user@example.com", "subject": "Hello"},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# result.executed is True only for exact COMMIT, a matching digest,
|
|
138
|
+
# and a non-redirect response from that URL.
|
|
139
|
+
# A target redirect sets request_sent True and executed False:
|
|
140
|
+
# the digested host may already have seen the POST.
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
`from dcl import DCLGuard` is the public guard import. `from dcl_core import ChainState` is the chain import. This distribution publishes both packages (`pyproject.toml` includes `dcl` and `dcl_core*`). They are not the same module.
|
|
144
|
+
|
|
145
|
+
`post()` accepts `json=`. It does not accept `data=` or `files=`. A missing JSON body is hashed as an empty string, not as JSON `null`.
|
|
146
|
+
|
|
147
|
+
### Request binding
|
|
148
|
+
|
|
149
|
+
`dcl.guard.request_digest()` is SHA-256 (hex, no `0x`) of this preimage, UTF-8:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
METHOD
|
|
153
|
+
canonical URL
|
|
154
|
+
canonical JSON body
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`METHOD` is stripped and uppercased. The digest includes the HTTP method, the canonical URL, and the canonical JSON body.
|
|
158
|
+
|
|
159
|
+
The digest does not include HTTP headers, `Authorization`, `Content-Type`, x402 headers, cookies, `User-Agent`, other transport metadata, TLS metadata, or the network connection. `post()` can still send `headers=` to the target after COMMIT. Those headers are not sent to the Oracle and are not in the digest.
|
|
160
|
+
|
|
161
|
+
The Oracle receives the action, the target, and the JSON body in the `response` field, not only the digest, because checks such as jailbreak evaluate that text. On the production Oracle that text is sent to `https://webhook.fronesislabs.com`, which is the same disclosure as publishing the decision on the public audit board.
|
|
162
|
+
|
|
163
|
+
If a header changes what the request means, the Oracle decision is not cryptographically bound to that header.
|
|
164
|
+
|
|
165
|
+
### Canonicalization
|
|
166
|
+
|
|
167
|
+
This is this package's own deterministic scheme. It is not RFC 8785 JCS.
|
|
168
|
+
|
|
169
|
+
- `dcl.guard.canonical_json()`: sorted object keys, compact separators `( ",", ":" )`, `ensure_ascii=False`. Array order is preserved. No Unicode normalization.
|
|
170
|
+
- `dcl.guard.canonical_url()`: strip surrounding whitespace, lowercase scheme and hostname, drop the fragment, omit default ports 80 and 443, keep any other port, sort decoded query pairs and encode them again, keep the path, and use `/` when the path is empty.
|
|
171
|
+
|
|
172
|
+
A published vector is tested by hashing that exact preimage, not by calling `request_digest()` twice. Method `POST`, URL `https://API.Example.com:443/orders?b=2&a=1#fragment`, JSON `{"z":1,"a":"é"}` canonicalizes to:
|
|
173
|
+
|
|
174
|
+
```text
|
|
175
|
+
POST
|
|
176
|
+
https://api.example.com/orders?a=1&b=2
|
|
177
|
+
{"a":"é","z":1}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
SHA-256: `102854ec6909e5be3774fffbfc7bee1922341a2dede88277f7e30f12bc3a8123`.
|
|
181
|
+
|
|
182
|
+
Semantically similar URLs or JSON values that this scheme does not normalize (path percent-encoding, object key duplicates, `1` versus `1.0`) produce different digests. That is the current scheme, not an accident to paper over.
|
|
183
|
+
|
|
184
|
+
### Enforcement boundary
|
|
185
|
+
|
|
186
|
+
This is library-level enforcement inside the process that calls `DCLGuard.post()`. It does not enforce anything at the network. An application can still POST the same URL with another client and skip the guard.
|
|
187
|
+
|
|
188
|
+
### Oracle trust boundary
|
|
189
|
+
|
|
190
|
+
Three different things:
|
|
191
|
+
|
|
192
|
+
1. Request-digest binding: the guard compares the digest it computed with the `request_digest` string in the Oracle JSON.
|
|
193
|
+
2. Oracle transport and trust: the default client uses HTTPS/TLS to the configured Oracle URL and does not follow Oracle redirects. Unsigned JSON remains supported. When OracleTrustConfig is configured, the guard verifies the Oracle signed wrapper before accepting the response.
|
|
194
|
+
3. Audit-chain verification: `dcl_core.verify_chain()` recomputes a chain record hash. The guard does not do that.
|
|
195
|
+
|
|
196
|
+
Python `DCLGuard` with `trust=OracleTrustConfig(...)` verifies a signed wrapper. Without `trust`, it uses the current unsigned path. The TypeScript guard is still unsigned. Nonce replay protection is not implemented. HTTPS/TLS to the configured Oracle endpoint is still required. This does not mean every Oracle response is cryptographically signed.
|
|
197
|
+
|
|
198
|
+
### Signed decision envelope
|
|
199
|
+
|
|
200
|
+
[Signed Oracle decision envelope v1](docs/SIGNED_ORACLE_DECISION_ENVELOPE_V1.md) is the contract. Python `DCLGuard` verifies it only when the caller passes `trust=OracleTrustConfig(...)`. `trust=None`, the default, keeps the current unsigned Oracle JSON. Passing `trust` does not reject that unsigned JSON. A signed wrapper without `trust` stays a denial. The TypeScript guard does not verify envelopes yet.
|
|
201
|
+
|
|
202
|
+
The signed message is the UTF-8 canonical bytes of the ten-field envelope, not the HTTP headers and not `oracle.tx_hash`. The public key comes from `trust.keys`, not from the Oracle body. Unknown `key_id` is a denial. `COMMIT` after a signed wrapper requires a valid Ed25519 signature plus the envelope's `request_digest`, `trace_id`, `issued_at`, and `expires_at` checks. Nonce format is checked and is not stored; that is not replay protection. The signature does not replace HTTPS/TLS. The request-digest algorithm, Audit Event v1.0, and the production proof JSON stay as they are. `oracle.tx_hash` remains the audit-chain hash, not a Base transaction.
|
|
203
|
+
|
|
204
|
+
`verdict` must be exactly `COMMIT` or exactly `NO_COMMIT`. `COMMITTED`, `commit`, `COMMIT `, and any other string are denials. `allowed` is true only when `verdict == "COMMIT"`.
|
|
205
|
+
|
|
206
|
+
### Failure behavior
|
|
207
|
+
|
|
208
|
+
These outcomes are fail-closed in `tests/test_dcl_guard.py` and `packages/dcl/test/guard.test.ts`. `result.executed` is false and the target server used by the test receives no request, except the target-redirect case noted below:
|
|
209
|
+
|
|
210
|
+
- Oracle HTTP 402, including a body that says `COMMIT`
|
|
211
|
+
- Oracle HTTP 404 and HTTP 500
|
|
212
|
+
- Oracle timeout
|
|
213
|
+
- Oracle connection failure
|
|
214
|
+
- any other exception from the Oracle transport
|
|
215
|
+
- malformed JSON, JSON that is not an object, missing verdict, unknown verdict, missing or non-string `reason`
|
|
216
|
+
- `COMMIT` with no `request_digest`, an empty digest, or a different digest
|
|
217
|
+
- `NO_COMMIT`
|
|
218
|
+
- Oracle HTTP redirect (not followed)
|
|
219
|
+
- target HTTP redirect (not followed; see below)
|
|
220
|
+
|
|
221
|
+
### Redirect behavior
|
|
222
|
+
|
|
223
|
+
The default Oracle client (`_urllib_transport`, and `redirect: "manual"` in TypeScript) does not follow a 3xx from the Oracle. The guard then denies the side effect. A custom `transport` is responsible for its own Oracle requests; the guard still will not call the target unless that transport returns exact `COMMIT` plus the matching digest.
|
|
224
|
+
|
|
225
|
+
The target POST also does not follow redirects. `urllib` redirect codes are turned into `TargetRedirectRefused` (TypeScript uses `redirect: "manual"`). `Location` is not requested.
|
|
226
|
+
|
|
227
|
+
Known case: `request_sent` is true and `executed` is false (`requestSent` in TypeScript). The POST to the digested URL has already been sent, so that host may have seen the request. `executed: false` does not mean the request never left this process. The redirect destination is not contacted.
|
|
228
|
+
|
|
229
|
+
### Production proof
|
|
230
|
+
|
|
231
|
+
Live runs against `https://webhook.fronesislabs.com` and `https://httpbin.org/post`, from commit `79390fc93f9ecf0baebe96e750d25e133ff1ef3e`:
|
|
232
|
+
|
|
233
|
+
[Production proof](examples/production_web2_proof.py)
|
|
234
|
+
|
|
235
|
+
Positive path, `2026-10-02T06:45:16Z`: Oracle 402 → x402 payment → `COMMIT` with `request_digest` `841bae2cf5392e1c55c4be5bae4187722df1af40166b1f8fc5c485d515b70019` → target POST 200.
|
|
236
|
+
|
|
237
|
+
Negative path, `2026-10-02T06:46:17Z`: Oracle 402 → x402 payment → `NO_COMMIT` (`forbidden: 'jailbreak'`) with `request_digest` `bc6bc2e1d5e9101a177d0d07469670d81699305e41e23b6fe6ff46d70b8b806e` → the guard did not call the target.
|
|
238
|
+
|
|
239
|
+
Evidence:
|
|
240
|
+
|
|
241
|
+
- [production_web2_proof.json](examples/production_web2_proof.json)
|
|
242
|
+
- [production_web2_negative_proof.json](examples/production_web2_negative_proof.json)
|
|
243
|
+
|
|
244
|
+
`oracle.tx_hash` is the identifier the Oracle returned. In the DCL chain, `ChainState.append` sets that field to `0x` plus the SHA-256 of the canonical audit-chain record. It is not a Base blockchain transaction hash. Positive: `0xaa13b48b6089eb31c9050719b61705d2a33dce911e0299b32c5244cb8821db83`. Negative: `0xa244dccca0682a89441f99bc982ec1f260a2493f667e80c72bd1c4a22d846689`.
|
|
245
|
+
|
|
246
|
+
`payment.tx_hash` on the positive proof is the x402 settlement transaction: `0x33f8af305b255b1a5d15b891f036b5940a47afa90fe9742201ea35af28ce2ad1`.
|
|
247
|
+
|
|
248
|
+
These two JSON files, from commit `79390fc93f9ecf0baebe96e750d25e133ff1ef3e` at `2026-10-02T06:45:16Z` and `2026-10-02T06:46:17Z`, are the current evidence. This repository does not contain an earlier proof note. A write-up that cites commit `8b620d0` or different hashes is a previous run, not a second canonical proof. The identifier that note called an Oracle transaction reference is the audit-chain hash (`oracle.tx_hash`), not a Base transaction.
|
|
249
|
+
|
|
250
|
+
### Evidence limitations
|
|
251
|
+
|
|
252
|
+
The production proof shows what this client observed: the 402, one Oracle payment, the Oracle JSON, the digest comparison, the COMMIT or NO_COMMIT branch, and whether `DCLGuard.post()` then called the target.
|
|
253
|
+
|
|
254
|
+
The negative proof's `target.called: false` means this client did not run the target POST after `NO_COMMIT`. `httpbin.org` does not provide an independent call counter in this proof. The proof does not show that no other client reached httpbin.
|
|
255
|
+
|
|
256
|
+
The JSON does not contain the audit-chain fields `verify_chain()` needs (`index`, `prev_hash`, `input_hash`, `policy_hash`, `confidence`, `timestamp`, `drift_context`, and the rest of the canonical record). It is not an offline cryptographic proof of the Oracle verdict, and it does not authenticate the Oracle response.
|
|
257
|
+
|
|
258
|
+
It also does not provide network-level enforcement or stop another code path from skipping `DCLGuard`.
|
|
259
|
+
|
|
260
|
+
### Payment
|
|
261
|
+
|
|
262
|
+
x402 payment is not part of `DCLGuard`. The proof script passes `examples/oracle_x402_transport.py` as the Oracle `transport`. That adapter reads `DCL_PAYER_PRIVATE_KEY`, or `X402_PRIVATE_KEY` as its only alias. A general `PRIVATE_KEY` is ignored, even when it is set for another wallet. If neither accepted variable is set, the adapter raises before any network call or signature and names the missing variable. It will not pay more than `DCL_MAX_PAYMENT_USDC` (default `0.01`). The guard never sees the key and never signs a payment. The target POST stays unpaid.
|
|
263
|
+
|
|
264
|
+
Do not commit a private key or a seed phrase. Use a dedicated wallet for automated x402 payments, keep only a small operational balance, and do not point this adapter at a treasury or personal wallet. Rotate a credential that has been exposed.
|
|
265
|
+
|
|
266
|
+
See [Protect an HTTP side effect with DCL](docs/PROTECT_HTTP_SIDE_EFFECT.md) and [examples/README.md](examples/README.md).
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Developer-facing gate in front of an existing DCL Trust Oracle.
|
|
2
|
+
|
|
3
|
+
Ask the Oracle whether an HTTP side effect may run. Execute the call only
|
|
4
|
+
when the verdict is COMMIT. Timeouts, malformed replies, server errors, and
|
|
5
|
+
unpaid checks fail closed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dcl.guard import DCLGuard, Decision, OracleTrustConfig, SideEffectResult, request_digest
|
|
9
|
+
from dcl.sandbox import LocalSandbox
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DCLGuard",
|
|
13
|
+
"Decision",
|
|
14
|
+
"LocalSandbox",
|
|
15
|
+
"OracleTrustConfig",
|
|
16
|
+
"SideEffectResult",
|
|
17
|
+
"request_digest",
|
|
18
|
+
]
|