toolgraph 0.3.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 (60) hide show
  1. {toolgraph-0.3.0 → toolgraph-0.4.0}/CHANGELOG.md +347 -0
  2. {toolgraph-0.3.0 → toolgraph-0.4.0}/PKG-INFO +23 -7
  3. {toolgraph-0.3.0 → toolgraph-0.4.0}/README.md +22 -6
  4. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/__init__.py +1 -1
  5. toolgraph-0.4.0/toolgraph/artifact_safety.py +260 -0
  6. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/artifacts.py +6 -3
  7. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/cli.py +136 -27
  8. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/config.py +19 -7
  9. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/control_plan.py +63 -6
  10. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/crawler/crawl.py +3 -2
  11. toolgraph-0.4.0/toolgraph/error_text.py +259 -0
  12. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/errors.py +15 -0
  13. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/loader.py +2 -1
  14. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/queries.py +10 -2
  15. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/health.py +6 -5
  16. toolgraph-0.4.0/toolgraph/log_safety.py +183 -0
  17. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/manifest/ingest.py +11 -4
  18. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/models.py +86 -5
  19. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/policy_impact.py +2 -2
  20. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/policy_propose.py +24 -8
  21. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/redaction.py +205 -14
  22. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/review_candidates.py +35 -22
  23. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/server/app.py +32 -2
  24. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/yaml_loader.py +37 -6
  25. toolgraph-0.3.0/toolgraph/artifact_safety.py +0 -171
  26. {toolgraph-0.3.0 → toolgraph-0.4.0}/.gitignore +0 -0
  27. {toolgraph-0.3.0 → toolgraph-0.4.0}/LICENSE +0 -0
  28. {toolgraph-0.3.0 → toolgraph-0.4.0}/SECURITY.md +0 -0
  29. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/control-plan.schema.json +0 -0
  30. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/control-preflight.schema.json +0 -0
  31. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/observation-window.schema.json +0 -0
  32. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/policy-bundle.schema.json +0 -0
  33. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/policy-impact.schema.json +0 -0
  34. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/policy-review-plan.schema.json +0 -0
  35. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/preflight.schema.json +0 -0
  36. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/review-annotations.schema.json +0 -0
  37. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/tracegraph/README.md +0 -0
  38. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/tracegraph/review-candidates.schema.json +0 -0
  39. {toolgraph-0.3.0 → toolgraph-0.4.0}/contracts/tracegraph/v1.json +0 -0
  40. {toolgraph-0.3.0 → toolgraph-0.4.0}/pyproject.toml +0 -0
  41. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/crawler/__init__.py +0 -0
  42. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/crawler/client.py +0 -0
  43. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/crawler/transports.py +0 -0
  44. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/__init__.py +0 -0
  45. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/driver.py +0 -0
  46. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/schema.py +0 -0
  47. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/selector.py +0 -0
  48. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/graph/store.py +0 -0
  49. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/manifest/__init__.py +0 -0
  50. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/manifest/parser.py +0 -0
  51. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/policy_bundle.py +0 -0
  52. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/policy_impact_models.py +0 -0
  53. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/policy_review.py +0 -0
  54. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/preflight.py +0 -0
  55. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/quickstart/README.md +0 -0
  56. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/quickstart/governance.yaml +0 -0
  57. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/quickstart/policy_gateway_server.py +0 -0
  58. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/quickstart/servers.yaml +0 -0
  59. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/server/__init__.py +0 -0
  60. {toolgraph-0.3.0 → toolgraph-0.4.0}/toolgraph/uris.py +0 -0
@@ -2,6 +2,353 @@
2
2
 
3
3
  All notable changes to Toolgraph are documented here.
4
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
+
5
352
  ## 0.3.0 - 2026-10-19
6
353
 
7
354
  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.0
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
 
@@ -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.0"