toolgraph 0.3.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {toolgraph-0.3.0 → toolgraph-0.4.1}/CHANGELOG.md +433 -0
  2. {toolgraph-0.3.0 → toolgraph-0.4.1}/PKG-INFO +23 -7
  3. {toolgraph-0.3.0 → toolgraph-0.4.1}/README.md +22 -6
  4. {toolgraph-0.3.0 → toolgraph-0.4.1}/pyproject.toml +1 -1
  5. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/__init__.py +1 -1
  6. toolgraph-0.4.1/toolgraph/artifact_safety.py +266 -0
  7. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/artifacts.py +6 -3
  8. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/cli.py +136 -27
  9. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/config.py +23 -10
  10. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/control_plan.py +63 -6
  11. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/crawler/crawl.py +3 -2
  12. toolgraph-0.4.1/toolgraph/error_text.py +259 -0
  13. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/errors.py +15 -0
  14. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/driver.py +8 -5
  15. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/loader.py +2 -1
  16. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/queries.py +10 -2
  17. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/health.py +6 -5
  18. toolgraph-0.4.1/toolgraph/log_safety.py +183 -0
  19. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/manifest/ingest.py +11 -4
  20. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/models.py +86 -5
  21. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/policy_impact.py +2 -2
  22. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/policy_propose.py +36 -13
  23. toolgraph-0.4.1/toolgraph/redaction.py +528 -0
  24. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/review_candidates.py +35 -22
  25. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/server/app.py +32 -2
  26. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/yaml_loader.py +43 -8
  27. toolgraph-0.3.0/toolgraph/artifact_safety.py +0 -171
  28. toolgraph-0.3.0/toolgraph/redaction.py +0 -252
  29. {toolgraph-0.3.0 → toolgraph-0.4.1}/.gitignore +0 -0
  30. {toolgraph-0.3.0 → toolgraph-0.4.1}/LICENSE +0 -0
  31. {toolgraph-0.3.0 → toolgraph-0.4.1}/SECURITY.md +0 -0
  32. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/control-plan.schema.json +0 -0
  33. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/control-preflight.schema.json +0 -0
  34. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/observation-window.schema.json +0 -0
  35. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/policy-bundle.schema.json +0 -0
  36. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/policy-impact.schema.json +0 -0
  37. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/policy-review-plan.schema.json +0 -0
  38. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/preflight.schema.json +0 -0
  39. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/review-annotations.schema.json +0 -0
  40. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/tracegraph/README.md +0 -0
  41. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/tracegraph/review-candidates.schema.json +0 -0
  42. {toolgraph-0.3.0 → toolgraph-0.4.1}/contracts/tracegraph/v1.json +0 -0
  43. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/crawler/__init__.py +0 -0
  44. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/crawler/client.py +0 -0
  45. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/crawler/transports.py +0 -0
  46. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/__init__.py +0 -0
  47. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/schema.py +0 -0
  48. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/selector.py +0 -0
  49. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/graph/store.py +0 -0
  50. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/manifest/__init__.py +0 -0
  51. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/manifest/parser.py +0 -0
  52. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/policy_bundle.py +0 -0
  53. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/policy_impact_models.py +0 -0
  54. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/policy_review.py +0 -0
  55. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/preflight.py +0 -0
  56. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/quickstart/README.md +0 -0
  57. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/quickstart/governance.yaml +0 -0
  58. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/quickstart/policy_gateway_server.py +0 -0
  59. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/quickstart/servers.yaml +0 -0
  60. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/server/__init__.py +0 -0
  61. {toolgraph-0.3.0 → toolgraph-0.4.1}/toolgraph/uris.py +0 -0
@@ -2,6 +2,439 @@
2
2
 
3
3
  All notable changes to Toolgraph are documented here.
4
4
 
5
+ ## 0.4.1 - 2026-11-09
6
+
7
+ Sixth alpha, a patch release: diagnostics only. Nothing that 0.4.0 accepted
8
+ is refused, and no command, flag, default, function signature or JSON key
9
+ changes. Read **Upgrading from 0.4.0** below if a script or tool matches
10
+ Toolgraph's error text, or reads the configuration path it reports: more of
11
+ it is hidden or withheld whole.
12
+
13
+ ### Upgrading from 0.4.0
14
+
15
+ - Diagnostics hide more, and more of them are withheld whole (#241, #245,
16
+ #262). An `Authorization` value is hidden to the end of the message
17
+ (`Authorization: Bearer abc; retrying in 5s` now reads
18
+ `Authorization=<redacted>`). A message whose URL ends in a credential name
19
+ that assigns a value after it is withheld, in each form the scrubber reads
20
+ as an assignment (`GET https://h/authorization=Basic returned 404`). A
21
+ credential-shaped path in a YAML, `policy propose` export, `doctor` or
22
+ Ladybug backend error reads `<path withheld: credential-shaped>`, and so
23
+ does the configuration `path` in `doctor --json`, where 0.4.0 showed a
24
+ partly scrubbed path or withheld the line.
25
+ `toolgraph.config.check_config_health()` returns that display value in
26
+ `path` and `detail` too, where 0.4.0 returned the path as given. Tooling
27
+ that matches stderr, MCP error text or `doctor` output should expect these
28
+ forms; the entries under Fixed give each one.
29
+
30
+ ### Changed
31
+
32
+ - Scrubbing a diagnostic is faster (#263), with the same output as before
33
+ that change: a check that needs a `%`, a backslash, a quote or `://` is
34
+ skipped when the message holds none, and each repr literal is parsed once
35
+ instead of twice. Against 0.4.0, measured locally (CPU time, best of three),
36
+ 19 of the 21 adversarial inputs in `tests/test_redaction.py` run at least
37
+ 1.4 times faster and a short plain message about 4.5 times; the two
38
+ percent-encoded URL inputs run about 12% slower, the cost of the decoded
39
+ check #241 added.
40
+ - Distributions are built with `hatchling` 1.33.0 (was 1.32.4); the sdist
41
+ pins it in `[build-system] requires`.
42
+
43
+ ### Fixed
44
+
45
+ - A credential name at the end of a URL is caught in the forms the scrubber
46
+ reads as an assignment, not only those the refusal rule does (#241). 0.4.0
47
+ withheld `fetch http://h/password =secret` but printed
48
+ `http://h/authorization=Basic secret` as `http://h secret`, and likewise
49
+ `Authorization=Token`, `proxy_authorization=Basic`, `password :secret`,
50
+ `password:[a, secret]` and `credential:{a: secret}`. Such a message is now
51
+ withheld whole, and so is one where the name or its delimiter is
52
+ percent-encoded (`http://h/pass%77ord :secret`,
53
+ `http://h/password%3A[a, secret]`), judged against each URL where the
54
+ rewrite finds it. More diagnostics are withheld for the same reason: a URL
55
+ path ending in a credential name and a scheme word with text after it
56
+ (`GET https://h/authorization=Basic returned 404`), or a bracket opening
57
+ inside the URL after a `:` (`GET https://h/password:[id] returned 404`),
58
+ as one after `=` already was. Not caught: an encoded delimiter that joins
59
+ the text after a URL to the start of another one
60
+ (`http://h/password %3Ahunter2://host`); an encoded name before a URL's
61
+ scheme (`%70assword://hunter2`); and a repr-escaped URL followed by an
62
+ assignment (`'http://h/password\t':hunter2`) (#246).
63
+ - Diagnostics hide everything after an `Authorization` name, not only the
64
+ first word of its value (#245). 0.4.0 knew only the `Bearer`, `Basic` and
65
+ `Token` schemes and took any other scheme word as the whole value, so
66
+ `Authorization: Digest username=alice, response=hunter2` printed
67
+ `Authorization=<redacted> username=alice, response=hunter2`; Negotiate,
68
+ AWS4-HMAC-SHA256 `Signature=`, OAuth, SCRAM and Hawk parameters showed the
69
+ same way, and so did a URL ending in the name
70
+ (`http://h/authorization=Digest username=alice,response=hunter2` printed
71
+ `http://h username=alice,response=hunter2`; it is now withheld). A name with
72
+ a prefix is scrubbed too: `proxy_authorization:hunter2` and
73
+ `HTTP_AUTHORIZATION:hunter2` were printed as given. Where such a value ends
74
+ cannot be told from the text, so the rest of the message goes with it:
75
+ `Authorization: Bearer abc; retrying in 5s` now prints
76
+ `Authorization=<redacted>`, and a URL holding an Authorization name with
77
+ text after it is withheld (`GET https://h/authorization=abc returned 404`).
78
+ - More error lines show a credential-shaped path as
79
+ `<path withheld: credential-shaped>` and keep the rest of the line (#262):
80
+ a YAML file over the size limit or without a mapping at its root, a
81
+ `policy propose` export over the size limit or with a bad envelope or row,
82
+ the manifest digest hint for an export without an envelope, `doctor`'s
83
+ configuration check, and the `BACKEND_CORRUPT`, `BACKEND_UNAVAILABLE` and
84
+ `BACKEND_LOCKED` errors for a Ladybug database. 0.4.0 scrubbed these paths
85
+ along with the line, so a directory named `password=x` printed a path that
86
+ does not exist (`expected a YAML mapping at document root:
87
+ /tmp/password=<redacted>`; an export error lost its `:1:` line number with
88
+ it), and a percent-encoded name (`/tmp/?%74oken=x`) withheld the whole
89
+ line, error code included.
90
+
91
+ ## 0.4.0 - 2026-10-21
92
+
93
+ Fifth alpha. Read **Upgrading from 0.3.0** below before installing: it lists
94
+ eight changes an existing setup, script or embedding program can notice.
95
+ Most setups need no change, because the new refusals reject only identities
96
+ and notes that hold credential-shaped data. What most often notices is
97
+ tooling that reads Toolgraph's text: evidence paths, error lines and log
98
+ output are shown differently, and more often withheld whole.
99
+
100
+ ### Upgrading from 0.3.0
101
+
102
+ - A governance manifest or `servers.yaml` holding a credential-shaped
103
+ identity is refused at input (#217): an agent, policy id, tool ref or
104
+ `granted_by`, a `governed_by` policy or tool-ref key, or a server's name.
105
+ The manifest is rejected whole, so `ingest-manifest`, `policy propose` and
106
+ `policy impact` exit 1, and so is a `servers.yaml`, before any server is
107
+ contacted. A crawled server that reports such a tool name, or such a name
108
+ when `servers.yaml` gives it none, fails its own crawl; the rest of the
109
+ fleet still loads. The error names the field, never the value. Rename the
110
+ identity. A graph keeps the values it stored earlier until they are
111
+ re-ingested or reset.
112
+ - Identities refuse any URI with an `@` after its `://`, raw or as `%40`
113
+ (#210, #215, #216), where 0.3.0 refused only a `user:password@` it could
114
+ bound: agent ids, tool keys, run ids, candidates and graph instance ids in
115
+ preflight, policy bundles, review plans, `policy impact` and `policy
116
+ propose`, the run ids and tool keys of a Tracegraph review report, and a
117
+ control plan's identities and keys. `policy propose` refuses to emit a
118
+ proposal whose note would list a live-graph tool key holding userinfo.
119
+ - Review notes, reviewer names and control plans refuse a URI with a quote
120
+ in its userinfo and an `@` after it (#210), such as
121
+ `neo4j://u:"p w"@host`, which 0.3.0 accepted. A quoted URL followed by an
122
+ address in the same text, `"https://h" mail a@b`, is refused too.
123
+ - Artifact evidence shows resource URIs differently (#212). A consumer that
124
+ matches evidence text should expect `https://<redacted-uri>` when an `@`
125
+ follows a URI's authority (`https://registry/@scope/pkg`), and
126
+ `<redacted-uri>` for a percent-encoded scheme. A resource that cannot be
127
+ parsed is now hidden instead of stopping the command, `export:///sink`
128
+ keeps its three slashes, and the paths of `check-access`, `unsafe-tools`,
129
+ `blast-radius`, `audit-report` and the matching MCP tools show resources
130
+ and policy ids redacted. The raw `resource` and `policy` row values are
131
+ unchanged. See the first bullets under Changed.
132
+ - Diagnostics read differently, and more of them are withheld whole (#206,
133
+ #221, #223, #229, #232, #236). Validation errors are one line with fixed
134
+ text and `<key>` for keys outside the schema; YAML errors read `invalid
135
+ YAML at line L, column C: <what failed>`; MCP tool-call errors for a bad
136
+ request no longer quote it; a CLI usage error is withheld whole when a
137
+ token on the command line, or the tokens joined by spaces, is
138
+ credential-shaped; and a printed path that is
139
+ credential-shaped reads `<path withheld: credential-shaped>`. Messages
140
+ holding an escaped credential, or a URL whose path ends in a credential
141
+ name with text after it, are withheld: `Error accessing
142
+ https://host/token: connection refused` now reads `details withheld (they
143
+ contain credential-shaped data)`. Tooling
144
+ that matches stderr or MCP error text should expect these forms; the
145
+ entries under Fixed give each one.
146
+ - Logging changes for programs that embed Toolgraph (#206). Importing
147
+ `toolgraph.server.app` into a process with no root log handler filters
148
+ the root handlers the MCP SDK then creates, and running the Typer app
149
+ in-process filters `logging.lastResort` for the rest of the process once a
150
+ command reaches the root callback (root `--help`, `--version` and an
151
+ unknown command do not). Every record through a
152
+ filtered handler, the program's own included, is rebuilt: its message is
153
+ scrubbed (`password=<redacted>`), each exception in its chain is shown as
154
+ `<Type>: <message>` without a traceback, and `extra` attributes and `stack_info` are dropped,
155
+ so a formatter that names an `extra` field fails with `Formatting field
156
+ not found in record`. Configure logging before importing
157
+ `toolgraph.server.app`, or replace the handlers it filtered. Handlers a
158
+ program configured itself are not touched.
159
+ - Library callers: `ingest_governance`, `dry_run_ingest` and
160
+ `load_crawl_result` re-validate the model they are given, and a model
161
+ built with `model_construct()` or changed after validation that no longer
162
+ validates raises `ValueError: <Model> does not validate; build it with
163
+ <Model>.model_validate to see why` before the graph is opened.
164
+ `build_policy_impact` already re-validated, and now raises that error
165
+ instead of pydantic's. A YAML failure that PyYAML does not mark with a line
166
+ and column raises `toolgraph.yaml_loader.YAMLContentError`, a `ValueError`
167
+ (#223); marked failures still raise PyYAML's `MarkedYAMLError`.
168
+ - A durability warning escalated with `-W error` ends a command with one
169
+ `ERROR:` line and exit 1, without a traceback or a JSON receipt (#129).
170
+ `init` keeps the database it created, where 0.3.0 deleted it and kept the
171
+ `config.json` naming it. The annotation warning's text now ends `do not
172
+ retry: a retry appends a second event`.
173
+
174
+ ### Changed
175
+
176
+ - Artifact evidence shows each resource URI as a whole value (#212). A
177
+ resource is redacted before it is written into an evidence path, and
178
+ resource fields (`resource`, `resource_uri`, `read_resource`,
179
+ `write_resource`, `policy`, a resource `ref` and the `resources_*` lists of
180
+ `ingest-manifest --dry-run --json`) are redacted as one value even when
181
+ they hold whitespace. In free text, such as an exception reason, a URI ends
182
+ at whitespace or at a `)`, as in 0.3.0. Consumers that match evidence can
183
+ notice:
184
+ - An `@` anywhere after a URI's authority, raw or as `%40`, hides
185
+ everything after the scheme, since where an unquoted password ends cannot
186
+ be told: `https://registry/@scope/pkg` is shown as
187
+ `https://<redacted-uri>`. In free text the rest of the text goes too, as
188
+ it already did after a quoted password, because it may hold the rest of
189
+ the password; this includes a URI-shaped tool name inside evidence text.
190
+ - A value whose scheme is percent-encoded (`https%3A%2F%2Fh/r`) is shown as
191
+ `<redacted-uri>`, in free text with the rest of the text.
192
+ - A resource value that cannot be parsed (`https://u:p@[::1/r`) is hidden
193
+ as `<scheme>://<redacted-uri>` instead of stopping the command, and so is
194
+ any evidence string that is nothing but such a URI, whitespace around it
195
+ aside. A URI that cannot be parsed among other text, such as
196
+ `see https://u:p@[::1/r` in an exception reason, still stops it, as in
197
+ 0.3.0.
198
+ - An evidence path no longer loses the rest of its line when its resource
199
+ holds a `)` or a quoted password: a resource `neo4j://u:p)ss@host/r` is
200
+ shown as `(t) -READS-> (neo4j://host/r) -GOVERNED_BY-> (p:DENY)`, and a
201
+ quoted one as `(t) -READS-> (neo4j://<redacted-uri>) -GOVERNED_BY->
202
+ (p:DENY)`. In free text, a `)` followed by an `@` hides everything after
203
+ the scheme.
204
+ - `export:///sink` keeps its three slashes; it was shown as `export:/sink`.
205
+ - Paths in `check-access`, `unsafe-tools`, `blast-radius` and `audit-report`
206
+ output, and in the matching MCP tools, show resources and policy ids the
207
+ same way. The rows still carry the raw `resource` and `policy` values
208
+ beside the path.
209
+ - `ingest-manifest` warnings that name a resource show it redacted.
210
+ - Identities are refused when they hold a URI with an `@` anywhere after its
211
+ `://`, raw or percent-encoded (#210): agent ids, tool keys, run ids,
212
+ candidates and graph instance ids in preflight, policy bundle, review plan,
213
+ policy impact and `policy propose` input and output, and the run ids and
214
+ tool keys of a Tracegraph review report. Before, only a `user:password@`
215
+ that the refusal rule could bound was refused, so `neo4j://u:p/w@host/r`
216
+ passed. An identity has no reason to hold userinfo, and an unquoted
217
+ password may hold `/`, `)` or whitespace. Review notes, reviewer names,
218
+ the notes of `policy propose` rows and control plans keep the rule for
219
+ prose. The live-graph tool keys an ambiguous grant's note lists are
220
+ checked as identities before they are joined into it, so `policy propose`
221
+ refuses to emit a proposal whose note would show one holding userinfo
222
+ (#216).
223
+
224
+ - The governance manifest and the crawler refuse a credential-shaped identity
225
+ at input, with the same identity rule (#217): an agent, policy id, tool ref
226
+ or `granted_by`, a `governed_by` policy or tool-ref key, a server's name
227
+ (its `servers.yaml` name, else the name it reports) and the tool names it
228
+ reports. Before, such a
229
+ value was stored and printed by `check-access`, `unsafe-tools`,
230
+ `blast-radius`, `unbacked-edges`, `audit-report` and `eligible-tools`. The
231
+ error names the field, never the value. A manifest is rejected whole
232
+ (`ingest-manifest`, `policy propose` and `policy impact` exit 1), and so
233
+ is a `servers.yaml`, before any server is contacted. A server without a
234
+ `servers.yaml` name that reports such a name, or any server that reports
235
+ such a tool name, fails its own crawl, and the rest of the fleet still
236
+ loads; a server's own reported name is never stored when `servers.yaml`
237
+ names it. Resource URIs are not identities and are not checked. A graph
238
+ keeps the values it stored earlier until they are re-ingested or reset.
239
+
240
+ - A control plan's identities get the identity rule at load (#215): `run_id`,
241
+ producer name and version, principal ids, node ids and the principals a
242
+ node lists, edge endpoints and candidate tools, so a credential URI with a
243
+ `/` in its password or a percent-encoded scheme is refused there instead of
244
+ being published as the result's `run_id`. Keys are field names and get the
245
+ same rule, so a duplicate or forbidden-field error can no longer print an
246
+ encoded credential-shaped key. String values of additive fields keep the
247
+ prose rule. The result's `graph_state` goes through the same artifact
248
+ check as the rest of the result.
249
+
250
+ ### Fixed
251
+
252
+ - Artifact evidence no longer shows URI passwords the evidence scan could not
253
+ bound (#212). Before, a password holding an unquoted `/`, `?` or `#`
254
+ (`neo4j://u:p/w@host/r`, also read as a port when it starts with digits),
255
+ a credential URI inside another URI's path, an encoded `@` (`%40`), and a
256
+ resource whose query holds `)` (`https://h/r?k=a)b` kept `)b`) could reach
257
+ preflight, policy bundle, control-plan and review artifacts, `policy
258
+ impact` and `policy check` output and `ingest-manifest --dry-run --json`,
259
+ including its warnings. Inside free text some shapes are still not
260
+ bounded, as in 0.3.0: a password holding whitespace without quotes, a
261
+ query or fragment holding `)` (`see https://h/r?k=a)b` still shows `)b`),
262
+ and text after a `)` that no longer reads as a URI.
263
+ - A URI with a quote in its userinfo and an `@` after it is refused like any
264
+ other credential URI, raw or percent-encoded (#210): in identities, review
265
+ notes, reviewer names and control plans. Before, a quoted password holding
266
+ whitespace (`neo4j://u:"p w"@host`), a quoted part after a `?` or `#`
267
+ (`neo4j://u:p?"w s"@host`, `neo4j://u?x:"p w"@host`) and a quoted user
268
+ part (`neo4j://"u v":"p w"@host`) were accepted. The refusal now matches
269
+ what error-message scrubbing already hides, so a quoted URL followed by an
270
+ address in the same text (`"https://h" mail a@b`, `"https://h:443" mail
271
+ a@b`) is refused too; `see "https://x/y" and @alice`, whose quote follows
272
+ a path, is not. Error messages hide the new shapes as well: before,
273
+ `neo4j://u:p?"w s"@h/a` kept `"w s"@h/a`. A message whose decoded or
274
+ scrubbed text still holds such a URI is withheld, as one holding
275
+ `u:%22pw%22@` already was; `see "https://x/y" and @alice` as an error
276
+ message is now withheld for that reason. Userinfo whose unquoted part
277
+ holds whitespace (`neo4j://u v:"p w"@host`) still passes in prose and in
278
+ error messages, where it reads as a URL followed by text; identities
279
+ refuse it.
280
+ - Pydantic validation errors no longer print mapping keys, unknown field
281
+ names or input values (#221). Before, an invalid `servers.yaml` or
282
+ governance manifest (`crawl`, `ingest-manifest`, `policy impact`,
283
+ `policy propose`), an invalid review-candidate sidecar, and a server whose
284
+ `tools/list` reply the MCP SDK rejects printed the error location with
285
+ every key on the way, and the SDK's errors also printed the input; only the
286
+ credential shapes the scrubber recognizes were hidden, so
287
+ `governed_by: {"postgres://u:prefix QXZQ@h/x": ...}` showed `QXZQ@h/x` and
288
+ an `env` key showed as written. These errors are now one line, such as
289
+ `1 validation error for ServersConfig: servers.0.env.<key>: expected a
290
+ string [type=string_type]`: field names, list indexes and union member
291
+ names that match the schema are kept, any other key is `<key>`, and the
292
+ message is fixed text chosen by error type. A field's own refusal, such as
293
+ `agent must not contain credential-shaped data`, is kept as written.
294
+ Where Toolgraph cannot tell which model failed (`policy propose`, a
295
+ rejected server reply) no location is printed.
296
+ - A config file that is not valid YAML is reported as `invalid YAML at line L,
297
+ column C: <what failed>`, such as `could not be parsed` or `duplicate key`
298
+ (#223). Before, the message quoted the source line, and names from the
299
+ document: a duplicate key, an alias, anchor or tag, a character. A value
300
+ PyYAML could not convert was reported with the value (`!!int x`,
301
+ `!!float x`, `!!bool x`), or stopped `policy impact` and `policy propose`
302
+ with a traceback (`!!bool x`, `!!timestamp x`, an empty `!!int`); now it is
303
+ `invalid YAML: a value could not be read`. Invalid UTF-8 and a control
304
+ character name only their position.
305
+ Library callers still get PyYAML's error when it marks a line and column
306
+ (a `MarkedYAMLError`); the other failures raise
307
+ `toolgraph.yaml_loader.YAMLContentError`, a `ValueError`.
308
+ - Log output hides credential-shaped values and no longer prints tracebacks
309
+ (#206). Before, a crawled server whose reply failed JSON-RPC validation
310
+ made `crawl` print the MCP SDK's traceback with the reply's values
311
+ (`input_value=...`), a tool that crashed under `serve` logged its
312
+ exception message and traceback, and the SDK logged an unknown tool name
313
+ as sent. Now the CLI's fallback output (Python's last-resort handler, used
314
+ when no handler is configured) and the handlers the server configures
315
+ when the process has none print each record's message through the
316
+ credential scrubber, and each exception in its chain as
317
+ `<Type>: <message>` without its traceback, scrubbed the same way, with a validation error's
318
+ message rebuilt as in #221. A value the message or an exception prints
319
+ with `repr` (`%r`, a `KeyError` key), which escapes a tab or a no-break
320
+ space out of the scrubber's sight, is checked as it was before formatting,
321
+ and a credential-shaped one withholds the line. Other text in a message,
322
+ such as a tool name that is not credential-shaped, is still shown. Handlers a program
323
+ configured itself, before or after importing Toolgraph, and uvicorn's own
324
+ loggers under `serve --http` (#226), are not filtered.
325
+ - MCP tool-call errors for a bad request no longer quote the request (#206).
326
+ Before, the MCP SDK answered an unknown tool with `Unknown tool: <name>` as
327
+ sent, and invalid arguments with pydantic's text, which carried every
328
+ input value (`input_value={'token': ...}`). The tool name now goes through
329
+ the credential scrubber (`Unknown tool: nope` is unchanged), and invalid
330
+ arguments read `Error executing tool <name>: 1 validation error for
331
+ <name>Arguments: agent: expected a string [type=string_type]`, with field
332
+ names and fixed messages as in #221. A tool that crashes keeps the SDK's
333
+ generic `Error executing tool <name>`. Errors for resources and prompts,
334
+ which Toolgraph does not register, and the HTTP transport's reply to a
335
+ malformed JSON-RPC body still come from the SDK as they are.
336
+ - CLI usage errors and printed paths no longer show credential-shaped input
337
+ (#206). Before, Click's and Typer's own usage errors quoted what was typed
338
+ (`No such command '<arg>'`, `'<value>' is not a valid float`), as did
339
+ Toolgraph's (`unknown profile '<value>'`), and lines such as
340
+ `Invalid servers config at <path>` and the `--config` error printed the
341
+ path as given. A usage error is now withheld whole when a token on the
342
+ command line is credential-shaped, or the tokens joined by spaces are
343
+ (`password =hunter2` typed as two arguments), checked as typed, before
344
+ Click or Typer quotes or escapes them (`\x09`, `\xa0`) in the message;
345
+ otherwise its
346
+ message goes through the credential scrubber. Lines in
347
+ which Toolgraph prints a path it was given or created (`init`, `check`,
348
+ `crawl`, `ingest-manifest`, `example init`, `policy propose --output`, and
349
+ the runtime-config errors) show a credential-shaped path as
350
+ `<path withheld: credential-shaped>` and keep the rest of the line, and
351
+ an error from the operating system prints its file names the same way
352
+ (`[Errno 2] No such file or directory: '<path>'`) instead of quoting them
353
+ with `repr`, which escaped a tab out of the scrubber's sight. A typo reads
354
+ as before (`No such command 'chek'. Did you mean 'check'?`); a value the
355
+ scrubber treats as an assignment, such as `token:v2`, is still masked
356
+ (`'token=<redacted>`). The `"output"` path in the JSON receipts of `policy compile`,
357
+ `policy review-plan`, `policy impact --output`, `preflight` and
358
+ `control-preflight` is left as given: it names the file the caller asked
359
+ for.
360
+ - Diagnostics that print a value with `repr` no longer show a credential
361
+ whose tab or no-break space it escaped (#229). Before, `repr`, `ascii`,
362
+ `%r`, a list or dict printed whole, a `KeyError` key and library errors
363
+ such as `invalid literal for int() with base 10: '<value>'` or
364
+ `could not convert string to float: b'<value>'` printed
365
+ `password<TAB>=secret` as `password\t=secret`, which the scrubber does not
366
+ read as an assignment: in `unknown profile` errors from the MCP tools, the
367
+ `AGENT_NOT_FOUND` line of the selector commands, `policy propose`
368
+ observation errors, and log records whose arguments were bytes or
369
+ containers. Now every message that goes through the credential scrubber
370
+ (CLI errors, MCP error text, log records, printed paths) is read as given
371
+ for str and bytes literals that hold a backslash escape, exactly as `repr`
372
+ or `ascii` prints them; such a literal that reads back as
373
+ credential-shaped, alone or in place in the message, withholds the whole
374
+ message, and so do more than 32 such literals. A literal without an
375
+ escape is scrubbed as before (`'password=secret'` reads
376
+ `'password=<redacted>`). Ordinary text is unchanged: a path is read only
377
+ when quoted as `repr` would quote it, so `C:\bearer\notes.json` is still
378
+ shown. A message that holds an escaped literal and also a credential the
379
+ scrubber would mask is withheld instead of masked. Not read: a literal
380
+ printed through `repr` twice, one right after an odd run of backslashes,
381
+ and one cut without its closing quote, as `int()` prints a value whose
382
+ repr is longer than 200 characters; the `int()` error itself is now
383
+ rendered without its value (#236, below). Artifact and evidence text,
384
+ which is stored rather than printed, is not affected.
385
+ - An `int()` error and a reply the MCP SDK could not parse no longer show
386
+ a credential from the value they quote (#236, #239). Both print a
387
+ shortened repr the scrubber cannot read back: `int()` cuts it at 200
388
+ characters with no closing quote, and the SDK copies pydantic's error, which
389
+ shortens `input_value` in the middle and can split an escape such as
390
+ `\x01` or drop the quote that decided repr's quote character. A
391
+ streamable-http server answering a crawl with a JSON string showed
392
+ `password<TAB>=secret` in the `[error]` line, and an `int()` error
393
+ chained under a withheld one showed it in a log record. Now a
394
+ `ValueError` whose only argument is `int()`'s text prints
395
+ `invalid literal for int() with base N (value not shown)`, and an MCP
396
+ error with the JSON-RPC parse error code (-32700), including one a server
397
+ sent itself, prints `the server's reply could not be parsed (JSON-RPC
398
+ parse error)`, wherever Toolgraph renders an exception: crawl failures,
399
+ log records (where an exception whose arguments are credential-shaped is
400
+ still withheld whole, as before) and every exception `doctor` reports,
401
+ which now renders exceptions like the other commands. Not covered: an
402
+ `int()` error raised as a `ValueError` subclass (a `str` subclass whose
403
+ `__repr__` raises one), and `re.Match` reprs (`%.50R`).
404
+ - A credential name at the end of a URL no longer leaves its value readable
405
+ in a scrubbed message (#232). The scrubber rewrites each URL to its origin
406
+ before it masks credentials, and a URL ends at whitespace or a quote, so
407
+ `fetch http://h/password =secret` printed `fetch http://h =secret`; so did
408
+ a name in a query (`?token =x`), with a tab or `: ` after it,
409
+ `.../authorization: Basic x`, `.../bearer x`, and a bracketed value
410
+ starting in the URL (`.../token=[a, x]`). Such a message is now withheld
411
+ whole. The text cannot tell a prose colon from an assignment, so
412
+ diagnostics whose URL path ends in a credential name with text after it
413
+ are withheld too: `Error accessing https://host/token: connection
414
+ refused`, `GET https://h/oauth/bearer returned 404`, `Client error for url
415
+ 'https://h/token='`, `GET https://h/password={id} returned 404`, a host
416
+ that is a credential name (`Error accessing https://token: connection
417
+ refused`), and a name in the host position of a malformed scheme
418
+ (`1https://password : x`, which was masked). A value that starts inside
419
+ the URL and continues past a quote, `<`, `>` or a tab
420
+ (`http://h/x?token=abc"def` shows `"def`, and with an input placeholder
421
+ `http://h/token=abc<redacted>x` shows `http://h>x`), is not caught, nor
422
+ are forms only the scrubber reads as an assignment: `.../password :x`,
423
+ `.../password:[a, x]`, `.../authorization=Basic x` and
424
+ `.../proxy_authorization=Basic x` (#241).
425
+ - A command that writes a file or a review annotation no longer prints a
426
+ traceback when the durability warning is escalated to an error (#129).
427
+ The warning (`ArtifactDurabilityWarning`, or
428
+ `ReviewCandidateDurabilityWarning` for `review-candidates annotate`) is
429
+ raised after the write is published, when the directory could not be
430
+ fsynced. With `-W error` the command still fails, as asked, but with one
431
+ `ERROR:` line and exit 1 instead of a traceback; no JSON receipt is
432
+ printed. Before, `init` also deleted the database it had just created
433
+ while keeping the published `config.json` that names it; both are now
434
+ kept. The annotation warning now ends `do not retry: a retry appends a
435
+ second event`, since the event is already recorded. Library calls still
436
+ raise the escalated warning after publication.
437
+
5
438
  ## 0.3.0 - 2026-10-19
6
439
 
7
440
  Fourth alpha. Read **Upgrading from 0.2.0** below before installing: it lists
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: toolgraph
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Graph-native MCP/tool registry, policy compiler, and impact analyzer
5
5
  Project-URL: Homepage, https://github.com/memtomem/toolgraph
6
6
  Project-URL: Documentation, https://github.com/memtomem/toolgraph/blob/main/docs/beginner-guide.md
@@ -342,8 +342,13 @@ come from the orchestrator and Tracegraph runs against your own fleet.
342
342
  `preflight` wraps the same deterministic selector rules and stamps the result
343
343
  with a bracketed `graph_generation`. It remains advisory: rejected candidates
344
344
  produce `decision: advisory_warn` and exit 0. Unknown agents produce
345
- `unresolved_identity`, never a false allow. Resource URIs in evidence are
346
- scrubbed of userinfo and query strings before the artifact is written. Pass
345
+ `unresolved_identity`, never a false allow. Resource URIs in evidence paths
346
+ and resource fields are scrubbed of userinfo, query strings and fragments
347
+ before the artifact is written; one with an `@` after its authority, or one
348
+ that cannot be parsed, is shown as `<scheme>://<redacted-uri>`. Inside free
349
+ text, such as an exception reason, a URI ends at whitespace or `)`, so a
350
+ query or fragment holding `)` (`https://h/r?k=a)b`) still shows what follows
351
+ the `)`. Pass
347
352
  `--features` only when the consumer needs the full selector feature rows.
348
353
  With `--out` the artifact is written privately and atomically, and the command
349
354
  prints a result envelope carrying `artifact_digest` — the SHA-256 of the exact
@@ -378,11 +383,14 @@ Local review notes may cite paths but reject credential-shaped content: a
378
383
  `<name>=`, or a `<name>:` followed by a space or a quote, where the name is a
379
384
  token, password, secret, credential, API key or authorization, so `token:
380
385
  rotated weekly` has to be rephrased while a versioned tool name such as
381
- `srv::get_token:v2` is fine. Reviewer names and the report's run ids and tool
382
- keys are checked the same way.
386
+ `srv::get_token:v2` is fine. Reviewer names are checked the same way, and the
387
+ report's run ids and tool keys more strictly: as identities, they also refuse
388
+ any URI with an `@` after its `://`.
383
389
  If a genuine parent-directory fsync error occurs after replacement, the event
384
390
  remains successful and visible while a durability warning is written to stderr;
385
- retrying would append a duplicate event.
391
+ retrying would append a duplicate event. With that warning escalated to an
392
+ error (`-W error`), `annotate` exits 1 with one `ERROR:` line, and the event is
393
+ still recorded: do not retry.
386
394
 
387
395
  Candidate ids use the same exact-tuple UUIDv5 as SyncMill's merged
388
396
  `board import-review-candidates` consumer (SyncMill PR #57), so the two human
@@ -472,12 +480,20 @@ to stderr) and exit 0 by default — gating is opt-in.
472
480
  | `audit-report --fail-on-blocking` | 1 when `status` is `fail` (unsafe violations or drifted governed tools) |
473
481
  | `preflight` | 0 for advisory allow, warn, and unresolved identity · 1 when the artifact cannot be built or written |
474
482
  | `control-preflight` | 0 including advisory warnings · 1 on an invalid plan or an I/O failure |
475
- | `policy compile` | 0 after atomic bundle replacement · 1 on unresolved identity/state or I/O failure |
483
+ | `policy compile` | 0 after atomic bundle replacement (see the note below on escalated durability warnings) · 1 on unresolved identity/state or I/O failure |
476
484
  | `policy review-plan` | 0 · 1 on an invalid or stale report, unresolved graph state, or I/O failure |
477
485
  | `policy propose` | 0 · 1 on invalid input or a refused export · 2 for `--json` with `--output`, or `--min-would-block` below 1 |
478
486
  | `review-candidates list/annotate` | 0 on valid JSON output · 1 on invalid/stale artifacts or I/O failure · 2 on invalid CLI arguments |
479
487
  | `example init` | 2 when the destination already exists |
480
488
 
489
+ A command that writes a file (`init`, `preflight --out`, `control-preflight
490
+ --out`, `policy impact --output`, `policy compile`, `policy review-plan`,
491
+ `policy propose --output`) or a review annotation warns on stderr and exits as
492
+ above when the file is published but its directory could not be fsynced. With
493
+ that warning escalated to an error (`-W error`), it exits 1 with one `ERROR:`
494
+ line and prints no receipt; the file or event is published, but its crash
495
+ durability is uncertain.
496
+
481
497
  An error outside these classes exits 1 with a Python traceback instead of an
482
498
  `ERROR:` line.
483
499
 
@@ -303,8 +303,13 @@ come from the orchestrator and Tracegraph runs against your own fleet.
303
303
  `preflight` wraps the same deterministic selector rules and stamps the result
304
304
  with a bracketed `graph_generation`. It remains advisory: rejected candidates
305
305
  produce `decision: advisory_warn` and exit 0. Unknown agents produce
306
- `unresolved_identity`, never a false allow. Resource URIs in evidence are
307
- scrubbed of userinfo and query strings before the artifact is written. Pass
306
+ `unresolved_identity`, never a false allow. Resource URIs in evidence paths
307
+ and resource fields are scrubbed of userinfo, query strings and fragments
308
+ before the artifact is written; one with an `@` after its authority, or one
309
+ that cannot be parsed, is shown as `<scheme>://<redacted-uri>`. Inside free
310
+ text, such as an exception reason, a URI ends at whitespace or `)`, so a
311
+ query or fragment holding `)` (`https://h/r?k=a)b`) still shows what follows
312
+ the `)`. Pass
308
313
  `--features` only when the consumer needs the full selector feature rows.
309
314
  With `--out` the artifact is written privately and atomically, and the command
310
315
  prints a result envelope carrying `artifact_digest` — the SHA-256 of the exact
@@ -339,11 +344,14 @@ Local review notes may cite paths but reject credential-shaped content: a
339
344
  `<name>=`, or a `<name>:` followed by a space or a quote, where the name is a
340
345
  token, password, secret, credential, API key or authorization, so `token:
341
346
  rotated weekly` has to be rephrased while a versioned tool name such as
342
- `srv::get_token:v2` is fine. Reviewer names and the report's run ids and tool
343
- keys are checked the same way.
347
+ `srv::get_token:v2` is fine. Reviewer names are checked the same way, and the
348
+ report's run ids and tool keys more strictly: as identities, they also refuse
349
+ any URI with an `@` after its `://`.
344
350
  If a genuine parent-directory fsync error occurs after replacement, the event
345
351
  remains successful and visible while a durability warning is written to stderr;
346
- retrying would append a duplicate event.
352
+ retrying would append a duplicate event. With that warning escalated to an
353
+ error (`-W error`), `annotate` exits 1 with one `ERROR:` line, and the event is
354
+ still recorded: do not retry.
347
355
 
348
356
  Candidate ids use the same exact-tuple UUIDv5 as SyncMill's merged
349
357
  `board import-review-candidates` consumer (SyncMill PR #57), so the two human
@@ -433,12 +441,20 @@ to stderr) and exit 0 by default — gating is opt-in.
433
441
  | `audit-report --fail-on-blocking` | 1 when `status` is `fail` (unsafe violations or drifted governed tools) |
434
442
  | `preflight` | 0 for advisory allow, warn, and unresolved identity · 1 when the artifact cannot be built or written |
435
443
  | `control-preflight` | 0 including advisory warnings · 1 on an invalid plan or an I/O failure |
436
- | `policy compile` | 0 after atomic bundle replacement · 1 on unresolved identity/state or I/O failure |
444
+ | `policy compile` | 0 after atomic bundle replacement (see the note below on escalated durability warnings) · 1 on unresolved identity/state or I/O failure |
437
445
  | `policy review-plan` | 0 · 1 on an invalid or stale report, unresolved graph state, or I/O failure |
438
446
  | `policy propose` | 0 · 1 on invalid input or a refused export · 2 for `--json` with `--output`, or `--min-would-block` below 1 |
439
447
  | `review-candidates list/annotate` | 0 on valid JSON output · 1 on invalid/stale artifacts or I/O failure · 2 on invalid CLI arguments |
440
448
  | `example init` | 2 when the destination already exists |
441
449
 
450
+ A command that writes a file (`init`, `preflight --out`, `control-preflight
451
+ --out`, `policy impact --output`, `policy compile`, `policy review-plan`,
452
+ `policy propose --output`) or a review annotation warns on stderr and exits as
453
+ above when the file is published but its directory could not be fsynced. With
454
+ that warning escalated to an error (`-W error`), it exits 1 with one `ERROR:`
455
+ line and prints no receipt; the file or event is published, but its crash
456
+ durability is uncertain.
457
+
442
458
  An error outside these classes exits 1 with a Python traceback instead of an
443
459
  `ERROR:` line.
444
460
 
@@ -77,7 +77,7 @@ notebook = ["nbclient>=0.10", "ipykernel>=6"]
77
77
  # different bytes behind the same reviewed commit. `scripts/verify-artifacts.sh`
78
78
  # asserts the sdist rebuilds the wheel byte for byte, which only holds while
79
79
  # the backend is fixed. Bump deliberately, then rehearse.
80
- requires = ["hatchling==1.32.4"]
80
+ requires = ["hatchling==1.33.0"]
81
81
  build-backend = "hatchling.build"
82
82
 
83
83
  [tool.hatch.version]
@@ -1,3 +1,3 @@
1
1
  """toolgraph — graph-native MCP/tool registry for advisory governance analysis."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.4.1"