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