custody-ledger 0.3.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.
Files changed (37) hide show
  1. custody_ledger-0.3.0/.github/workflows/publish.yml +97 -0
  2. custody_ledger-0.3.0/.gitignore +8 -0
  3. custody_ledger-0.3.0/PKG-INFO +229 -0
  4. custody_ledger-0.3.0/README.md +202 -0
  5. custody_ledger-0.3.0/demo/build_page.py +38 -0
  6. custody_ledger-0.3.0/demo/fixtures/bank-statement-2026-06.txt +11 -0
  7. custody_ledger-0.3.0/demo/fixtures/paystub-2026-07-15.txt +16 -0
  8. custody_ledger-0.3.0/demo/fixtures/w2-2025.txt +8 -0
  9. custody_ledger-0.3.0/demo/ledger.json +363 -0
  10. custody_ledger-0.3.0/demo/pipeline.py +189 -0
  11. custody_ledger-0.3.0/docs/information-security.md +119 -0
  12. custody_ledger-0.3.0/docs/ll-2026-04.md +127 -0
  13. custody_ledger-0.3.0/pyproject.toml +42 -0
  14. custody_ledger-0.3.0/src/custody/__init__.py +31 -0
  15. custody_ledger-0.3.0/src/custody/adapters.py +121 -0
  16. custody_ledger-0.3.0/src/custody/chain.py +174 -0
  17. custody_ledger-0.3.0/src/custody/cli.py +311 -0
  18. custody_ledger-0.3.0/src/custody/examiner.py +118 -0
  19. custody_ledger-0.3.0/src/custody/ledger.py +306 -0
  20. custody_ledger-0.3.0/src/custody/page.html +576 -0
  21. custody_ledger-0.3.0/src/custody/redact.py +81 -0
  22. custody_ledger-0.3.0/src/custody/render.py +52 -0
  23. custody_ledger-0.3.0/src/custody/server.py +179 -0
  24. custody_ledger-0.3.0/src/custody/signing.py +201 -0
  25. custody_ledger-0.3.0/src/custody/store.py +186 -0
  26. custody_ledger-0.3.0/src/custody/verify.py +203 -0
  27. custody_ledger-0.3.0/tests/canon_check.js +39 -0
  28. custody_ledger-0.3.0/tests/test_chain.py +195 -0
  29. custody_ledger-0.3.0/tests/test_concurrency.py +97 -0
  30. custody_ledger-0.3.0/tests/test_crosslang.py +78 -0
  31. custody_ledger-0.3.0/tests/test_gate.py +206 -0
  32. custody_ledger-0.3.0/tests/test_ledger.py +266 -0
  33. custody_ledger-0.3.0/tests/test_serve_auth.py +117 -0
  34. custody_ledger-0.3.0/tests/test_signing.py +150 -0
  35. custody_ledger-0.3.0/tests/test_standalone_verifier.py +118 -0
  36. custody_ledger-0.3.0/tests/verify_like_the_page.js +68 -0
  37. custody_ledger-0.3.0/verify_packet.py +185 -0
@@ -0,0 +1,97 @@
1
+ name: publish
2
+
3
+ # Publishing happens on a version tag, and only after the tests pass.
4
+ #
5
+ # There is no API token anywhere in this workflow or in the repository secrets.
6
+ # PyPI Trusted Publishing exchanges GitHub's OIDC identity for a short-lived
7
+ # upload token at the moment of publish, so there is no long-lived credential to
8
+ # leak, rotate, or find in a repo three years from now. For a project whose
9
+ # subject is evidence integrity, a stored publishing secret would be a poor look.
10
+ #
11
+ # One-time setup on PyPI (Your projects -> Publishing -> Add a pending publisher):
12
+ # PyPI project name custody-ledger
13
+ # Owner Himansh97
14
+ # Repository name custody
15
+ # Workflow name publish.yml
16
+ # Environment name pypi
17
+ #
18
+ # Then: git tag v0.2.0 && git push origin v0.2.0
19
+
20
+ on:
21
+ push:
22
+ tags: ["v*"]
23
+ workflow_dispatch:
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ test:
30
+ # Publishing an artifact nobody ran is how a broken release reaches people
31
+ # who trusted the version number. The whole suite gates the upload.
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: actions/setup-python@v5
36
+ with:
37
+ python-version: "3.12"
38
+ - name: Install
39
+ run: |
40
+ python -m pip install --upgrade pip
41
+ pip install -e .
42
+ - name: Run the suite
43
+ run: |
44
+ set -e
45
+ for t in tests/test_*.py; do
46
+ echo "--- $t"
47
+ python "$t"
48
+ done
49
+ - name: Verify the demo ledger the way a browser would
50
+ run: |
51
+ python demo/pipeline.py
52
+ node tests/verify_like_the_page.js demo/ledger.json
53
+
54
+ build:
55
+ needs: test
56
+ runs-on: ubuntu-latest
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+ - uses: actions/setup-python@v5
60
+ with:
61
+ python-version: "3.12"
62
+ - name: Build
63
+ run: |
64
+ python -m pip install --upgrade pip build twine
65
+ python -m build
66
+ twine check dist/*
67
+ - name: Install the built wheel and exercise the CLI
68
+ # The wheel is what users get, so the wheel is what gets tested -- not
69
+ # the source tree it came from. This is the step that caught page.html
70
+ # missing from the package and serve dying on a second thread.
71
+ run: |
72
+ python -m venv /tmp/check
73
+ /tmp/check/bin/pip install dist/*.whl
74
+ cd /tmp
75
+ /tmp/check/bin/custody keygen --out k.key
76
+ printf 'Wages 98,410.00\n' > w2.txt
77
+ printf '{"fields":{"wages":98410.0},"confidence":0.95,"citations":{"wages":"w2"}}\n' > r.json
78
+ /tmp/check/bin/custody run --loan 1 --principal ci@example.com \
79
+ --instruction "Extract wages." --doc w2.txt --replay r.json --key k.key
80
+ /tmp/check/bin/custody verify custody.db
81
+ - uses: actions/upload-artifact@v4
82
+ with:
83
+ name: dist
84
+ path: dist/
85
+
86
+ publish:
87
+ needs: build
88
+ runs-on: ubuntu-latest
89
+ environment: pypi
90
+ permissions:
91
+ id-token: write # the only credential involved; nothing is stored
92
+ steps:
93
+ - uses: actions/download-artifact@v4
94
+ with:
95
+ name: dist
96
+ path: dist/
97
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.db
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ custody-signing.key
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.5
2
+ Name: custody-ledger
3
+ Version: 0.3.0
4
+ Summary: A signed chain of evidence for AI decisions in mortgage lending
5
+ Project-URL: Homepage, https://himansh97.github.io/custody.html
6
+ Project-URL: Source, https://github.com/Himansh97/custody
7
+ Author-email: Himanshu Srivastava <hsrivast22@gmail.com>
8
+ License: MIT
9
+ Keywords: ai-governance,audit,compliance,ll-2026-04,mortgage
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Financial and Insurance Industry
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Office/Business :: Financial
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: cryptography>=41
17
+ Provides-Extra: anthropic
18
+ Requires-Dist: anthropic>=0.40; extra == 'anthropic'
19
+ Provides-Extra: azure
20
+ Requires-Dist: azure-identity>=1.15; extra == 'azure'
21
+ Requires-Dist: azure-keyvault-keys>=4.9; extra == 'azure'
22
+ Provides-Extra: dev
23
+ Requires-Dist: anthropic>=0.40; extra == 'dev'
24
+ Requires-Dist: azure-identity>=1.15; extra == 'dev'
25
+ Requires-Dist: azure-keyvault-keys>=4.9; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Custody
29
+
30
+ **A signed chain of evidence for AI decisions in mortgage lending.**
31
+
32
+ Fannie Mae Lender Letter [LL-2026-04](https://singlefamily.fanniemae.com/news-events/lender-letter-ll-2026-04-governance-framework-use-artificial-intelligence-and-machine-learning)
33
+ took effect on 6 August 2026. Seller/servicers using AI or ML in origination or
34
+ servicing must have policies governing its development, use and maintenance,
35
+ must extend governance no less protective to their vendors, and **on Fannie
36
+ Mae's request must promptly disclose the types of AI/ML in use, the purpose and
37
+ manner of that use, and the safeguards implemented to mitigate the risks.**
38
+
39
+ There are two ways to answer that request. One is a document describing what you
40
+ intend to happen. The other is a record of what did happen, decision by decision,
41
+ that the person asking can verify without taking your word for it.
42
+
43
+ Custody produces the second.
44
+
45
+ **It is worth being precise, because vendor summaries of this letter are not.**
46
+ LL-2026-04 does not specify a record schema, does not name any fields, and does
47
+ not require append-only or signed logs. The design below is one implementation of
48
+ the letter's disclosure and safeguard obligations — a defensible one, and the one
49
+ this library takes. It is not a transcription of Fannie Mae's instructions.
50
+ `docs/ll-2026-04.md` sets out the letter's actual requirements and marks the
51
+ boundary between them and our choices.
52
+
53
+ **[Live demo](https://himansh97.github.io/custody.html)** — a synthetic loan file
54
+ through five AI steps, with the hash chain re-verified in your own browser. Break
55
+ a record and watch it get caught.
56
+
57
+ ---
58
+
59
+ ## The idea
60
+
61
+ Compliance logging fails when it is a separate step. Someone adds a model call in
62
+ a hurry, the `log.write(...)` after it never gets written, and nobody notices
63
+ until an examiner asks. So here the call *is* the record:
64
+
65
+ ```python
66
+ from custody import Ledger
67
+
68
+ ledger = Ledger(policy="uw-policy-v3.2", signing_key=key)
69
+
70
+ with ledger.decision(loan="1000254", principal="jane@lender.com",
71
+ purpose="income_calculation", identifiers=[borrower]) as d:
72
+
73
+ out = d.call(model="claude-sonnet-5", prompt=prompt,
74
+ sources=[paystub, w2], invoke=call_the_model)
75
+
76
+ verdict = d.gate(out, citations={"monthly_income": "paystub-2026-07-15"},
77
+ confidence=0.91)
78
+
79
+ if verdict.ok:
80
+ d.commit(outcome=out)
81
+ else:
82
+ d.route_to_human(queue="uw-review")
83
+ ```
84
+
85
+ Every field is filled as a side effect of normal use. A decision
86
+ that is opened and abandoned still writes a record. One that raises writes a
87
+ record and re-raises. An audit trail with holes where the awkward cases were is
88
+ worse than none, because it looks complete.
89
+
90
+ ## The gate
91
+
92
+ Deterministic checks only — no model judges another model. Given the same output
93
+ and the same sources, the verdict is the same forever, and an examiner can re-run
94
+ it.
95
+
96
+ | Check | Rejects |
97
+ |---|---|
98
+ | Figure binding | a number in the output that appears in no supplied source document |
99
+ | Field grounding | an extracted field that cites no source |
100
+ | Closed vocabulary | a classification outside its allowed set |
101
+ | Confidence floor | *routes to a human* below threshold — not being sure is not being wrong |
102
+
103
+ Verdicts are `pass` / `review` / `reject`. That verdict is the recorded
104
+ `response_treatment` — the evidence that a safeguard ran and what it concluded.
105
+
106
+ ## The chain
107
+
108
+ Each record's hash covers the previous record's hash, and each hash is signed.
109
+ The two answer different questions: the chain says *was anything changed or
110
+ removed*, the signature says *did this come from the system that claims to have
111
+ written it*. The chain needs no key to verify, which is why the demo page can
112
+ re-verify in a visitor's own browser.
113
+
114
+ Storage is append-only, enforced by SQLite triggers rather than by convention,
115
+ and appends are atomic so concurrent writers cannot fork the chain.
116
+
117
+ ## Where the signing key lives
118
+
119
+ A key in a file is a development convenience, and the first thing a lender's
120
+ security review will object to. So the signer is pluggable:
121
+
122
+ ```python
123
+ from custody.signing import KeyVaultSigner
124
+ ledger = Ledger(policy="uw-v3", signer=KeyVaultSigner(vault_url, "custody"))
125
+ ```
126
+
127
+ The private key never enters the process. Custody sends a digest and gets a
128
+ signature back; the most an attacker gets from a compromised host is the ability
129
+ to sign while they hold the credential, which the vault logs.
130
+
131
+ **Azure Key Vault has no Ed25519** -- EC and RSA only, and AWS KMS is the same.
132
+ So the algorithm is a parameter, not a constant: `ed25519` locally,
133
+ `ecdsa-p256-sha256` (ES256) in a vault. It is written into the hashed body of
134
+ every record, so a downgraded algorithm claim breaks the chain before anyone
135
+ reaches a signature check.
136
+
137
+ ```bash
138
+ az keyvault key create --vault-name <vault> --name custody --kty EC --curve P-256
139
+ export CUSTODY_KEY_VAULT=https://<vault>.vault.azure.net/
140
+ export CUSTODY_KEY_NAME=custody
141
+ ```
142
+
143
+ ## Verifying without trusting us
144
+
145
+ `verify_packet.py` is a single file with no dependency on this package and
146
+ nothing outside the standard library. An auditor reads it end to end in a few
147
+ minutes and satisfies themselves the cryptography is real:
148
+
149
+ ```bash
150
+ python3 verify_packet.py packet.json
151
+ ```
152
+
153
+ With `cryptography` installed it checks signatures too; without it, it checks
154
+ the chain and says plainly that it did not check signatures rather than printing
155
+ a bare OK. It also states what it cannot prove -- that the records are *true*,
156
+ and that nothing was withheld -- because a verifier that only ever says OK
157
+ teaches people to over-read it.
158
+
159
+ ## Install and run it
160
+
161
+ Not on PyPI yet, so install from the repo:
162
+
163
+ ```bash
164
+ pip install "git+https://github.com/Himansh97/custody" # one dependency: cryptography
165
+ custody keygen # Ed25519 signing key, mode 600
166
+
167
+ custody run --loan 1000254 --principal you@lender.com \
168
+ --instruction "Extract qualifying monthly income." \
169
+ --doc paystub.txt --doc w2.txt \
170
+ --redact "Borrower Name" --model claude-sonnet-5
171
+
172
+ custody verify custody.db --public-key <hex> # recompute the chain
173
+ custody packet 1000254 --out packet.json # evidence for one loan
174
+ custody serve # prints a URL with a one-time token
175
+ ```
176
+
177
+ `custody serve` binds loopback and mints a token unless you supply one, and
178
+ refuses outright to bind anywhere else without one &mdash; it is serving an audit
179
+ trail containing loan numbers. A shared token is a floor, not a control; put it
180
+ behind your SSO before anyone but you uses it.
181
+
182
+ `custody run` calls a real model when `ANTHROPIC_API_KEY` is set
183
+ (`pip install "custody-ledger[anthropic] @ git+https://github.com/Himansh97/custody"`), or replays a fixed response with
184
+ `--replay fixture.json`. The gate neither knows nor cares which.
185
+
186
+ Custody does not call your model on your behalf in library use &mdash; it wraps a
187
+ call you already make. A governance layer that requires you to rewrite your AI
188
+ does not get adopted.
189
+
190
+ ## From a source checkout
191
+
192
+ ```bash
193
+ python demo/pipeline.py # the synthetic loan, produces demo/ledger.json
194
+ python demo/build_page.py # bakes the review page from that ledger
195
+ python tests/test_chain.py # and test_gate, test_ledger, test_crosslang
196
+ node tests/verify_like_the_page.js demo/ledger.json
197
+ ```
198
+
199
+ The demo's model outputs are fixed, so the ledger is reproducible byte-for-byte
200
+ by anyone who clones this.
201
+
202
+ `tests/test_crosslang.py` diffs the Python and JavaScript canonicalisers against
203
+ every shipped record. If they ever drift, the browser would report tampering on
204
+ an honest ledger &mdash; the most damaging failure this project could have &mdash; so
205
+ the agreement is tested rather than assumed.
206
+
207
+ ## What this is not
208
+
209
+ Not legal advice, it does not certify compliance, and it is not a statement of
210
+ what Fannie Mae requires. It does no bias or fair-lending
211
+ testing — that is an ECOA and fair-lending obligation rather than something this
212
+ letter specifies, it is a separate product, and vendors already occupy it. It writes to no loan origination system. It cannot
213
+ inventory models it never sees.
214
+
215
+ `docs/ll-2026-04.md` sets out what the letter actually says, what Custody helps
216
+ with, and the obligations it does not touch at all.
217
+
218
+ `docs/information-security.md` does the same against Fannie Mae's Information
219
+ Security and Business Resiliency Supplement, control by control. It says no five
220
+ times &mdash; encryption at rest, encryption in transit, real access management, log
221
+ retention, and independent review of the cryptography. A buyer should start from
222
+ that page rather than from a questionnaire.
223
+
224
+ ## Data
225
+
226
+ All demo data is synthetic — borrower, employer, documents, loan number. There is
227
+ no real PII in this repository. The demo signing key is committed on purpose so
228
+ the shipped ledger verifies for anyone who clones it; a real deployment keeps its
229
+ private key in a KMS and never in source.
@@ -0,0 +1,202 @@
1
+ # Custody
2
+
3
+ **A signed chain of evidence for AI decisions in mortgage lending.**
4
+
5
+ Fannie Mae Lender Letter [LL-2026-04](https://singlefamily.fanniemae.com/news-events/lender-letter-ll-2026-04-governance-framework-use-artificial-intelligence-and-machine-learning)
6
+ took effect on 6 August 2026. Seller/servicers using AI or ML in origination or
7
+ servicing must have policies governing its development, use and maintenance,
8
+ must extend governance no less protective to their vendors, and **on Fannie
9
+ Mae's request must promptly disclose the types of AI/ML in use, the purpose and
10
+ manner of that use, and the safeguards implemented to mitigate the risks.**
11
+
12
+ There are two ways to answer that request. One is a document describing what you
13
+ intend to happen. The other is a record of what did happen, decision by decision,
14
+ that the person asking can verify without taking your word for it.
15
+
16
+ Custody produces the second.
17
+
18
+ **It is worth being precise, because vendor summaries of this letter are not.**
19
+ LL-2026-04 does not specify a record schema, does not name any fields, and does
20
+ not require append-only or signed logs. The design below is one implementation of
21
+ the letter's disclosure and safeguard obligations — a defensible one, and the one
22
+ this library takes. It is not a transcription of Fannie Mae's instructions.
23
+ `docs/ll-2026-04.md` sets out the letter's actual requirements and marks the
24
+ boundary between them and our choices.
25
+
26
+ **[Live demo](https://himansh97.github.io/custody.html)** — a synthetic loan file
27
+ through five AI steps, with the hash chain re-verified in your own browser. Break
28
+ a record and watch it get caught.
29
+
30
+ ---
31
+
32
+ ## The idea
33
+
34
+ Compliance logging fails when it is a separate step. Someone adds a model call in
35
+ a hurry, the `log.write(...)` after it never gets written, and nobody notices
36
+ until an examiner asks. So here the call *is* the record:
37
+
38
+ ```python
39
+ from custody import Ledger
40
+
41
+ ledger = Ledger(policy="uw-policy-v3.2", signing_key=key)
42
+
43
+ with ledger.decision(loan="1000254", principal="jane@lender.com",
44
+ purpose="income_calculation", identifiers=[borrower]) as d:
45
+
46
+ out = d.call(model="claude-sonnet-5", prompt=prompt,
47
+ sources=[paystub, w2], invoke=call_the_model)
48
+
49
+ verdict = d.gate(out, citations={"monthly_income": "paystub-2026-07-15"},
50
+ confidence=0.91)
51
+
52
+ if verdict.ok:
53
+ d.commit(outcome=out)
54
+ else:
55
+ d.route_to_human(queue="uw-review")
56
+ ```
57
+
58
+ Every field is filled as a side effect of normal use. A decision
59
+ that is opened and abandoned still writes a record. One that raises writes a
60
+ record and re-raises. An audit trail with holes where the awkward cases were is
61
+ worse than none, because it looks complete.
62
+
63
+ ## The gate
64
+
65
+ Deterministic checks only — no model judges another model. Given the same output
66
+ and the same sources, the verdict is the same forever, and an examiner can re-run
67
+ it.
68
+
69
+ | Check | Rejects |
70
+ |---|---|
71
+ | Figure binding | a number in the output that appears in no supplied source document |
72
+ | Field grounding | an extracted field that cites no source |
73
+ | Closed vocabulary | a classification outside its allowed set |
74
+ | Confidence floor | *routes to a human* below threshold — not being sure is not being wrong |
75
+
76
+ Verdicts are `pass` / `review` / `reject`. That verdict is the recorded
77
+ `response_treatment` — the evidence that a safeguard ran and what it concluded.
78
+
79
+ ## The chain
80
+
81
+ Each record's hash covers the previous record's hash, and each hash is signed.
82
+ The two answer different questions: the chain says *was anything changed or
83
+ removed*, the signature says *did this come from the system that claims to have
84
+ written it*. The chain needs no key to verify, which is why the demo page can
85
+ re-verify in a visitor's own browser.
86
+
87
+ Storage is append-only, enforced by SQLite triggers rather than by convention,
88
+ and appends are atomic so concurrent writers cannot fork the chain.
89
+
90
+ ## Where the signing key lives
91
+
92
+ A key in a file is a development convenience, and the first thing a lender's
93
+ security review will object to. So the signer is pluggable:
94
+
95
+ ```python
96
+ from custody.signing import KeyVaultSigner
97
+ ledger = Ledger(policy="uw-v3", signer=KeyVaultSigner(vault_url, "custody"))
98
+ ```
99
+
100
+ The private key never enters the process. Custody sends a digest and gets a
101
+ signature back; the most an attacker gets from a compromised host is the ability
102
+ to sign while they hold the credential, which the vault logs.
103
+
104
+ **Azure Key Vault has no Ed25519** -- EC and RSA only, and AWS KMS is the same.
105
+ So the algorithm is a parameter, not a constant: `ed25519` locally,
106
+ `ecdsa-p256-sha256` (ES256) in a vault. It is written into the hashed body of
107
+ every record, so a downgraded algorithm claim breaks the chain before anyone
108
+ reaches a signature check.
109
+
110
+ ```bash
111
+ az keyvault key create --vault-name <vault> --name custody --kty EC --curve P-256
112
+ export CUSTODY_KEY_VAULT=https://<vault>.vault.azure.net/
113
+ export CUSTODY_KEY_NAME=custody
114
+ ```
115
+
116
+ ## Verifying without trusting us
117
+
118
+ `verify_packet.py` is a single file with no dependency on this package and
119
+ nothing outside the standard library. An auditor reads it end to end in a few
120
+ minutes and satisfies themselves the cryptography is real:
121
+
122
+ ```bash
123
+ python3 verify_packet.py packet.json
124
+ ```
125
+
126
+ With `cryptography` installed it checks signatures too; without it, it checks
127
+ the chain and says plainly that it did not check signatures rather than printing
128
+ a bare OK. It also states what it cannot prove -- that the records are *true*,
129
+ and that nothing was withheld -- because a verifier that only ever says OK
130
+ teaches people to over-read it.
131
+
132
+ ## Install and run it
133
+
134
+ Not on PyPI yet, so install from the repo:
135
+
136
+ ```bash
137
+ pip install "git+https://github.com/Himansh97/custody" # one dependency: cryptography
138
+ custody keygen # Ed25519 signing key, mode 600
139
+
140
+ custody run --loan 1000254 --principal you@lender.com \
141
+ --instruction "Extract qualifying monthly income." \
142
+ --doc paystub.txt --doc w2.txt \
143
+ --redact "Borrower Name" --model claude-sonnet-5
144
+
145
+ custody verify custody.db --public-key <hex> # recompute the chain
146
+ custody packet 1000254 --out packet.json # evidence for one loan
147
+ custody serve # prints a URL with a one-time token
148
+ ```
149
+
150
+ `custody serve` binds loopback and mints a token unless you supply one, and
151
+ refuses outright to bind anywhere else without one &mdash; it is serving an audit
152
+ trail containing loan numbers. A shared token is a floor, not a control; put it
153
+ behind your SSO before anyone but you uses it.
154
+
155
+ `custody run` calls a real model when `ANTHROPIC_API_KEY` is set
156
+ (`pip install "custody-ledger[anthropic] @ git+https://github.com/Himansh97/custody"`), or replays a fixed response with
157
+ `--replay fixture.json`. The gate neither knows nor cares which.
158
+
159
+ Custody does not call your model on your behalf in library use &mdash; it wraps a
160
+ call you already make. A governance layer that requires you to rewrite your AI
161
+ does not get adopted.
162
+
163
+ ## From a source checkout
164
+
165
+ ```bash
166
+ python demo/pipeline.py # the synthetic loan, produces demo/ledger.json
167
+ python demo/build_page.py # bakes the review page from that ledger
168
+ python tests/test_chain.py # and test_gate, test_ledger, test_crosslang
169
+ node tests/verify_like_the_page.js demo/ledger.json
170
+ ```
171
+
172
+ The demo's model outputs are fixed, so the ledger is reproducible byte-for-byte
173
+ by anyone who clones this.
174
+
175
+ `tests/test_crosslang.py` diffs the Python and JavaScript canonicalisers against
176
+ every shipped record. If they ever drift, the browser would report tampering on
177
+ an honest ledger &mdash; the most damaging failure this project could have &mdash; so
178
+ the agreement is tested rather than assumed.
179
+
180
+ ## What this is not
181
+
182
+ Not legal advice, it does not certify compliance, and it is not a statement of
183
+ what Fannie Mae requires. It does no bias or fair-lending
184
+ testing — that is an ECOA and fair-lending obligation rather than something this
185
+ letter specifies, it is a separate product, and vendors already occupy it. It writes to no loan origination system. It cannot
186
+ inventory models it never sees.
187
+
188
+ `docs/ll-2026-04.md` sets out what the letter actually says, what Custody helps
189
+ with, and the obligations it does not touch at all.
190
+
191
+ `docs/information-security.md` does the same against Fannie Mae's Information
192
+ Security and Business Resiliency Supplement, control by control. It says no five
193
+ times &mdash; encryption at rest, encryption in transit, real access management, log
194
+ retention, and independent review of the cryptography. A buyer should start from
195
+ that page rather than from a questionnaire.
196
+
197
+ ## Data
198
+
199
+ All demo data is synthetic — borrower, employer, documents, loan number. There is
200
+ no real PII in this repository. The demo signing key is committed on purpose so
201
+ the shipped ledger verifies for anyone who clones it; a real deployment keeps its
202
+ private key in a KMS and never in source.
@@ -0,0 +1,38 @@
1
+ """Bake the static demo page from the committed ledger.
2
+
3
+ Built rather than hand-written so it cannot drift from the ledger it claims to
4
+ display: run `pipeline.py`, then this, and the page is showing the records the
5
+ library actually produced.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import pathlib
11
+ import sys
12
+
13
+ ROOT = pathlib.Path(__file__).resolve().parent.parent
14
+ sys.path.insert(0, str(ROOT / "src"))
15
+
16
+ from custody.render import build_html # noqa: E402
17
+
18
+ LEDGER = ROOT / "demo" / "ledger.json"
19
+ DEFAULT_OUT = pathlib.Path.home() / "careeros-portfolio" / "custody-ledger.html"
20
+
21
+
22
+ def build(out: pathlib.Path = DEFAULT_OUT) -> pathlib.Path:
23
+ if not LEDGER.exists():
24
+ raise SystemExit("demo/ledger.json is missing - run demo/pipeline.py first")
25
+ bundle = json.loads(LEDGER.read_text(encoding="utf-8"))
26
+ if not bundle["chain"]["verified"]:
27
+ # Publishing a page whose headline claim is "this verifies" while the
28
+ # ledger behind it does not would be the worst thing this could ship.
29
+ raise SystemExit(f"refusing to build: the ledger does not verify - {bundle['chain']}")
30
+ out.parent.mkdir(parents=True, exist_ok=True)
31
+ out.write_text(build_html(bundle), encoding="utf-8")
32
+ return out
33
+
34
+
35
+ if __name__ == "__main__":
36
+ target = pathlib.Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUT
37
+ written = build(target)
38
+ print(f"wrote {written} ({written.stat().st_size:,} bytes)")
@@ -0,0 +1,11 @@
1
+ NORTHWIND CREDIT UNION — STATEMENT OF ACCOUNT
2
+ Statement period: 06/01/2026 - 06/30/2026
3
+ Account ending: 4471
4
+ ------------------------------------------------------------------
5
+ Beginning balance 18,204.11
6
+ Total deposits 6,380.48
7
+ Total withdrawals 4,912.30
8
+ Ending balance 19,672.29
9
+ ------------------------------------------------------------------
10
+ 06/15 DIRECT DEP ACME LOGISTICS PAYROLL 3,190.24
11
+ 06/30 DIRECT DEP ACME LOGISTICS PAYROLL 3,190.24
@@ -0,0 +1,16 @@
1
+ ACME LOGISTICS LLC EARNINGS STATEMENT
2
+ 1400 Corporate Drive, Irving TX 75038
3
+ ------------------------------------------------------------------
4
+ Employee: [BORROWER] Employee ID: E-40881
5
+ Pay period: 07/01/2026 - 07/15/2026 Pay date: 07/15/2026
6
+ ------------------------------------------------------------------
7
+ CURRENT YEAR TO DATE
8
+ Regular 3,846.15 50,000.00
9
+ Overtime 359.85 4,672.00
10
+ Gross pay 4,206.00 54,672.00
11
+ ------------------------------------------------------------------
12
+ Federal income tax 694.00 9,022.00
13
+ Social Security 260.77 3,389.66
14
+ Medicare 60.99 792.74
15
+ ------------------------------------------------------------------
16
+ Net pay 3,190.24 41,467.60
@@ -0,0 +1,8 @@
1
+ FORM W-2 WAGE AND TAX STATEMENT — TAX YEAR 2025
2
+ Employer: ACME LOGISTICS LLC EIN: 75-3310042
3
+ ------------------------------------------------------------------
4
+ Box 1 Wages, tips, other compensation 98,410.00
5
+ Box 2 Federal income tax withheld 16,229.00
6
+ Box 3 Social Security wages 98,410.00
7
+ Box 5 Medicare wages and tips 98,410.00
8
+ Box 12 D 401(k) elective deferrals 7,200.00