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