toolgraph 0.1.0__tar.gz → 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 (64) hide show
  1. toolgraph-0.3.0/CHANGELOG.md +772 -0
  2. {toolgraph-0.1.0 → toolgraph-0.3.0}/PKG-INFO +310 -81
  3. {toolgraph-0.1.0 → toolgraph-0.3.0}/README.md +304 -77
  4. {toolgraph-0.1.0 → toolgraph-0.3.0}/SECURITY.md +1 -1
  5. toolgraph-0.3.0/contracts/control-preflight.schema.json +128 -0
  6. toolgraph-0.3.0/contracts/observation-window.schema.json +113 -0
  7. toolgraph-0.3.0/contracts/policy-impact.schema.json +291 -0
  8. {toolgraph-0.1.0 → toolgraph-0.3.0}/pyproject.toml +52 -18
  9. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/__init__.py +1 -1
  10. toolgraph-0.3.0/toolgraph/artifact_safety.py +171 -0
  11. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/artifacts.py +33 -16
  12. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/cli.py +313 -96
  13. toolgraph-0.3.0/toolgraph/config.py +257 -0
  14. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/control_plan.py +253 -79
  15. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/crawler/client.py +87 -18
  16. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/crawler/crawl.py +64 -15
  17. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/crawler/transports.py +13 -4
  18. toolgraph-0.3.0/toolgraph/graph/driver.py +590 -0
  19. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/graph/loader.py +8 -7
  20. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/graph/queries.py +205 -282
  21. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/graph/schema.py +134 -12
  22. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/graph/selector.py +46 -60
  23. toolgraph-0.3.0/toolgraph/health.py +230 -0
  24. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/manifest/ingest.py +473 -61
  25. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/models.py +23 -1
  26. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/policy_bundle.py +18 -24
  27. toolgraph-0.3.0/toolgraph/policy_impact.py +486 -0
  28. toolgraph-0.3.0/toolgraph/policy_impact_models.py +170 -0
  29. toolgraph-0.3.0/toolgraph/policy_propose.py +959 -0
  30. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/policy_review.py +25 -23
  31. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/preflight.py +8 -9
  32. toolgraph-0.3.0/toolgraph/quickstart/README.md +55 -0
  33. toolgraph-0.3.0/toolgraph/quickstart/governance.yaml +63 -0
  34. toolgraph-0.3.0/toolgraph/redaction.py +252 -0
  35. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/review_candidates.py +66 -93
  36. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/server/app.py +90 -15
  37. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/uris.py +1 -0
  38. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/yaml_loader.py +1 -3
  39. toolgraph-0.1.0/CHANGELOG.md +0 -153
  40. toolgraph-0.1.0/contracts/control-preflight.schema.json +0 -75
  41. toolgraph-0.1.0/toolgraph/artifact_safety.py +0 -97
  42. toolgraph-0.1.0/toolgraph/config.py +0 -106
  43. toolgraph-0.1.0/toolgraph/graph/driver.py +0 -275
  44. toolgraph-0.1.0/toolgraph/quickstart/governance.yaml +0 -28
  45. toolgraph-0.1.0/toolgraph/redaction.py +0 -82
  46. {toolgraph-0.1.0 → toolgraph-0.3.0}/.gitignore +0 -0
  47. {toolgraph-0.1.0 → toolgraph-0.3.0}/LICENSE +0 -0
  48. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/control-plan.schema.json +0 -0
  49. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/policy-bundle.schema.json +0 -0
  50. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/policy-review-plan.schema.json +0 -0
  51. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/preflight.schema.json +0 -0
  52. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/review-annotations.schema.json +0 -0
  53. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/tracegraph/README.md +0 -0
  54. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/tracegraph/review-candidates.schema.json +0 -0
  55. {toolgraph-0.1.0 → toolgraph-0.3.0}/contracts/tracegraph/v1.json +0 -0
  56. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/crawler/__init__.py +0 -0
  57. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/errors.py +0 -0
  58. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/graph/__init__.py +0 -0
  59. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/graph/store.py +0 -0
  60. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/manifest/__init__.py +0 -0
  61. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/manifest/parser.py +0 -0
  62. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/quickstart/policy_gateway_server.py +0 -0
  63. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/quickstart/servers.yaml +0 -0
  64. {toolgraph-0.1.0 → toolgraph-0.3.0}/toolgraph/server/__init__.py +0 -0
@@ -0,0 +1,772 @@
1
+ # Changelog
2
+
3
+ All notable changes to Toolgraph are documented here.
4
+
5
+ ## 0.3.0 - 2026-10-19
6
+
7
+ Fourth alpha. Read **Upgrading from 0.2.0** below before installing: it lists
8
+ twelve changes an existing setup, checkout or embedding program can notice.
9
+ The one most installations meet first is that there is no implicit backend
10
+ any more: a setup that relied on Neo4j being chosen when nothing named a
11
+ backend now sets `TOOLGRAPH_BACKEND=neo4j` and installs `toolgraph[neo4j]`.
12
+
13
+ ### Upgrading from 0.2.0
14
+
15
+ - There is no implicit backend any more (ADR-0017). 0.2.0 chose Neo4j at
16
+ `NEO4J_URI` whenever neither `TOOLGRAPH_BACKEND` nor a `backend` field in
17
+ the runtime config file named one; now a command that opens the graph
18
+ exits 1 with ``ERROR: no backend configured: run `toolgraph init`, or set
19
+ TOOLGRAPH_BACKEND to ladybug or neo4j``, `doctor` reports a Configuration
20
+ FAIL (`backend_source: none`), and `serve` and `python -m
21
+ toolgraph.server.app` exit 1 with that line before serving. `NEO4J_*`
22
+ variables alone no longer select Neo4j. A deployment that relied on the
23
+ fallback sets `TOOLGRAPH_BACKEND=neo4j` and installs `toolgraph[neo4j]`. A
24
+ `.env` copied from `.env.example` already sets it; a hand-written `.env`
25
+ with only `NEO4J_*` lines does not. A `--config` file without `backend`
26
+ also meant Neo4j; it is now refused with exit 1 and a line naming the file,
27
+ and the environment is still not consulted.
28
+ `serve` also refuses at startup, with one line and exit 1, an unknown
29
+ `TOOLGRAPH_BACKEND`, a backend variable that conflicts with the config file
30
+ and an unreadable or malformed config file found through
31
+ `TOOLGRAPH_CONFIG` or the working directory; 0.2.0 started in all four
32
+ cases and failed each tool call instead. (A malformed `--config` file was
33
+ already refused before serving, exit 2.) A missing `neo4j` extra or an
34
+ unreachable backend still surfaces per tool call.
35
+ Library callers that read `toolgraph.config.settings` with nothing
36
+ configured get `RuntimeConfigurationError`; `Settings(backend=...)` is
37
+ unaffected. `config.configure(path)` still reads the file at once but no
38
+ longer builds the settings: they are built when `config.settings` is next
39
+ read, so a file without `backend` is refused there, and a `NEO4J_*`
40
+ variable changed in between is the one used. Read `config.settings` right
41
+ after `configure()` to keep 0.2.0's timing.
42
+ - The Neo4j driver is now the `neo4j` extra, and Ladybug is a core
43
+ dependency (ADR-0017). A Neo4j deployment installs `toolgraph[neo4j]`
44
+ (`uv sync --extra neo4j` from a checkout). Without it, a command that
45
+ opens a Neo4j graph exits 1 with one line: `ERROR: backend=neo4j requires
46
+ the 'neo4j' extra: install with ...`. `toolgraph[ladybug]` still resolves,
47
+ as an empty extra, so existing install recipes keep working. Installing
48
+ Toolgraph now installs Ladybug on every platform. Ladybug 0.18.3 ships
49
+ wheels for CPython 3.12–3.14 on macOS 15+ (arm64, x86_64), manylinux
50
+ (x86_64 glibc 2.27+, aarch64 glibc 2.26+), musllinux 1.2 and Windows
51
+ (amd64, arm64). Elsewhere, for example on macOS 14, pip builds Ladybug
52
+ from its source distribution; that build has not been verified.
53
+ - Neo4j 5.7 is now the minimum server version. Toolgraph asks the server
54
+ not to send `UNRECOGNIZED` notifications, which needs Bolt 5.2; against an
55
+ older server every command that opens the graph exits 1 with `Neo4j
56
+ server is older than 5.7, the minimum Toolgraph supports; upgrade the
57
+ server` (measured on 5.6 and 4.4). Before this change the README
58
+ walkthrough ran on 5.6, with notification noise, and on 4.4 seven of its
59
+ eighteen commands already failed with Cypher syntax errors. Library
60
+ callers get `BackendConfigurationError` from the same seams as below.
61
+ Tooling that reads stderr no longer sees `Received notification from DBMS
62
+ server` lines for `UNRECOGNIZED` notifications; warning-level ones, such
63
+ as deprecations, still appear.
64
+ - `toolgraph crawl` now requires the `servers` key in `servers.yaml`. A
65
+ blank file, a comment-only file or `{}` exits 1 with `Invalid servers
66
+ config` before any server is contacted; 0.2.0 treated those three as an
67
+ empty fleet and, under default pruning, retired every crawled server.
68
+ (`servers: null`, mapping or scalar values and unknown keys were already
69
+ rejected; a YAML set, `!!set {}`, is newly rejected instead of being
70
+ coerced to an empty list.) To retire the whole fleet on purpose, write `servers: []`
71
+ (library callers: `ServersConfig(servers=[])`); to crawl nothing and keep
72
+ the graph, add `--no-prune`. See ADR-0014.
73
+ - Crawls behave differently in three smaller ways. A server whose catalog
74
+ exceeds 10,000 records or 4 MiB of retained record JSON now fails its
75
+ crawl instead of loading (ADR-0016); raise the limit with
76
+ `--max-catalog-records` / `--max-catalog-bytes`, or pass
77
+ `budget=CatalogBudget(...)` from a library call. A failed server's reason,
78
+ printed by `crawl` and returned in the failures of `crawl_all` /
79
+ `crawl_all_async`, names the underlying exceptions instead of
80
+ `ExceptionGroup: unhandled errors in a TaskGroup`. And `crawl_all` /
81
+ `crawl_all_async` raise `ValueError` for a `concurrency` that is not a
82
+ positive `int`: `0` used to hang, while a float or `bool` was accepted.
83
+ - Read commands and MCP read tools no longer create a missing Ladybug
84
+ database: they exit 1 with one `BACKEND_UNAVAILABLE` line (MCP:
85
+ `backend_unavailable`). A Ladybug file without Toolgraph's schema, or with
86
+ another schema version, is refused by graph query commands, with a line
87
+ naming `init-schema` or the version mismatch, and by MCP read tools, as
88
+ `backend_misconfigured` with cause `schema_not_initialized` or
89
+ `schema_version_mismatch`; `check` tests connectivity only and still
90
+ prints `OK` for it. A script that let a read command
91
+ create the file runs `toolgraph init` or `init-schema` first. Writers
92
+ (`crawl`, `ingest-manifest`, `init-schema`) still create it.
93
+ - `toolgraph check` and `doctor`'s Connectivity check now look in
94
+ `NEO4J_DATABASE`. With a database the server does not have, `check` exits
95
+ 1 where 0.2.0 printed `OK`, and Connectivity FAILs where it passed. A
96
+ health check built on either now reports that setup as broken. Separately,
97
+ `doctor` and `doctor --json` report exactly one `Configuration` check;
98
+ when the settings could not be built, 0.2.0 reported a PASS or WARN
99
+ `Configuration` followed by a FAIL with the same name.
100
+ - MCP tools report backend configuration problems as a new error kind,
101
+ `backend_misconfigured`, with `retryable: false` and a `cause`, where 0.2.0
102
+ returned the SDK's generic `Error executing tool <name>`. A client that
103
+ switches on `error_kind` should handle it; `backend_unavailable` is
104
+ unchanged. See the first bullet under Changed for the causes.
105
+ - Code that calls `driver.session()`, `acquire_readonly_session()` or
106
+ `verify_connectivity()` and catches `neo4j.exceptions.AuthError` (or a
107
+ `ClientError` for expired credentials, the authentication rate limit or
108
+ `Database.DatabaseNotFound`) now gets
109
+ `toolgraph.graph.driver.BackendConfigurationError`, with the driver error
110
+ as `__cause__`; the CLI prints these as one `ERROR:` line, exit 1, instead
111
+ of a traceback. `BackendConfigurationError` now takes exactly one message
112
+ and an optional keyword `cause`, so constructing it with no arguments or
113
+ with two positional arguments raises `TypeError`.
114
+ - More values are refused as credential-shaped (#130). Review notes, reviewer
115
+ names, review-report run ids and tool keys, every control-plan key and string
116
+ value, and artifact identities (now including `graph_instance_id` and
117
+ `graph_state.instance_id`) refuse `name=value` and `name: value` assignments
118
+ of a token, password, secret, credential, API key or authorization name,
119
+ `Bearer` values, and credential URIs and queries, also when the name is
120
+ percent-encoded (`?%74oken=`). A review note such as `token: rotated weekly`
121
+ now has to be rephrased. `policy propose` reports a refused export value
122
+ by its input line (`<file>:<line>: tool_key contains credential-shaped
123
+ data`) instead of its position in the proposal, and refuses it before any
124
+ other error could quote it. In the other direction, `policy impact` now
125
+ accepts single-colon names such as `srv::get_token:v2` and
126
+ `agent-token:blue`, which it refused before, and so does `policy propose`
127
+ where such a name reaches its output (an observed agent, an export
128
+ `candidate`, an ambiguous grant or a graph match in a note): 0.2.0 refused
129
+ the proposal with `additions[0].agent contains credential-shaped data`. An
130
+ export row's `tool_key` must still be `<server>::<tool>` with no other
131
+ colon.
132
+ - Artifact evidence hides more around URI credentials (#204, #208, #209).
133
+ Policy bundles, preflight and control-plan artifacts, policy review plans,
134
+ `policy impact` and `policy check` output, and `ingest-manifest --dry-run
135
+ --json` scrub it in three steps. A quote, `%22` or `%27` after a URI's
136
+ scheme with an `@` anywhere after it hides everything after the scheme,
137
+ the rest of the evidence line included. Then a URI that cannot be parsed
138
+ (an unclosed `[`, an invalid bracketed host) stops the command as in
139
+ 0.2.0, now with `artifact evidence holds a URI that cannot be parsed; it
140
+ is not shown` instead of the parser's message, which could quote the host
141
+ with its password. Then a `)` with an `@` after it in the same unbroken run
142
+ hides the rest of that run. So `https://u:"pw"@[::1/r` is shown as
143
+ `https://<redacted-uri>`, while `https://u:p[)ss@h/r` stops the command.
144
+ The hiding rules also hide values that hold no password. A consumer that
145
+ matches evidence paths should expect `<scheme>://<redacted-uri>`.
146
+ - Error messages that go through Toolgraph's shared scrubber hide more (#201,
147
+ #202): CLI errors, crawl failures and the server's
148
+ backend-misconfiguration log record. A message drops what follows a quoted
149
+ or bracketed credential value; one whose scrubbed or percent-decoded form
150
+ is still credential-shaped reads `details withheld (they contain
151
+ credential-shaped data)` after the command's own context; and a
152
+ `<redacted` that the input itself contains is shown as `‹redacted`
153
+ (U+2039). CLI usage errors, `--dry-run` JSON warnings, MCP SDK errors
154
+ outside Toolgraph's own envelopes and logged tracebacks do not go through
155
+ the scrubber (#206).
156
+
157
+ ### Added
158
+
159
+ - **A per-server catalog budget for crawls** (ADR-0016). Each server's
160
+ retained tools and resources are capped at 10,000 records and 4 MiB of
161
+ record JSON (UTF-8). A server over budget fails like any other crawl
162
+ failure, so no partial catalog is loaded and ADR-0007's failure
163
+ protection applies. Configure it with `toolgraph crawl
164
+ --max-catalog-bytes N --max-catalog-records N`, or `CatalogBudget` for
165
+ `crawl_server` / `crawl_all`. It bounds retained data, not transport
166
+ buffering or peak memory. Crawl failure messages now show the underlying
167
+ error instead of `ExceptionGroup: unhandled errors in a TaskGroup`.
168
+
169
+ - **Read-only policy impact previews**: `toolgraph policy impact proposed.yaml
170
+ SERVER::TOOL... --agent AGENT` compares current and hypothetical selector
171
+ decisions with evidence. JSON reports bind graph state, governance digests,
172
+ profile and scoped catalog; `toolgraph policy check impact.json` checks the
173
+ proposed-side regression expectations against current state. Neither command
174
+ applies policy. See [the impact guide](docs/policy-impact.md).
175
+
176
+ - **A first-run notebook**, `docs/notebooks/01-first-policy-bundle.ipynb`,
177
+ walks the Beginner Guide path one command at a time inside a temporary
178
+ directory. It goes from the quickstart to a compiled bundle, then removes and
179
+ restores an evidence pointer and runs the audits. It is committed with its
180
+ outputs. `tests/test_notebooks.py` checks those outputs row by row, and a new
181
+ CI `notebooks` job re-executes the notebook from a wheel install. The job
182
+ and the regeneration recipe install `nbclient` and `ipykernel` from a new
183
+ `notebook` dependency group in `uv.lock`, so Dependabot updates them.
184
+
185
+ ### Changed
186
+
187
+ - MCP tools report a backend configuration problem as a typed result
188
+ instead of the SDK's generic `Error executing tool <name>`. Missing
189
+ `neo4j` extra, rejected Neo4j credentials, a missing `NEO4J_DATABASE`, a
190
+ Neo4j server older than 5.7, a missing Ladybug, a database file that is
191
+ not a Ladybug database and, on Ladybug, a graph without Toolgraph's schema
192
+ or with another schema version now return `isError: true` with
193
+ `structuredContent`
194
+ `{"error_kind": "backend_misconfigured", "retryable": false, "cause": ...,
195
+ "message": ...}`. The message is fixed per cause; the path or status code
196
+ goes to the server log as one WARNING record instead of a traceback. On
197
+ stdio, where the SDK's log handler wraps at 80 columns, a corrupt Ladybug
198
+ file went from 284 stderr lines to 9. A consumer that keys on
199
+ `error_kind == "backend_unavailable"` sees no change: these failures still
200
+ are not outages.
201
+
202
+ - Toolgraph no longer chooses a backend when nothing names one (ADR-0017
203
+ step 3). The backend comes from `TOOLGRAPH_BACKEND` or the runtime config
204
+ file's `backend` field, or with `--config` from that file alone; otherwise
205
+ commands that open the graph, `serve` and the MCP server module refuse with
206
+ one line, and `doctor` reports a Configuration FAIL. Commands that do not
207
+ open the graph (help, `--version`, `example init`, `review-candidates`,
208
+ `init`) are unaffected.
209
+
210
+ - Ladybug moves into the core dependencies and `neo4j` to a `[neo4j]`
211
+ extra (ADR-0017 step 2). No Ladybug path imports the Neo4j driver, so a
212
+ Ladybug install runs without it: a new required CI job, `no-extras`,
213
+ installs the wheel with no extras and checks that. Every Neo4j path
214
+ imports the driver through one guarded loader, so a missing extra is a
215
+ configuration error naming `toolgraph[neo4j]` rather than an
216
+ `ImportError`.
217
+
218
+ - `toolgraph doctor` and `doctor --json` report exactly one
219
+ `Configuration` check. When the settings cannot be built (for example a
220
+ `TOOLGRAPH_BACKEND` that contradicts the config file), that check FAILs
221
+ with the error and keeps the file details such as `backend_source`;
222
+ previously a passing `Configuration` check was followed by a second,
223
+ failing one with the same name.
224
+
225
+ - `servers.yaml` must declare its fleet explicitly (ADR-0014). The
226
+ `servers` key is required and must be a list; `servers: []` is the only
227
+ way to declare an empty fleet. A YAML set (`!!set {}`) is rejected too:
228
+ the field is strict, so it is not coerced to an empty list. The CLI
229
+ diagnostic, exit code and `--prune/--no-prune` semantics are unchanged.
230
+ ADR-0007's "empty file = empty fleet" consequence is amended accordingly.
231
+
232
+ - The fleet duplicate checks (`duplicate_name_overrides`,
233
+ `duplicate_identities`) count names in one pass, so their cost grows
234
+ linearly with fleet size instead of quadratically. Results are unchanged.
235
+ `scripts/benchmark_duplicate_checks.py` is a repeatable size sweep; on one
236
+ developer machine an 8,000-server fixture went from about 300 ms to well
237
+ under a millisecond per check.
238
+
239
+ - `SECURITY.md` now states that security fixes cover the latest published
240
+ Toolgraph release only, with no backports, instead of naming the `0.0.x`
241
+ line the policy was written for.
242
+
243
+ ### Fixed
244
+
245
+ - Evidence in artifacts no longer shows a quoted URI password (#204). Policy
246
+ bundles, preflight and control-plan artifacts, policy review plans, policy
247
+ impact and `policy check` output, and `ingest-manifest --dry-run --json`
248
+ remove the userinfo the URI parser finds and keep the path of an ordinary
249
+ URI. A password in quotes (`neo4j://u:"p w"@host/db`) was kept whole when
250
+ it held a space or `)`, and partly when it held `@` and `/`. Resource URIs
251
+ read from the graph carry the quotes percent-encoded, and some of those
252
+ leaked too: `u:%22p@x/y%22@host` kept `x/y%22`. When a quote, `%22` or `%27`
253
+ follows a URI's scheme and an `@` follows it, everything after the scheme
254
+ is now hidden as `<redacted-uri>`, path and the rest of the evidence line
255
+ included, since where such a password ends cannot be found reliably. This
256
+ also hides values that hold no password, such as `https://host/a%22b@c`.
257
+ Other values are redacted as before; a password the parser cannot bound,
258
+ such as one holding an unquoted `/`, can still show (#212). Scanning a
259
+ long line of letters is also linear now: before this fix a
260
+ 100,000-character line took 4.9 s.
261
+
262
+ - Evidence in artifacts no longer shows a URI password holding an unquoted
263
+ `)` (#208). Evidence wraps a resource in graph notation,
264
+ `(tool) -READS-> (uri)`, so the scan stopped at the first `)` and kept
265
+ `neo4j://u:p)ss@host/r` whole. When a `)` follows a URI's scheme and an
266
+ `@` follows it with no whitespace between, the rest of that run is now
267
+ hidden as `<redacted-uri>`, the resource's closing `)` included. This also
268
+ hides values that hold no password, such as `file:///a(b)@c`.
269
+
270
+ - Commands that write artifacts refuse evidence holding a URI that cannot be
271
+ parsed (#209), such as one with an unclosed `[` (`https://u:p@[::1/r`), an
272
+ invalid bracketed host or a character invalid under NFKC normalization.
273
+ `ingest-manifest --dry-run --json` failed with a traceback, and
274
+ `preflight` and `policy compile` printed the parser's message, which can
275
+ quote the host with its password (`netloc 'u:pw@h#x' contains invalid
276
+ characters ...`). They now print `ERROR: artifact evidence holds a URI that
277
+ cannot be parsed; it is not shown`, exit 1 and write nothing. The rest of
278
+ the artifact is withheld as before: printing it around a hidden URI could
279
+ show other values the scan cannot bound.
280
+
281
+ - An error message that would still show a credential after scrubbing is
282
+ withheld (#202). Scrubbing cannot recognise every credential, for example
283
+ a percent-encoded name such as `?%74oken=x` quoted in a validation error
284
+ about an observed export, a selector profile or a governance key. Such a
285
+ message now reads `details withheld (they contain credential-shaped
286
+ data)` after the command's own context, where it used to print the
287
+ value. A message whose percent-decoded form is credential-shaped is
288
+ withheld before scrubbing, since the scrubber does not decode: this also
289
+ withholds some messages it could have cleaned, such as an encoded URL with
290
+ a `?token=` query. A placeholder that the input itself contains is shown
291
+ as `‹redacted` (U+2039), so it cannot pass for one the scrubber inserted.
292
+ This covers every message that goes through the shared scrubber:
293
+ CLI errors, crawl failures and the server's backend-misconfiguration log
294
+ record. CLI usage errors, `--dry-run` JSON warnings, MCP SDK errors
295
+ outside Toolgraph's own envelopes and logged tracebacks do not go through
296
+ it yet (#206). Checking for credential URIs is also linear now: a long run of
297
+ `a:` with no `@` took seconds.
298
+
299
+ - Error text no longer shows part of a quoted credential (#201). A password
300
+ in quotes inside a URI (`neo4j://u:"p@ss"@host`) was printed whole, and a
301
+ quoted value with a space (`password="hunter 2"`, `NEO4J_PASSWORD="s3cr3t
302
+ pass"`) kept its tail. A quoted or bracketed credential value, and a URI
303
+ with a quote in its authority and an `@` anywhere after it, are now hidden
304
+ through the end of the message: the message loses what follows them, and
305
+ never shows part of the value. A URI that is only wrapped in quotes, such
306
+ as `'neo4j://host:7687'` in a driver error, is shown as before unless an
307
+ `@` follows later in the message. Scrubbing a long message is also linear
308
+ now; before this fix a 100,000-character line of letters took 25 s.
309
+
310
+ - Values that Toolgraph refuses rather than scrubs go through one rule,
311
+ `toolgraph.redaction.is_credential_shaped`, instead of five detectors that
312
+ disagreed (#130). Review notes accepted `Bearer abc123`, `"token"=abc` and
313
+ `mycredential=x`; control plans accepted `password=hunter2` and
314
+ credential-shaped keys, and echoed a forbidden or duplicate key in the
315
+ error; review metadata and graph instance ids were not checked; percent-encoded
316
+ query keys passed everywhere but `policy impact`. Errors name where a refused
317
+ value sits, never the value.
318
+
319
+ - Commands and MCP tools that only read the graph no longer create a missing
320
+ Ladybug database. Pointed at a path that was never initialized, the read
321
+ commands (`check-access`, `unmapped-tools`, `audit-report`, `drift` and
322
+ the rest) created the file, then failed with a 72-98 line `Binder
323
+ exception: Table ... does not exist` traceback, and `check` printed `OK`
324
+ for that path from then on. Now a missing file gives one line, `ERROR:
325
+ backend unavailable: BACKEND_UNAVAILABLE: Ladybug database <path> does
326
+ not exist or is not a file`, and MCP returns `backend_unavailable`, as
327
+ `check` already did. A Ladybug file without Toolgraph's schema gives
328
+ `ERROR: backend schema is not initialized or incomplete — run 'toolgraph
329
+ init-schema'`, and MCP returns `backend_misconfigured` with cause
330
+ `schema_not_initialized` (`schema_version_mismatch` for another schema
331
+ version). A read that opened the database and fails this way closes it,
332
+ so `init-schema` from another process can repair it while `serve` runs
333
+ (a process that already wrote keeps its handle).
334
+ Neo4j is unchanged: an uninitialized Neo4j database still answers reads
335
+ with empty results.
336
+
337
+ - `toolgraph check` and `doctor`'s Connectivity check now select
338
+ `NEO4J_DATABASE`. With a database the Neo4j server does not have, `check`
339
+ printed `OK` and the next graph command exited 1 with a
340
+ `DatabaseNotFound` traceback; now `check` and every graph command exit 1
341
+ with `ERROR: Neo4j database named by NEO4J_DATABASE does not exist on the
342
+ server`, and `doctor` reports that line as a Connectivity FAIL
343
+ instead of a Connectivity PASS followed by two raw driver errors.
344
+ Connectivity is checked with a read query in the configured database,
345
+ in READ access mode like the driver's own check (measured on Neo4j 5.7
346
+ and 5.26).
347
+
348
+ - README's exit-code section no longer gives a rejected Neo4j password as an
349
+ error that ends in a Python traceback; since the authentication change
350
+ above it prints one `ERROR:` line. The exit-code table lists rejected
351
+ credentials, a missing Neo4j database and a server older than 5.7 among
352
+ the one-line errors, with the `init` and `serve` exceptions spelled out
353
+ for every class.
354
+
355
+ - Neo4j no longer floods stderr with driver notifications. Every query that
356
+ named a label, property or relationship type the graph did not hold yet
357
+ logged a `Received notification from DBMS server` line, so a fresh
358
+ database printed eight before `doctor`'s diagnosis and a README walkthrough
359
+ printed 92. The driver now disables the `UNRECOGNIZED` classification.
360
+ Warning-level notifications, such as deprecations, still reach stderr;
361
+ performance notifications are still logged at INFO. Requires Neo4j 5.7
362
+ (see Upgrading).
363
+
364
+ - `.env.example` no longer presents `TOOLGRAPH_SERVERS=servers.yaml` and
365
+ `TOOLGRAPH_GOVERNANCE=governance.yaml` as configured paths. Those values
366
+ are the built-in defaults, and the repository root has neither file, so
367
+ after `cp .env.example .env` a bare `toolgraph crawl` still exits 1 with
368
+ `Invalid servers config at servers.yaml` (`ingest-manifest`: `Invalid
369
+ governance config at governance.yaml`). The lines are now commented out
370
+ with a note to pass `--servers examples/servers.yaml` explicitly. They are
371
+ not pointed at `examples/`: a bare `crawl` would then reconcile the graph
372
+ against the example fleet and retire every other server.
373
+
374
+ - A Neo4j that rejects the credentials no longer makes CLI commands print a
375
+ native traceback. Wrong, expired or rate-limited credentials and an
376
+ expired auth-provider token are typed at the driver seam as a
377
+ configuration error and reported as one line, `ERROR: Neo4j refused
378
+ authentication (<Neo4j status code>); check NEO4J_USER, NEO4J_PASSWORD
379
+ and the account's auth provider`, exit 1. The driver's own message is not
380
+ echoed. It is not treated as a retryable outage, and `doctor`'s schema
381
+ check keeps that message instead of rewording it as a catalog permission
382
+ problem. Query-level `Forbidden` errors are unchanged.
383
+
384
+ - `crawl_all` and `crawl_all_async` now reject a `concurrency` that is not a
385
+ positive integer (`0`, negatives, `bool`, floats, strings, `None`) with a
386
+ `ValueError` before any server is contacted. Previously `concurrency=0` made
387
+ every crawl of a non-empty fleet wait forever, because the per-server
388
+ timeout only starts once the semaphore is held. The CLI always used the
389
+ default of 5 and is unaffected.
390
+
391
+ ## 0.2.0 - 2026-10-15
392
+
393
+ Third alpha. Read **Upgrading from 0.1.0** below before installing: it lists
394
+ seven changes an existing setup, checkout or embedding program can notice.
395
+ The one most installations meet first is that commands now refuse a backend
396
+ variable that disagrees with the config file.
397
+
398
+ ### Upgrading from 0.1.0
399
+
400
+ - Commands stop with `ERROR: conflicting backend configuration (...)` when
401
+ `TOOLGRAPH_BACKEND` or `TOOLGRAPH_DB_PATH` disagrees with the config file;
402
+ 0.1.0 silently let the variable win. Pass `toolgraph --config <file>
403
+ <command>`, or unset the variable; when it comes from `.env`, remove it
404
+ there, since the next command loads `.env` again. See the first bullet
405
+ under Changed.
406
+ - `control-preflight` can now return `advisory_warn` for a plan that 0.1.0
407
+ answered with `advisory_allow`. See the `control-preflight` bullet under
408
+ Changed before relying on `advisory_allow` as a gate.
409
+ - A program that imports Toolgraph no longer gets `./.env` loaded for it.
410
+ - `toolgraph check` no longer creates a missing Ladybug database; it exits 1.
411
+ A script that used `check` to initialize should run `toolgraph init` or
412
+ `init-schema` first. See the `check` bullet under Changed.
413
+ - Identities, control-plan strings and review notes that 0.1.0 accepted can
414
+ now be refused as credential-shaped (for example `?access_token=` or `?credential=`). See
415
+ the credential bullet under Changed.
416
+ - `GraphState.token()` and `queries.with_generation()` are gone; use the
417
+ `graph_state` token results carry and `queries.with_graph_state()`. See
418
+ Removed.
419
+ - The repository's `examples/policy_propose_from_observed.py`, which 0.1.0
420
+ introduced, is gone; use `toolgraph policy propose`, which reads the live
421
+ graph. See its entry under Removed for the provenance flags.
422
+
423
+ ### Added
424
+
425
+ - **`toolgraph doctor [--json]`** checks the configuration file (present,
426
+ valid JSON, mode 0600, and `backend_source`: which of `--config`, the
427
+ environment, the config file or the built-in default decided the backend),
428
+ backend connectivity, the physical schema and the graph state. It opens the
429
+ database read-only — on Neo4j in READ access mode — and redacts what it
430
+ reports. It exits 1 when any check fails; a warning alone exits 0.
431
+ - **`toolgraph ingest-manifest --dry-run [--json]`** reports the full delta a
432
+ manifest would apply without changing the database: agents, policies,
433
+ grants and `granted_by`, data access, `GOVERNED_BY`, expected exceptions,
434
+ provenance, and the resources and tools that would be pruned for having no
435
+ edges left. It exits 1 when the manifest would be rejected; with `--json` it
436
+ then prints a rejection report with the warnings, not a delta (the delta
437
+ lists stay empty because no comparison was made). A missing database file is refused and none is
438
+ created; an existing but uninitialized one is refused with `init-schema`
439
+ guidance. On Neo4j the preview
440
+ session opens in READ access mode, so the server rejects an auto-commit write
441
+ issued through it (verified on Neo4j 5.26 over `bolt://`); that is an access
442
+ mode, not access control, and does not replace a read-only role. `--json`
443
+ requires `--dry-run`. Plain `ingest-manifest` behaves as before.
444
+ - **`toolgraph policy propose GOVERNANCE OBSERVED`** proposes grant changes to
445
+ `governance.yaml` from an observation-window JSONL export
446
+ (`contracts/observation-window.schema.json`), resolving grants against the
447
+ live graph. It never writes to the graph and never edits the manifest. The
448
+ export's envelope must match the manifest and the live graph;
449
+ `--revalidate-current` accepts a moved generation and re-checks additions
450
+ against the current strict catalog, and `--unverified-provenance` accepts a
451
+ missing envelope or identity and labels the result unverified. Other
452
+ options: `--min-would-block` (default 3), `--window-label`, `--json`, and
453
+ `-o/--output`, which cannot be combined with `--json` and writes the file
454
+ atomically with mode 0600 (a failure before the replacement leaves an
455
+ existing file as it was). It refuses, with a redacted `ERROR:` diagnostic
456
+ and exit 1 rather than a traceback: an export over 50,000,000 bytes,
457
+ malformed YAML, and any proposal row it would emit whose agent, tool or note
458
+ is credential-shaped — that error names the position (`additions[0].tool`),
459
+ never the value. See
460
+ `docs/policy-propose.md`.
461
+ - **`control-preflight` reports potential exposures.** Each record in the new
462
+ `potential_exposures` field joins an eligible reader tool on one plan node to
463
+ an eligible writer tool on a distinct node downstream of it, with the
464
+ resources involved (redacted) and a shortest witness path. It marks a
465
+ potential path, not proof that data moved. The search is bounded: at most 100 records,
466
+ 64,000 bytes of records and 65,536 traversal steps; when it stops early the
467
+ result also carries `truncated: true` and `total_candidate_pairs_found`,
468
+ which is then a lower bound. The fields are additive to the v1 schema. See
469
+ `docs/adr/0013-bounded-control-plan-preflight.md`.
470
+ - `toolgraph audit-report` JSON now carries the graph state it was read at
471
+ (`graph_generation`, `graph_instance_id`, `graph_state`,
472
+ `graph_state_verified`), as the MCP `audit_report` tool already did. The
473
+ Markdown format is unchanged.
474
+ - `toolgraph example init` now writes a `README.md` into the generated
475
+ directory. `docs/` is excluded from the sdist, so a user who installed from
476
+ PyPI previously landed in a directory of three unexplained files with only
477
+ the CLI's one-line hint to go on. The file lists the commands in order,
478
+ states the expected outcome, and links the full guide.
479
+ - `examples/README.md` maps each `servers-*.yaml` / `governance-*.yaml` input
480
+ to the script or workflow that consumes it, and to what it needs to run.
481
+ - A `Documentation` URL in the project metadata, so the beginner guide is
482
+ reachable from the PyPI sidebar.
483
+
484
+ ### Changed
485
+
486
+ - **An environment variable can no longer silently override `toolgraph
487
+ init`'s config.** Without `--config`, `TOOLGRAPH_BACKEND` and
488
+ `TOOLGRAPH_DB_PATH` used to win over `.toolgraph/config.json` (or the file
489
+ `TOOLGRAPH_CONFIG` names). Copying `.env.example` (which sets
490
+ `TOOLGRAPH_BACKEND=neo4j`) into a directory where `toolgraph init` had
491
+ written a Ladybug config sent every command to Neo4j with no warning. When
492
+ the two disagree on the backend or on the resolved database path, commands
493
+ now stop with `ERROR: conflicting backend configuration (...)`. To migrate,
494
+ pass `--config <file>` to choose the file, or unset the variable (remove it
495
+ from `.env` if it is set there). Agreeing
496
+ values, or only one of the two being set, behave as before. `--help`,
497
+ `toolgraph init` and `toolgraph example init` are not blocked, so `init` can
498
+ still be used to rewrite the file. `toolgraph doctor` reports which input
499
+ decides the backend as `backend_source`.
500
+ - **Only the CLI reads `./.env`.** Importing `toolgraph` as a library used to
501
+ load `.env` from the current directory into `os.environ`. The CLI, including
502
+ `toolgraph serve`, still does; a program that embeds Toolgraph and relied on
503
+ that should call `toolgraph.config.load_dotenv_file()` or set its own
504
+ environment.
505
+ - **`toolgraph check` no longer creates a database.** It opens the configured
506
+ Ladybug file read-only, so a mistyped `db_path` exits 1 with
507
+ `BACKEND_UNAVAILABLE ... does not exist` instead of creating an empty
508
+ database and printing `OK`. Run `toolgraph init` or `init-schema` first.
509
+ - **`control-preflight` can now warn where 0.1.0 allowed.** The decision is
510
+ `advisory_warn`, not `advisory_allow`, when the plan has a potential
511
+ exposure (see Added) or the exposure search was truncated. The exit code is
512
+ still 0 for both. A consumer that gates on `advisory_allow` will see more
513
+ warnings and should review them rather than treat them as failures of the
514
+ tool. A result without the exposure fields, from 0.1.0, means "not
515
+ checked", not "none found"; `total_candidate_pairs_found` on a truncated
516
+ result is a lower bound. ADR-0013 states the decision rules.
517
+ - **Credential checks are shared and refuse more.** Agent and tool identities
518
+ (preflight, `policy compile`, `policy review-plan`, `policy propose`) and
519
+ strings in control plans are now also refused when they carry
520
+ `?access_token=` or `?credential=`, a password URI whose username is an
521
+ email address, or an `@` in a query after a port
522
+ (`https://user@host:443?x=a@b`). `review-candidates annotate` notes that
523
+ assign `secret=` or `access_token=` are now refused too. Error text
524
+ now also hides `credential=...` and a bare `Bearer <token>`. Nothing that
525
+ was refused or hidden before is accepted or shown now.
526
+ - Control-plan validation errors give the location and the reason, never the
527
+ rejected value: `invalid control plan at nodes.0.principals.1: ...`, and a
528
+ bad candidate tool is reported by its index.
529
+ - A file that is not a Ladybug database is now a non-retryable
530
+ `BACKEND_CORRUPT` configuration error, printed as one `ERROR:` line. In
531
+ 0.1.0 it surfaced as a Python traceback from Ladybug.
532
+ - `config.configure()` closes the cached backend connection, so a library
533
+ caller switching configs no longer keeps talking to the previous database.
534
+ - Deny-path rows (`check-access` evidence, `unsafe-tools`, `audit-report`,
535
+ selector deny paths) now have a fixed order when rows used to tie: one
536
+ tool under several DENY policies, one tool name on several servers, or a
537
+ READS and a WRITES edge to the same resource. Ties are broken by tool key,
538
+ policy and mode; before, the backend chose, and Ladybug and Neo4j could
539
+ disagree. Rows that did not tie keep their order.
540
+
541
+ ### Removed
542
+
543
+ - **`examples/policy_propose_from_observed.py`, the offline proposal script.**
544
+ There is no longer a graph-free way to produce a proposal. Its replacement,
545
+ `toolgraph policy propose` (see Added), applies the same row and proposal
546
+ rules but resolves grants against the live graph, and checks the envelope
547
+ against it too: an envelope the script accepted is refused when it lacks a
548
+ graph instance id, or when its generation, digest or instance differs from
549
+ the live graph. Re-export from the current graph, or pass
550
+ `--revalidate-current` (a moved generation) or `--unverified-provenance`
551
+ (missing identity) to proceed. The script was repository-only and never
552
+ shipped in the distribution. Its tests now drive `toolgraph.policy_propose`.
553
+ - `GraphState.token()`, which nothing called and which returned a different
554
+ set of keys than the `graph_state` token that results actually carry.
555
+ - `toolgraph.graph.queries.with_generation()`, which had no production
556
+ caller; use `with_graph_state()`. A strict read that sees the graph state
557
+ change on every retry now raises `queries.GraphStateChurnError`, a
558
+ `RuntimeError` subclass with the same message, so CLI output and exit codes
559
+ are unchanged.
560
+
561
+ ### Fixed
562
+
563
+ - Backend configuration errors print one `ERROR:` line and exit 1 instead of
564
+ a Python traceback: an unknown `TOOLGRAPH_BACKEND`, a missing `ladybug`
565
+ extra, and a schema-version mismatch. `toolgraph doctor` is the exception
566
+ by design: it reports the same failure as a FAIL check (exit 1) in its text
567
+ or JSON report.
568
+ - An outage or other backend error while reading the schema version is no
569
+ longer taken for a fresh database, so `init`, `init-schema`, `crawl` and
570
+ `ingest-manifest` stop before running any DDL.
571
+ - `toolgraph check` no longer prints credentials embedded in `NEO4J_URI`, and
572
+ `ingest-manifest` warnings and notices are redacted.
573
+ - `toolgraph init` with an unusable `--state-dir` prints `ERROR: cannot
574
+ initialize ...` and exits 1 instead of a traceback raised from its cleanup;
575
+ a partial database file it cannot remove is named in a warning.
576
+ - `policy review-plan` refuses a graph with no instance id instead of writing
577
+ a plan its own schema rejects.
578
+ - `policy compile` (and `build_policy_bundle()`) refuses a graph state whose
579
+ generation is not a non-negative integer — a boolean, float, string, null
580
+ or negative value — instead of writing it into the bundle. Toolgraph's own
581
+ graph always yields one; the check matters for a hand-edited graph or a
582
+ library caller's own graph reader.
583
+ - Writing a private artifact — `init`'s config file, `preflight --out`,
584
+ `control-preflight --out`, `policy compile --output`, `policy review-plan
585
+ --output` — no longer reports failure when the new file was already in
586
+ place and only re-applying its 0600 mode afterwards failed. The file is
587
+ created 0600 before it is written, so that second step was redundant.
588
+ - `crawl` no longer prints `vNone` for a server that reports no version.
589
+ - The packaged quickstart governance manifest now carries `provenance` on
590
+ every authored edge. The beginner guide claimed every authored edge had an
591
+ evidence pointer and then told the reader to run `unbacked-edges`, which
592
+ listed all three of them. The audit now comes back empty, and both the
593
+ manifest and the guide say what that does and does not prove: the demo tools
594
+ return strings and touch no file, so the READS/WRITES effects are synthetic
595
+ and their evidence cites the fixture that defines them. An empty audit means
596
+ every edge has a pointer, not that a claim is true. As a result `read_note`'s
597
+ risk in the quickstart drops from 0.4 to 0.0 and the compiled bundle's
598
+ digest changes.
599
+ - Beginner guide corrections (both languages): the shared-backend section
600
+ needs a repository clone, since `docker-compose.yml` and `.env.example` ship
601
+ with neither the package nor the generated quickstart; `policy compile`
602
+ prints metadata describing the bundle, not the bundle; and the gateway step
603
+ is labelled optional, matching the README's statement that memtomem-stm is
604
+ not a required dependency.
605
+
606
+ ### Dependencies
607
+
608
+ - The `pyjwt` security floor rises from `>=2.13.0` to `>=2.15.0`. 2.13.0
609
+ carries 14 advisories published after it was set: PYSEC-2026-4140 through
610
+ PYSEC-2026-4152 and PYSEC-2026-4183. PYSEC-2026-4141 and PYSEC-2026-4183
611
+ are fixed in 2.15.0, PYSEC-2026-4146 lists 2.13.0 as its last affected
612
+ version, and the rest are fixed in 2.14.0. The floor is what keeps an
613
+ install from PyPI from resolving an older version; the lock already resolves
614
+ a newer one. Toolgraph does not import `pyjwt` itself; it is pinned for the
615
+ MCP SDK's dependency tree.
616
+
617
+ ### Testing
618
+
619
+ - The `sse` crawl transport and auth-header forwarding on both HTTP
620
+ transports are now exercised end to end, at the lockfile version and at the
621
+ declared MCP SDK floor. Neither had been run before; no runtime behaviour
622
+ changed.
623
+
624
+ ## 0.1.0 - 2026-09-10
625
+
626
+ Second alpha. Upgrading from `0.0.1` takes three things: the MCP SDK 2.x, one
627
+ new field for anything caching MCP responses, and — if you generated a
628
+ quickstart with `0.0.1` — a regenerated server script. Read **Upgrading from
629
+ 0.0.1** below before installing.
630
+
631
+ ### Upgrading from 0.0.1
632
+
633
+ `toolgraph example init` copies a quickstart out of the package, so a
634
+ quickstart generated by `0.0.1` holds its own copy of the demo MCP server —
635
+ one that imports `mcp.server.fastmcp`. Upgrading that environment installs MCP
636
+ 2.x and the copied server stops importing, so the next `toolgraph crawl`
637
+ fails. Updating the package does not touch a directory it already wrote, and
638
+ `example init` refuses an existing destination on purpose.
639
+
640
+ To migrate one, generate a fresh quickstart beside it and move your own work
641
+ across:
642
+
643
+ ```bash
644
+ toolgraph example init quickstart-0.1.0
645
+ # Keep your authored governance and your database; take the new server script.
646
+ cp quickstart/governance.yaml quickstart-0.1.0/governance.yaml
647
+ cp -R quickstart/.toolgraph quickstart-0.1.0/.toolgraph
648
+ ```
649
+
650
+ `servers.yaml` records the interpreter that generated it, so regenerating from
651
+ the upgraded environment already points at the right one; check it only if you
652
+ generated from somewhere else. Re-run `toolgraph crawl`, then
653
+ `toolgraph ingest-manifest`, to confirm.
654
+
655
+ Verified end to end against the published `0.0.1`: a quickstart it generated
656
+ crawls cleanly on `0.0.1`, fails on the next crawl once the environment holds
657
+ `0.1.0`, and returns the same eligibility verdicts after these steps.
658
+
659
+ ### Added
660
+
661
+ - `examples/policy_propose_from_observed.py` reads a window of observed gateway
662
+ usage and prints a reviewable `governance.yaml` grant proposal. It never
663
+ writes governance. Repository-only — it is not part of the distribution — and
664
+ deliberately narrow: it requires an envelope declaring the governance digest
665
+ it was recorded against and refuses a mismatch, refuses an export with no
666
+ envelope unless `--unverified-provenance` is given, proposes a removal only
667
+ from an explicit zero-call row, and accepts only fully qualified timestamps
668
+ with an explicit UTC offset. Digest equality shows what the export declared,
669
+ not where its rows came from; issue #89 tracks what a real binding needs.
670
+
671
+ ### Changed
672
+
673
+ - **Breaking:** requires the MCP SDK 2.x (`mcp>=2.1.1,<3`). Environments pinned
674
+ to `mcp` 1.x can no longer install this package, because 2.x removed
675
+ `mcp.server.fastmcp` and renamed every model field from camelCase to
676
+ snake_case. The MCP *wire* format is unchanged, so a client that only speaks
677
+ the protocol needs no change; that is not the same as no change for anything
678
+ written against the Python SDK, caching responses, or reading exception text.
679
+ - An unexpected exception raised by a toolgraph tool body no longer puts its
680
+ message on the wire. Only errors raised deliberately as `ContractError` (for
681
+ example an unknown selector profile) are forwarded; the rest keep the SDK's
682
+ generic text, so an internal failure cannot disclose filesystem paths, query
683
+ bodies or credential-shaped values. Argument-validation failures still echo
684
+ the caller's own input, as the MCP SDK intends.
685
+ - **Breaking:** an MCP response whose read could not be bracketed by a stable
686
+ graph state now carries `graph_generation: null` and
687
+ `graph_state_verified: false` instead of the generation read afterwards.
688
+ Labelling such a result with a generation it was never read at let a verdict
689
+ from before a permission change be cached under the generation that changed
690
+ it, while the tool documentation tells callers to cache per generation.
691
+ Successful reads gain `graph_state_verified: true`, so the absence of a
692
+ guarantee is never inferred from a missing field. Null is not self-enforcing
693
+ — `None` is a usable cache key — so the obligation to skip the cache and to
694
+ attribute nothing to a graph state is the consumer's; ADR-0004 and the README
695
+ state it, and a consumer that turns eligibility into an authorization
696
+ decision should reject or retry such a response.
697
+
698
+ ## 0.0.1 - 2026-09-09
699
+
700
+ Initial public alpha release.
701
+
702
+ - Crawl MCP servers into a graph and combine factual capability data with
703
+ operator-authored governance.
704
+ - Explain access, reachability, drift, blast radius, and tool selection.
705
+ - Compile deterministic policy bundles for an independent runtime gateway.
706
+ - Support local Ladybug and shared Neo4j backends.
707
+ - Reject unknown manifest fields and duplicate YAML keys before graph mutation.
708
+ - Persist only credential-free endpoint labels and redact failure diagnostics.
709
+ - Ship an install-only, offline quickstart through `toolgraph example init`.
710
+ - Give MCP consumers a typed `backend_unavailable` error envelope for transient
711
+ graph outages while keeping contract and internal errors fail-loud. Outages
712
+ are typed at the backend seam, and `retryable` follows the tool's advertised
713
+ read-only annotation instead of being assumed, failing closed to `false`
714
+ whenever that annotation cannot be read.
715
+ - Add a body-free `toolgraph.control-plan` v1 contract and advisory
716
+ `control-preflight` command that lints bounded DAG topology and evaluates all
717
+ principal/tool surfaces under one collision-safe graph-state bracket.
718
+
719
+ ### Stabilization (2026-09-08)
720
+
721
+ - Resolve ingest references in bounded batches shared with the selector,
722
+ preserving atomic application, diagnostic order and ambiguous identities.
723
+ - Require explicit commit SHAs for ecosystem smokes and clean matching HEADs
724
+ for gateway smokes; record dependencies and artifact evidence.
725
+ - Include Ladybug in `make install` and refresh the pip audit dependency lock.
726
+ - Reconcile historical integration and selection roadmaps with shipped code.
727
+
728
+ ### Implementation review fixes (2026-09-07)
729
+
730
+ - Handle empty crawl and manifest collections on both backends while retaining
731
+ stale-edge reconciliation, revocation, and transactional generation updates.
732
+ - Preserve opaque artifact identities, including ordinary URI queries/fragments.
733
+ Reject credential-shaped identities and redact only evidence fields, including
734
+ policy review paths. Existing v1 schemas and decision vocabularies are unchanged.
735
+ - Use collision-safe `v2:` exception keys and deterministic last-entry-wins
736
+ metadata for duplicate logical exceptions. No physical schema migration is
737
+ required: existing keys remain readable. Re-ingest the original governance
738
+ manifest to rebuild keys and restore entries lost to old key collisions, then
739
+ recompile policy bundles. Old artifacts cannot reconstruct lost manifest rows.
740
+ - Require control-plan fields exactly as published: missing `maximum_cycles`,
741
+ explicit null bounds, string/boolean coercions are rejected. JSON integer
742
+ numbers such as `2.0` remain accepted; additive same-major fields remain valid.
743
+ - Compile catalogs in chunks of at most 4,096 candidates under one strict state
744
+ bracket; reject incomplete or conflicting evaluations. Injected readers own
745
+ all state reads, and adapters without the optional `evaluate` capability work.
746
+ - Load runtime settings lazily so help/version and explicit configuration remain
747
+ usable when the default config is broken. Invalid selected configs fail clearly.
748
+ - Reject naive and sub-minute timestamps before preflight/review I/O; development
749
+ validation now installs and checks the RFC 3339 format validator.
750
+
751
+ ### Upgrade note for pre-release users (historical)
752
+
753
+ `driver.session()` and `driver.verify_connectivity()` now raise
754
+ `BackendUnavailableError` where they previously surfaced the raw `neo4j`
755
+ `ServiceUnavailable`, `SessionExpired`, `ConnectionAcquisitionTimeoutError`, or
756
+ `DatabaseUnavailable`. Code embedding toolgraph as a library and catching those
757
+ types directly must catch `BackendUnavailableError` (or its `BackendLockedError`
758
+ subclass); the original exception is preserved as `__cause__`. Configuration,
759
+ authentication, and Cypher/client errors are unaffected, as is `get_driver()`.
760
+ Successful CLI output and query result shapes are unchanged; an unhandled
761
+ outage now names `BackendUnavailableError` in the traceback.
762
+
763
+ `driver.is_backend_unavailable()` narrowed to match: it now recognizes only
764
+ `BackendUnavailableError` (anywhere in the cause chain), not the raw `neo4j`
765
+ types it previously classified. Code that pairs the still-raw `get_driver()`
766
+ escape hatch with `is_backend_unavailable()` to decide whether to retry will
767
+ silently stop matching — catch `BackendUnavailableError`, or route the call
768
+ through `driver.session()` so the seam types it.
769
+
770
+ Earlier snapshots could persist stdio arguments or complete HTTP URLs in graph
771
+ endpoint/evidence properties. Reset and rebuild pre-release graphs after
772
+ upgrading. If any argument or URL contained a credential, rotate it as well.