toolgraph 0.0.1__py3-none-any.whl
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/__init__.py +3 -0
- toolgraph/artifact_safety.py +97 -0
- toolgraph/artifacts.py +122 -0
- toolgraph/cli.py +951 -0
- toolgraph/config.py +106 -0
- toolgraph/control_plan.py +494 -0
- toolgraph/crawler/__init__.py +0 -0
- toolgraph/crawler/client.py +120 -0
- toolgraph/crawler/crawl.py +103 -0
- toolgraph/crawler/transports.py +60 -0
- toolgraph/graph/__init__.py +0 -0
- toolgraph/graph/driver.py +275 -0
- toolgraph/graph/loader.py +280 -0
- toolgraph/graph/queries.py +984 -0
- toolgraph/graph/schema.py +166 -0
- toolgraph/graph/selector.py +423 -0
- toolgraph/graph/store.py +57 -0
- toolgraph/manifest/__init__.py +0 -0
- toolgraph/manifest/ingest.py +542 -0
- toolgraph/manifest/parser.py +26 -0
- toolgraph/models.py +192 -0
- toolgraph/policy_bundle.py +212 -0
- toolgraph/policy_review.py +97 -0
- toolgraph/preflight.py +77 -0
- toolgraph/quickstart/governance.yaml +28 -0
- toolgraph/quickstart/policy_gateway_server.py +31 -0
- toolgraph/quickstart/servers.yaml +5 -0
- toolgraph/redaction.py +82 -0
- toolgraph/review_candidates.py +579 -0
- toolgraph/server/__init__.py +0 -0
- toolgraph/server/app.py +306 -0
- toolgraph/uris.py +64 -0
- toolgraph/yaml_loader.py +68 -0
- toolgraph-0.0.1.dist-info/METADATA +674 -0
- toolgraph-0.0.1.dist-info/RECORD +38 -0
- toolgraph-0.0.1.dist-info/WHEEL +4 -0
- toolgraph-0.0.1.dist-info/entry_points.txt +2 -0
- toolgraph-0.0.1.dist-info/licenses/LICENSE +191 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: toolgraph
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Graph-native MCP/tool registry, policy compiler, and impact analyzer
|
|
5
|
+
Project-URL: Homepage, https://github.com/memtomem/toolgraph
|
|
6
|
+
Project-URL: Repository, https://github.com/memtomem/toolgraph
|
|
7
|
+
Project-URL: Issues, https://github.com/memtomem/toolgraph/issues
|
|
8
|
+
Author: Toolgraph contributors
|
|
9
|
+
Author-email: "DAPADA Inc." <contact@dapada.co.kr>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai,governance,graph,mcp,policy,security
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: <3.15,>=3.12
|
|
22
|
+
Requires-Dist: cryptography>=50.0.0
|
|
23
|
+
Requires-Dist: idna>=3.18
|
|
24
|
+
Requires-Dist: mcp<2,>=1.28.1
|
|
25
|
+
Requires-Dist: neo4j<7,>=6.2
|
|
26
|
+
Requires-Dist: pydantic-settings>=2.14.2
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
28
|
+
Requires-Dist: pyjwt>=2.13.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0
|
|
30
|
+
Requires-Dist: python-multipart>=0.0.31
|
|
31
|
+
Requires-Dist: pyyaml>=6.0
|
|
32
|
+
Requires-Dist: starlette>=1.3.1
|
|
33
|
+
Requires-Dist: typer>=0.27.0
|
|
34
|
+
Provides-Extra: ladybug
|
|
35
|
+
Requires-Dist: ladybug<0.19,>=0.18.2; extra == 'ladybug'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Toolgraph
|
|
39
|
+
|
|
40
|
+
> π§ **Alpha** β APIs and defaults may change between `0.x` minor releases.
|
|
41
|
+
> Feedback and issue reports are especially welcome:
|
|
42
|
+
> [Issues](https://github.com/memtomem/toolgraph/issues) Β·
|
|
43
|
+
> [Discussions](https://github.com/memtomem/toolgraph/discussions)
|
|
44
|
+
>
|
|
45
|
+
> Toolgraph is an advisory control-plane tool, not a runtime security boundary.
|
|
46
|
+
|
|
47
|
+
A graph-native registry for MCP tools. It crawls MCP servers into a local Ladybug or shared Neo4j graph,
|
|
48
|
+
lets you author governance (who can call what, what touches which resource, which
|
|
49
|
+
resources are under which policy), and answers **explainable reachability
|
|
50
|
+
questions** over the result:
|
|
51
|
+
|
|
52
|
+
- *Which tools can this agent call that touch a resource under a DENY policy?*
|
|
53
|
+
- *Is this agent allowed to call this tool β and does the call cross a deny policy?*
|
|
54
|
+
- *If this resource (or policy) changes, which agents and tools are affected?*
|
|
55
|
+
|
|
56
|
+
Each answer comes back with the **exact graph path as evidence**, not a bare boolean,
|
|
57
|
+
and carries per-edge **provenance** (crawled fact vs operator assertion vs inferred,
|
|
58
|
+
with a free-text evidence pointer) so reviewers can verify the claim.
|
|
59
|
+
|
|
60
|
+
> **Advisory, not enforcement.** toolgraph analyzes and explains; it does **not**
|
|
61
|
+
> sit in the traffic path and does not block calls at runtime. Think of it as a
|
|
62
|
+
> risk/impact analyzer and policy compiler for your MCP tool estate. Runtime
|
|
63
|
+
> enforcement consumes its product-neutral policy bundle in a gateway;
|
|
64
|
+
> memtomem-stm is the first reference consumer, not a required dependency.
|
|
65
|
+
|
|
66
|
+
## Why a graph (and what the alternatives are)
|
|
67
|
+
|
|
68
|
+
Discovery (βfind a tool like Xβ) is well served by vector search. Embedding
|
|
69
|
+
search **alone**, however, cannot give deterministic reachability or
|
|
70
|
+
blast-radius answers β `Agent β Tool β Resource β Policy` is a many-to-many
|
|
71
|
+
join across four node types, not a similarity problem.
|
|
72
|
+
|
|
73
|
+
Several structured-metadata approaches *can* answer the same questions if
|
|
74
|
+
given the same edges:
|
|
75
|
+
|
|
76
|
+
- OPA/Rego over a JSON-authored ACL model
|
|
77
|
+
- A relational schema with `(agent, tool, resource, policy)` joins
|
|
78
|
+
- A resource-tagged tool registry with namespace + grant tables
|
|
79
|
+
|
|
80
|
+
toolgraph is one implementation of that model, optimised for: idempotent
|
|
81
|
+
crawl-then-author flows, explainable paths returned as first-class output,
|
|
82
|
+
and exposing the same queries over MCP so an agent can audit the registry
|
|
83
|
+
through the protocol it was crawled from. It does **not** claim "only a
|
|
84
|
+
graph can answer this." It claims the graph form makes the authoring +
|
|
85
|
+
audit ergonomics simple, and that embedding-only registries cannot.
|
|
86
|
+
|
|
87
|
+
Every edge here is either a crawled fact or an operator-authored assertion;
|
|
88
|
+
nothing is LLM-guessed.
|
|
89
|
+
|
|
90
|
+
## Graph model
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
(MCPServer)-[:EXPOSES]->(Tool) # crawled (factual)
|
|
94
|
+
(MCPServer)-[:PROVIDES]->(Resource) # crawled (factual)
|
|
95
|
+
(Agent)-[:CAN_CALL]->(Tool) # authored
|
|
96
|
+
(Tool)-[:READS|WRITES]->(Resource) # authored
|
|
97
|
+
(Resource|Tool)-[:GOVERNED_BY]->(Policy) # authored
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`Policy{id, effect: ALLOW|DENY, scope, description}`.
|
|
101
|
+
|
|
102
|
+
## Quickstart
|
|
103
|
+
|
|
104
|
+
New to Toolgraph? Start with the first-time user guides:
|
|
105
|
+
[English](https://github.com/memtomem/toolgraph/blob/main/docs/beginner-guide.md) or
|
|
106
|
+
[Korean](https://github.com/memtomem/toolgraph/blob/main/docs/ko-beginner-guide.md).
|
|
107
|
+
|
|
108
|
+
For the context-engineering and tool-selection roadmap, see
|
|
109
|
+
the [context-engineering report](https://github.com/memtomem/toolgraph/blob/main/docs/context-engineering-tool-selection-report.md).
|
|
110
|
+
Contract-shaping decisions are recorded in the
|
|
111
|
+
[ADRs](https://github.com/memtomem/toolgraph/tree/main/docs/adr).
|
|
112
|
+
Maintainers should follow the [release runbook](https://github.com/memtomem/toolgraph/blob/main/docs/releasing.md).
|
|
113
|
+
|
|
114
|
+
### Install
|
|
115
|
+
|
|
116
|
+
The recommended local backend is included through the `ladybug` extra:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
uv tool install "toolgraph[ladybug]"
|
|
120
|
+
# or: python -m pip install "toolgraph[ladybug]"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
That puts `toolgraph` on your `PATH`; run `uv tool update-shell` once if `uv`
|
|
124
|
+
says its bin directory is not there yet.
|
|
125
|
+
|
|
126
|
+
You can also inspect the CLI without a persistent install:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
uvx --from "toolgraph[ladybug]" toolgraph --version
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary>From source</summary>
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
git clone https://github.com/memtomem/toolgraph.git
|
|
137
|
+
cd toolgraph
|
|
138
|
+
uv tool install --from '.[ladybug]' toolgraph
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
</details>
|
|
142
|
+
|
|
143
|
+
### First policy bundle
|
|
144
|
+
|
|
145
|
+
Once `toolgraph` is on your `PATH`, no Docker, Node.js or clone is required:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
toolgraph example init toolgraph-quickstart
|
|
149
|
+
cd toolgraph-quickstart
|
|
150
|
+
toolgraph init
|
|
151
|
+
toolgraph crawl --servers servers.yaml
|
|
152
|
+
toolgraph ingest-manifest --governance governance.yaml --strict-drift
|
|
153
|
+
toolgraph eligible-tools vibe-coder \
|
|
154
|
+
policy-gateway::read_note policy-gateway::publish_note --profile review
|
|
155
|
+
toolgraph selection-explain vibe-coder policy-gateway::publish_note
|
|
156
|
+
toolgraph policy compile --agent vibe-coder --profile review \
|
|
157
|
+
--output .toolgraph/policy-bundle.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Ladybug is the local single-process backend. Toolgraph commands own the DB
|
|
161
|
+
sequentially; gateways read only the generated JSON bundle. Use Neo4j for a
|
|
162
|
+
shared service or multiple concurrent processes. The beginner guides explain
|
|
163
|
+
the decision output and the memtomem-stm handoff.
|
|
164
|
+
|
|
165
|
+
For contributors and larger shared/fleet demos, clone the repository and run:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
uv sync --group dev --extra ladybug
|
|
169
|
+
docker compose up -d --wait
|
|
170
|
+
cp .env.example .env
|
|
171
|
+
bash scripts/demo.sh
|
|
172
|
+
bash scripts/demo-public.sh
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The compose file is development-only, uses a known password, and binds Neo4j
|
|
176
|
+
to loopback. It is not an authenticated production deployment. The demo
|
|
177
|
+
scripts themselves use disposable Ladybug databases and never reset your
|
|
178
|
+
configured graph.
|
|
179
|
+
|
|
180
|
+
The cross-server demo is the headline: `ci-bot` has no filesystem read grant,
|
|
181
|
+
yet `unsafe-tools ci-bot` flags it β because `git_show` on a *different* server
|
|
182
|
+
reaches the same governed secret. The graph form makes the cross-server path
|
|
183
|
+
explicit without per-server bookkeeping; the same authored edges in a
|
|
184
|
+
relational or OPA model would catch it too, but operators would have to
|
|
185
|
+
write the join themselves.
|
|
186
|
+
|
|
187
|
+
The same wedge appears for **proxy/surfacing servers**. `examples/servers-gate.yaml`
|
|
188
|
+
crawls memtomem + memtomem-stm (a docs proxy whose `surfacing` engine injects
|
|
189
|
+
results from the operator's memtomem LTM into every proxied response β see
|
|
190
|
+
`memtomem_stm/proxy/manager.py::ProxyManager._apply_surfacing`).
|
|
191
|
+
`examples/governance-gate.yaml` flags
|
|
192
|
+
an agent whose only grants are on proxied docs tools (`langchain__*`,
|
|
193
|
+
`langfuse__*`) β because surfacing makes every such call indirectly read the
|
|
194
|
+
operator's personal LTM namespace, which their descriptions never mention.
|
|
195
|
+
Run with:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
uv run toolgraph crawl --servers examples/servers-gate.yaml
|
|
199
|
+
uv run toolgraph ingest-manifest --governance examples/governance-gate.yaml
|
|
200
|
+
uv run toolgraph check-access helper "memtomem-stm::langchain__search_docs_by_lang_chain"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Or step by step:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
uv run toolgraph check # verify Neo4j connectivity
|
|
207
|
+
uv run toolgraph init-schema # uniqueness constraints
|
|
208
|
+
uv run toolgraph crawl --servers examples/servers.yaml # crawl -> load factual graph
|
|
209
|
+
uv run toolgraph ingest-manifest --governance examples/governance.yaml
|
|
210
|
+
# refs to tools no server currently exposes emit a non-fatal DRIFT notice;
|
|
211
|
+
# add --strict-drift to reject the manifest instead (CI gating, ADR-0002)
|
|
212
|
+
|
|
213
|
+
# positive-reachability queries
|
|
214
|
+
uv run toolgraph unsafe-tools public-bot # violations only (default)
|
|
215
|
+
uv run toolgraph unsafe-tools public-bot --all # + operator-declared exceptions
|
|
216
|
+
uv run toolgraph check-access public-bot read_file
|
|
217
|
+
uv run toolgraph blast-radius "file:///private/secrets.env"
|
|
218
|
+
|
|
219
|
+
# negative-truth / drift queries β what the graph DOESN'T say but should
|
|
220
|
+
uv run toolgraph unmapped-tools # granted tools w/ no READS/WRITES yet
|
|
221
|
+
uv run toolgraph orphan-policies # policies no agent currently reaches
|
|
222
|
+
uv run toolgraph unbacked-edges # READS/WRITES/GOVERNED_BY w/ no evidence
|
|
223
|
+
uv run toolgraph unbacked-edges --include-grants # also audit CAN_CALL grants
|
|
224
|
+
uv run toolgraph drift # tools w/ governance but no live EXPOSES
|
|
225
|
+
uv run toolgraph destructive-unsafeguarded # destructive-hinted tools w/ no policy path
|
|
226
|
+
uv run toolgraph annotation-contradictions # authored WRITES vs readOnlyHint claims
|
|
227
|
+
uv run toolgraph audit-report # whole-graph operational audit summary
|
|
228
|
+
uv run toolgraph audit-report --format markdown # human-readable audit report
|
|
229
|
+
|
|
230
|
+
# selector surface (ADR-0005) β batch, deterministic, no relevance scoring
|
|
231
|
+
uv run toolgraph rank-features planner git_status read_file export_data
|
|
232
|
+
uv run toolgraph eligible-tools planner git_status read_file --profile strict
|
|
233
|
+
uv run toolgraph selection-explain planner git_status
|
|
234
|
+
|
|
235
|
+
# advisory run artifact for syncmill/tracegraph correlation
|
|
236
|
+
uv run toolgraph preflight planner git_status read_file \
|
|
237
|
+
--profile review --run-id "$RUN_ID" --out preflight.json
|
|
238
|
+
|
|
239
|
+
# body-free orchestration topology + principal/tool-surface preflight
|
|
240
|
+
uv run toolgraph control-preflight control-plan.json \
|
|
241
|
+
--profile review --out control-preflight.json
|
|
242
|
+
|
|
243
|
+
# portable control-plane artifact for memtomem-stm or another MCP gateway
|
|
244
|
+
uv run toolgraph policy compile --agent planner --profile strict \
|
|
245
|
+
--output policy-bundle.json
|
|
246
|
+
|
|
247
|
+
# human review of Tracegraph T3 findings (no Neo4j or policy mutation)
|
|
248
|
+
uv run toolgraph review-candidates list candidates.json
|
|
249
|
+
uv run toolgraph review-candidates annotate candidates.json CANDIDATE_ID \
|
|
250
|
+
--status accepted --reviewer operator --note "manual policy review approved"
|
|
251
|
+
|
|
252
|
+
# accepted evidence -> current-policy work plan (still no policy mutation)
|
|
253
|
+
uv run toolgraph policy review-plan candidates.json --agent planner \
|
|
254
|
+
--profile review --output policy-review-plan.json
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`preflight` wraps the same deterministic selector rules and stamps the result
|
|
258
|
+
with a bracketed `graph_generation`. It remains advisory: rejected candidates
|
|
259
|
+
produce `decision: advisory_warn` and exit 0. Unknown agents produce
|
|
260
|
+
`unresolved_identity`, never a false allow. Resource URIs in evidence are
|
|
261
|
+
scrubbed of userinfo and query strings before the artifact is written. Pass
|
|
262
|
+
`--features` only when the consumer needs the full selector feature rows.
|
|
263
|
+
With `--out` the artifact is written privately and atomically, and the command
|
|
264
|
+
prints a result envelope carrying `artifact_digest` β the SHA-256 of the exact
|
|
265
|
+
file bytes in the ecosystem's `sha256:<hex>` form, so a run report can record
|
|
266
|
+
the value as-is and a consumer can verify it with `sha256sum`. Digests cover
|
|
267
|
+
bytes, not parsed JSON: a consumer that re-serializes the artifact is hashing a
|
|
268
|
+
different document. Without `--out` the artifact itself goes to stdout. SyncMill P3.1 may enforce this evidence before orchestration, but the
|
|
269
|
+
producer artifact and Toolgraph exit-code contract remain advisory (ADR-0008).
|
|
270
|
+
|
|
271
|
+
`control-preflight` accepts a versioned, body-free upper-bound control graph.
|
|
272
|
+
It checks one start and end, reachability, acyclicity, terminal paths, bounded
|
|
273
|
+
model fan-out, and aggregate call/parallel limits. It then evaluates every
|
|
274
|
+
declared principal and qualified candidate tool inside one collision-safe
|
|
275
|
+
`graph_state` bracket. The canonical private result is still advisory and
|
|
276
|
+
exits successfully for policy or structural warnings; an orchestrator such as
|
|
277
|
+
SyncMill decides whether those warnings block a run. Prompts, outputs, patches,
|
|
278
|
+
credentials, commands, memory bodies, and filesystem paths do not belong in
|
|
279
|
+
either exchange artifact. Toolgraph does not execute, persist, resume, or
|
|
280
|
+
observe the declared workflow (ADR-0013).
|
|
281
|
+
|
|
282
|
+
`policy compile` evaluates every currently exposed qualified tool, fingerprints
|
|
283
|
+
the crawled tool contract, and writes canonical JSON with private permissions
|
|
284
|
+
and atomic replacement. Each tool appears exactly once in the bundle; a
|
|
285
|
+
duplicated catalog row fails compilation instead of shipping. The bundle
|
|
286
|
+
carries a collision-safe
|
|
287
|
+
`graph_state: {instance_id, generation}` plus governance and catalog digests.
|
|
288
|
+
Toolgraph still does not claim that it blocked a call: a gateway consumes the
|
|
289
|
+
bundle, chooses review or strict behavior, and records the enforcement action
|
|
290
|
+
(ADR-0010). Re-run `ingest-manifest` before compilation when upgrading a graph
|
|
291
|
+
that predates the governance-digest field.
|
|
292
|
+
|
|
293
|
+
`review-candidates` is the G3 human-review boundary (ADR-0009). It consumes
|
|
294
|
+
Tracegraph's body-free schema-v1 report and projects only `run_id`, versioned
|
|
295
|
+
pattern identity, qualified `tool_key`, and the analyzed artifact digest.
|
|
296
|
+
Unknown additive v1 fields are discarded; unknown major versions fail closed.
|
|
297
|
+
The original report is never edited. `annotate` appends `open` / `accepted` /
|
|
298
|
+
`dismissed` events to `<REPORT>.toolgraph-review.json`, bound to the SHA-256 of
|
|
299
|
+
the exact report bytes and written under a local lock with atomic replacement.
|
|
300
|
+
The sidecar path is resolved before load, lock, and replacement so symlink
|
|
301
|
+
aliases cannot split history. Successful replacement fsyncs both the file and
|
|
302
|
+
its parent directory where the platform supports it.
|
|
303
|
+
Local review notes may cite paths but reject credential-shaped content.
|
|
304
|
+
If a genuine parent-directory fsync error occurs after replacement, the event
|
|
305
|
+
remains successful and visible while a durability warning is written to stderr;
|
|
306
|
+
retrying would append a duplicate event.
|
|
307
|
+
|
|
308
|
+
Candidate ids use the same exact-tuple UUIDv5 as SyncMill's merged
|
|
309
|
+
`board import-review-candidates` consumer (SyncMill PR #57), so the two human
|
|
310
|
+
review surfaces can be correlated without sharing storage. Their states remain
|
|
311
|
+
independent: Toolgraph `accepted` is only a policy-review disposition. It does
|
|
312
|
+
not complete a SyncMill board item, modify governance, change selector output,
|
|
313
|
+
or increment `graph_generation`.
|
|
314
|
+
|
|
315
|
+
`policy review-plan` is the explicit next step after acceptance (ADR-0012).
|
|
316
|
+
It selects only currently accepted candidates, evaluates their qualified tool
|
|
317
|
+
keys against one agent/profile, binds the result to the current graph instance
|
|
318
|
+
and generation, and writes a private atomic work-plan artifact. It deliberately
|
|
319
|
+
sets `decision_mode: "human_required"` and `automatic_change: false`: an
|
|
320
|
+
operator still edits the governance manifest, runs `ingest-manifest`, reviews
|
|
321
|
+
the resulting decision/blast radius, and then runs `policy compile`. Trace
|
|
322
|
+
evidence never grants, denies, or creates an exception by itself.
|
|
323
|
+
|
|
324
|
+
### Exit codes
|
|
325
|
+
|
|
326
|
+
Per [ADR-0003](https://github.com/memtomem/toolgraph/blob/main/docs/adr/0003-cli-output-and-exit-code-contract.md) this table
|
|
327
|
+
is a contract: query commands reserve stdout for their JSON (diagnostics go
|
|
328
|
+
to stderr) and exit 0 by default β gating is opt-in.
|
|
329
|
+
|
|
330
|
+
| Command | Exit code |
|
|
331
|
+
| --- | --- |
|
|
332
|
+
| query commands (default) | 0 always β advisory, even on DENY/violations |
|
|
333
|
+
| `unsafe-tools` / `rank-features` / `eligible-tools` / `selection-explain` (unknown agent) | 1 β `AGENT_NOT_FOUND` printed to stderr |
|
|
334
|
+
| `preflight` | 0 for advisory allow, warn, and unresolved identity; operational/argument errors are non-zero |
|
|
335
|
+
| `policy compile` | 0 after atomic bundle replacement Β· 1 on unresolved identity/state or I/O failure |
|
|
336
|
+
| `review-candidates list/annotate` | 0 on valid JSON output Β· 1 on invalid/stale artifacts or I/O failure Β· 2 on invalid CLI arguments |
|
|
337
|
+
| `ingest-manifest` (manifest REJECTED) | 1 |
|
|
338
|
+
| `crawl` (any server failed) | 1 |
|
|
339
|
+
| `check-access --fail-on-deny` | 1 on `DENY` with `all_authorized: false` Β· 2 on `AGENT_NOT_FOUND` / `TOOL_NOT_FOUND` / `AMBIGUOUS_TOOL` Β· 0 otherwise (`ALLOW`, `NOT_GRANTED`, authorized-only `DENY`) |
|
|
340
|
+
| `unsafe-tools --fail-on-violation` | 1 when any `violation` row remains in the printed output (with or without `--all`) |
|
|
341
|
+
|
|
342
|
+
A typo'd name exits 2 under `--fail-on-deny` so it can't pass a CI gate as a
|
|
343
|
+
clean run.
|
|
344
|
+
|
|
345
|
+
Note for CI scripts: exit 2 is overloaded. Click uses 2 for usage errors (a
|
|
346
|
+
bad flag or parameter), and `--fail-on-deny` uses 2 for not-found/ambiguous
|
|
347
|
+
verdicts. The two are indistinguishable by exit code alone β a gate that must
|
|
348
|
+
tell them apart should also check stderr (verdict trips print the JSON verdict
|
|
349
|
+
on stdout first; usage errors print click's usage text).
|
|
350
|
+
|
|
351
|
+
### Distinguishing what the graph doesn't know
|
|
352
|
+
|
|
353
|
+
- `check-access` verdicts include `AGENT_NOT_FOUND` (the agent string matches
|
|
354
|
+
no Agent node β typo or deleted agent) and `TOOL_NOT_FOUND`. The two are
|
|
355
|
+
not collapsed into `NOT_GRANTED`, so an operator iterating on names can
|
|
356
|
+
tell which case they're in. Every response carries `agent_found: bool`
|
|
357
|
+
and `found` for the tool β `True`/`False` when tool resolution ran, and
|
|
358
|
+
`null` on the `AGENT_NOT_FOUND` branch (the tool was not looked up).
|
|
359
|
+
Callers should use `result.get("found")` so the three-state shape doesn't
|
|
360
|
+
trip indexing.
|
|
361
|
+
- When `verdict: "DENY"`, the response also carries
|
|
362
|
+
`all_authorized: bool` β `true` when every `deny_evidence` row is
|
|
363
|
+
`authorized_but_governed` (an operator-declared exception). Lets a caller
|
|
364
|
+
detect "DENY only because of by-design grants" without aggregating per-row
|
|
365
|
+
classifications.
|
|
366
|
+
- `blast-radius` returns `found: false` when the node argument matches no
|
|
367
|
+
Resource or Policy. A typo'd URI is no longer indistinguishable from
|
|
368
|
+
`impacted: []`. `blast-radius` accepts a URI-shaped argument as either a
|
|
369
|
+
resource URI or a policy id β if the lexical URI pattern matches no
|
|
370
|
+
Resource node, it retries against Policy ids before declaring not-found
|
|
371
|
+
(so a policy id like `urn:secret` or `mailto:ops` works as a node).
|
|
372
|
+
- `unsafe-tools` (CLI) and the `unsafe_callable_tools` MCP tool both
|
|
373
|
+
default to `classification='violation'` only; pass `--all` (CLI) or
|
|
374
|
+
`include_authorized: true` (MCP) to see operator-declared exceptions
|
|
375
|
+
too. Both surfaces also carry `agent_found: bool` so a typo'd agent
|
|
376
|
+
doesn't look like an agent with zero unsafe tools.
|
|
377
|
+
|
|
378
|
+
### Result classification
|
|
379
|
+
|
|
380
|
+
`unsafe-tools` and `check-access` results carry a `classification`:
|
|
381
|
+
|
|
382
|
+
- `violation` β the agent reaches a DENY-governed resource and the operator
|
|
383
|
+
has not declared the reach intentional.
|
|
384
|
+
- `authorized_but_governed` β operator declared `expected_exceptions` in the
|
|
385
|
+
manifest covering this (agent, tool, resource?, policy); the row carries
|
|
386
|
+
the operator's stated `exception_reason`.
|
|
387
|
+
|
|
388
|
+
This replaces the bare "unsafe" framing: review tooling can filter to
|
|
389
|
+
`violation` rows and stop drowning reviewers in by-design grants.
|
|
390
|
+
|
|
391
|
+
### Provenance on every edge
|
|
392
|
+
|
|
393
|
+
Each edge β crawled or authored β carries `source` (`crawled` |
|
|
394
|
+
`operator_asserted` | `inferred`), `confidence` (`high` | `medium` | `low`),
|
|
395
|
+
and `evidence` (free-text pointer: URL, ticket id, commit SHA, source
|
|
396
|
+
location β prefer symbol anchors like `path/to/file.py::Class.method` over
|
|
397
|
+
`file:line`, which rots as the cited source evolves).
|
|
398
|
+
Queries surface this on every result so a reviewer can see whether a DENY
|
|
399
|
+
verdict rests on a crawled fact or an unverified assertion:
|
|
400
|
+
|
|
401
|
+
```yaml
|
|
402
|
+
data_access:
|
|
403
|
+
- tool: "memtomem-stm::langchain__search_docs_by_lang_chain"
|
|
404
|
+
resource: "memory:claude:toolgraph"
|
|
405
|
+
mode: READS
|
|
406
|
+
provenance:
|
|
407
|
+
source: inferred
|
|
408
|
+
confidence: high
|
|
409
|
+
evidence: "memtomem_stm/proxy/manager.py::ProxyManager._apply_surfacing -> memtomem_stm/surfacing/engine.py::SurfacingEngine.surface"
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`governed_by` bindings carry their own provenance β *why* this resource is
|
|
413
|
+
under this policy, which is often a different citation from the data-flow
|
|
414
|
+
edge. Bare-string entries are still accepted (no evidence, equivalent to
|
|
415
|
+
the old shape); add an object item to cite the binding's source:
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
governed_by:
|
|
419
|
+
"memory:claude:toolgraph":
|
|
420
|
+
- policy: operator-ltm-confidentiality
|
|
421
|
+
provenance:
|
|
422
|
+
source: operator_asserted
|
|
423
|
+
evidence: "~/.memtomem/config.json (namespace.rules)"
|
|
424
|
+
"file:///srv/app/.env": [secrets-deny] # bare string β no evidence
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
When a DENY fires via a resource path, the result row includes both
|
|
428
|
+
`provenance` (the data-flow edge that put the row in front of you) and
|
|
429
|
+
`policy_provenance` (the binding evidence, attached only when the author
|
|
430
|
+
cited one).
|
|
431
|
+
|
|
432
|
+
### Tool annotations are self-claims, not facts
|
|
433
|
+
|
|
434
|
+
The crawler stores MCP tool annotations (`readOnlyHint`, `destructiveHint`,
|
|
435
|
+
`idempotentHint`, `openWorldHint`) as Tool-node properties on the same crawl
|
|
436
|
+
pass that owns the node, and clears any hint the server stops sending
|
|
437
|
+
([ADR-0006](https://github.com/memtomem/toolgraph/blob/main/docs/adr/0006-crawl-tool-annotations-as-self-claims.md)).
|
|
438
|
+
|
|
439
|
+
Their provenance is `crawled` / **`medium`** β deliberately one tier below
|
|
440
|
+
EXPOSES/PROVIDES (`high`). "The server advertised this tool" is
|
|
441
|
+
protocol-observable; "this tool is read-only" is the server's *own unverified
|
|
442
|
+
claim about itself*, and a lying or sloppy server can hint anything. Letting
|
|
443
|
+
hints launder into the same confidence tier as protocol facts would defeat the
|
|
444
|
+
provenance model.
|
|
445
|
+
|
|
446
|
+
Annotations never auto-create READS/WRITES/GOVERNED_BY edges β they only feed
|
|
447
|
+
two audit queries that join self-claims against authored truth:
|
|
448
|
+
|
|
449
|
+
- `destructive-unsafeguarded` β tools hinting destructive with no
|
|
450
|
+
GOVERNED_BY path at all (direct or via any resource they touch). An
|
|
451
|
+
authoring priority queue at zero authoring cost, not a verdict.
|
|
452
|
+
- `annotation-contradictions` β authored WRITES edges on tools hinting
|
|
453
|
+
`readOnlyHint: true`. One side is wrong; each row cites both provenances
|
|
454
|
+
(the hint's crawl pass, the authored edge's evidence pointer) and the
|
|
455
|
+
operator picks the stronger evidence.
|
|
456
|
+
|
|
457
|
+
## Selector surface (deterministic, ADR-0005)
|
|
458
|
+
|
|
459
|
+
toolgraph can serve tool selection as a context-engineering layer: shrink a
|
|
460
|
+
candidate catalog deterministically *before* any relevance ranker sees it
|
|
461
|
+
([ADR-0005](https://github.com/memtomem/toolgraph/blob/main/docs/adr/0005-selector-surface-boundary.md), background in
|
|
462
|
+
[the report](https://github.com/memtomem/toolgraph/blob/main/docs/context-engineering-tool-selection-report.md)). Three
|
|
463
|
+
batch-first entry points (CLI + MCP) run a fixed number of queries
|
|
464
|
+
regardless of candidate count β per-candidate `check-access` calls scale
|
|
465
|
+
linearly, which is unacceptable on the online path:
|
|
466
|
+
|
|
467
|
+
- `rank-features AGENT CANDIDATESβ¦` β per-candidate facts in input order:
|
|
468
|
+
resolution (`found` / `ambiguous` / `tool_key`), grant, verdict, DENY
|
|
469
|
+
classification + evidence paths, drift, mapping, evidence coverage, the
|
|
470
|
+
four annotation self-claims, and a **rule-based** `risk_score` from this
|
|
471
|
+
fixed table (first match wins): `1.0` DENY violation Β· `0.8` drifted Β·
|
|
472
|
+
`0.6` unmapped Β· `0.4` unbacked load-bearing edge Β· `0.2`
|
|
473
|
+
authorized_but_governed Β· `0.0` otherwise.
|
|
474
|
+
- `eligible-tools AGENT CANDIDATESβ¦ --profile strict` β hard filter with
|
|
475
|
+
reject reasons and policy-evidence paths. Profiles are named rule sets,
|
|
476
|
+
not learned thresholds:
|
|
477
|
+
|
|
478
|
+
| Profile | Use | Rejects |
|
|
479
|
+
| --- | --- | --- |
|
|
480
|
+
| `strict` (default) | production agents | everything but a clean, fully-authored ALLOW: not-found / ambiguous / not-granted / any DENY / drifted / unmapped |
|
|
481
|
+
| `review` | human-in-the-loop | not-found / ambiguous / not-granted / DENY violations / drifted (operator exceptions and unmapped pass) |
|
|
482
|
+
| `explore` | offline eval / sandbox | not-found / ambiguous / not-granted / drifted only |
|
|
483
|
+
|
|
484
|
+
- `selection-explain AGENT TOOL` β compact human-readable reasons for one
|
|
485
|
+
decision, for operators and end users.
|
|
486
|
+
|
|
487
|
+
Hard boundary: no relevance scoring, no learning, ever β those belong to the
|
|
488
|
+
consumer. A learned model may rerank the eligible list but may **never
|
|
489
|
+
override a hard reject** (`NOT_GRANTED`, `violation`, drifted). Ambiguous
|
|
490
|
+
bare names are never auto-resolved β the consumer must re-submit the
|
|
491
|
+
server-qualified key. An unknown agent returns `agent_found: false`
|
|
492
|
+
(CLI: `AGENT_NOT_FOUND` on stderr, exit 1): selection should abort, because
|
|
493
|
+
a typo'd agent is a context-construction error, not an empty catalog.
|
|
494
|
+
|
|
495
|
+
MCP responses carry `graph_generation` (ADR-0004) so consumers can cache
|
|
496
|
+
features per graph state with one integer comparison.
|
|
497
|
+
|
|
498
|
+
If the graph backend is temporarily unavailable, every MCP tool returns
|
|
499
|
+
`isError: true` with a typed `structuredContent` envelope instead of exposing
|
|
500
|
+
driver-specific text:
|
|
501
|
+
|
|
502
|
+
```json
|
|
503
|
+
{
|
|
504
|
+
"error_kind": "backend_unavailable",
|
|
505
|
+
"retryable": true,
|
|
506
|
+
"message": "Toolgraph backend is temporarily unavailable; retry later."
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Only this exact discriminator is an availability signal. Validation,
|
|
511
|
+
configuration, query-contract, and unexpected failures keep the normal MCP
|
|
512
|
+
error behavior and should fail loud. Successful CLI output and Python result
|
|
513
|
+
shapes are unchanged; only the exception type surfaced on an outage differs
|
|
514
|
+
(see below).
|
|
515
|
+
|
|
516
|
+
Outages are typed once at the backend seam β `driver.session()` and
|
|
517
|
+
`verify_connectivity()` raise `BackendUnavailableError` β so the server layer
|
|
518
|
+
never inspects driver-specific exception types. `retryable` is read from the
|
|
519
|
+
annotations the server actually advertises for that tool, rather than assumed:
|
|
520
|
+
every current tool is a pure graph read. Anything else fails closed to
|
|
521
|
+
`retryable: false` β a tool that does not advertise `readOnlyHint`, an unknown
|
|
522
|
+
tool, and any case where the annotations cannot be read at all. Treat
|
|
523
|
+
`retryable: false` as "do not assume a retry is safe", not as proof that the
|
|
524
|
+
operation writes.
|
|
525
|
+
|
|
526
|
+
**API change for embedders.** Because outages are typed at that seam, code
|
|
527
|
+
calling `driver.session()` or `verify_connectivity()` directly now sees
|
|
528
|
+
`BackendUnavailableError` where it previously saw `neo4j.exceptions`
|
|
529
|
+
`ServiceUnavailable`, `SessionExpired`, `ConnectionAcquisitionTimeoutError`, or
|
|
530
|
+
`DatabaseUnavailable`. Catch `BackendUnavailableError` (or its
|
|
531
|
+
`BackendLockedError` subclass) instead; the original driver exception is
|
|
532
|
+
preserved as `__cause__`. Configuration, authentication, and Cypher/client
|
|
533
|
+
errors are untouched, as is the still-raw `get_driver()` escape hatch. Note
|
|
534
|
+
that `is_backend_unavailable()` narrowed with it: it classifies only
|
|
535
|
+
`BackendUnavailableError`, so exceptions raised straight out of `get_driver()`
|
|
536
|
+
no longer answer yes. Route retry-sensitive calls through `driver.session()`.
|
|
537
|
+
|
|
538
|
+
### Wiring the shipped consumer (memtomem-stm)
|
|
539
|
+
|
|
540
|
+
The first consumer of this surface is the memtomem-stm proxy
|
|
541
|
+
(memtomem-stm#465): at startup it consults `eligible_tools` +
|
|
542
|
+
`rank_features` over MCP stdio (`toolgraph serve`) and feeds the verdicts
|
|
543
|
+
into its tool-exposure hard filter. The gate exhibit carries the consumer's
|
|
544
|
+
default `agent_id` (`stm-proxy`) as a wiring fixture, so the consult path
|
|
545
|
+
is demoable against the exhibit graph:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
uv run toolgraph eligible-tools stm-proxy \
|
|
549
|
+
"memtomem-stm::langchain__search_docs_by_lang_chain" --profile strict
|
|
550
|
+
# -> rejected: DENY_GOVERNED (fail closed β the consumer's production default)
|
|
551
|
+
uv run toolgraph eligible-tools stm-proxy \
|
|
552
|
+
"memtomem-stm::langchain__search_docs_by_lang_chain" --profile review
|
|
553
|
+
# -> eligible (the operator-declared exception passes under review)
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Live wiring differs from the exhibit in one important way: the proxy builds
|
|
557
|
+
candidate refs as `"<upstream-connection-name>::<raw-tool-name>"` from its
|
|
558
|
+
own upstream config, while the exhibit crawls the proxy itself (server
|
|
559
|
+
`memtomem-stm`, prefixed tool names like `langchain__β¦`). For a live
|
|
560
|
+
deployment, crawl the proxy's *upstreams* directly so graph server names
|
|
561
|
+
match the STM connection keys β or map them in the consumer's config:
|
|
562
|
+
|
|
563
|
+
```json
|
|
564
|
+
"toolgraph": {
|
|
565
|
+
"enabled": true,
|
|
566
|
+
"agent_id": "stm-proxy",
|
|
567
|
+
"query_profile": "strict",
|
|
568
|
+
"server_name_map": { "docs-langchain": "langchain-docs" }
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
(`"docs-langchain"` is the STM upstream connection key; `"langchain-docs"`
|
|
573
|
+
is the name that upstream was crawled under in this graph.) Refs that don't
|
|
574
|
+
resolve come back `TOOL_NOT_FOUND` and are handled by the consumer's
|
|
575
|
+
`on_tool_not_found` knob β if every candidate is rejected as not-found, the
|
|
576
|
+
name mapping (not governance) is what's wrong.
|
|
577
|
+
|
|
578
|
+
## toolgraph as an MCP server
|
|
579
|
+
|
|
580
|
+
toolgraph exposes its governance and audit queries as MCP tools, so an agent
|
|
581
|
+
can ask the registry about itself over the same protocol toolgraph crawls:
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
uv run toolgraph serve # stdio
|
|
585
|
+
uv run toolgraph serve --http # streamable-http
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Tools: `check_access`, `unsafe_callable_tools`, `blast_radius`,
|
|
589
|
+
`unmapped_tools`, `orphan_policies`, `unbacked_edges`, `drifted_tools`,
|
|
590
|
+
`destructive_unsafeguarded`, `annotation_contradictions`, `audit_report`,
|
|
591
|
+
`rank_features`, `eligible_tools`, `selection_explain`.
|
|
592
|
+
|
|
593
|
+
All thirteen are pure graph reads and advertise `readOnlyHint` /
|
|
594
|
+
`idempotentHint`.
|
|
595
|
+
|
|
596
|
+
## Inputs
|
|
597
|
+
|
|
598
|
+
- `servers.yaml` β MCP servers to crawl (stdio `command`/`args`, or http `url`).
|
|
599
|
+
- `governance.yaml` β authored agents, policies, `CAN_CALL` grants, `READS/WRITES`
|
|
600
|
+
data access, `GOVERNED_BY` mappings, and `expected_exceptions` for intentional
|
|
601
|
+
DENY-zone reach. Per-edge `provenance` is optional everywhere. See `examples/`.
|
|
602
|
+
|
|
603
|
+
Two reference exhibits:
|
|
604
|
+
|
|
605
|
+
- `examples/servers-gate.yaml` + `governance-gate.yaml` β the 2-server
|
|
606
|
+
wedge demo (memtomem + memtomem-stm). Proves toolgraph composes
|
|
607
|
+
cross-server reach a vector registry of tool descriptions cannot.
|
|
608
|
+
- `examples/servers-fleet.yaml` + `governance-fleet.yaml` β a 7-server
|
|
609
|
+
fleet (the two above + filesystem, fetch, memory, sequential-thinking,
|
|
610
|
+
time) with minimal but real governance. Validates that per-server
|
|
611
|
+
authoring is minute-scale, not hour-scale. (Caveat: the original
|
|
612
|
+
single-pass took ~48s because the author had already read the
|
|
613
|
+
surfacing-engine source for the gate exhibit; an unfamiliar operator on
|
|
614
|
+
unfamiliar servers should plan for 2β5 minutes per server.)
|
|
615
|
+
|
|
616
|
+
## Layout
|
|
617
|
+
|
|
618
|
+
```
|
|
619
|
+
toolgraph/
|
|
620
|
+
crawler/ connect to MCP servers (stdio + streamable-http), enumerate tools/resources
|
|
621
|
+
graph/ Neo4j driver, schema/constraints, idempotent loader, governance/audit queries
|
|
622
|
+
manifest/ parse + ingest authored governance (idempotent)
|
|
623
|
+
server/ FastMCP server exposing the queries as MCP tools
|
|
624
|
+
cli.py crawl / ingest-manifest / check-access / unsafe-tools / blast-radius /
|
|
625
|
+
unmapped-tools / orphan-policies / unbacked-edges / drift /
|
|
626
|
+
destructive-unsafeguarded / annotation-contradictions /
|
|
627
|
+
rank-features / eligible-tools / selection-explain / serve / reset
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
## Tests
|
|
631
|
+
|
|
632
|
+
```bash
|
|
633
|
+
uv run pytest # uses a real Neo4j via testcontainers (no Cypher mocking)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## Scope (MVP)
|
|
637
|
+
|
|
638
|
+
This is a deliberately thin, thesis-validating MVP. **In:** factual crawl +
|
|
639
|
+
authored governance + three positive-reachability queries + four
|
|
640
|
+
negative-truth/drift queries + classification + per-edge provenance
|
|
641
|
+
(including per-`governed_by`-binding) + exception annotations + `found`
|
|
642
|
+
flags on `check-access` and `blast-radius` so unknown nodes are
|
|
643
|
+
distinguishable from empty results.
|
|
644
|
+
|
|
645
|
+
Idempotency is fully declarative: `ingest-manifest` re-applies authored
|
|
646
|
+
edges (CAN_CALL/READS/WRITES/GOVERNED_BY) AND prunes Agent + Policy nodes
|
|
647
|
+
the manifest no longer mentions, so renaming a policy in YAML does not
|
|
648
|
+
leave the old Policy node behind. Stale `prov_*` properties on Policy
|
|
649
|
+
nodes from earlier shapes are also cleaned at re-ingest.
|
|
650
|
+
|
|
651
|
+
Expected-exception validation enforces a full reachability invariant β
|
|
652
|
+
the operator can't declare an exception that fails to classify any row
|
|
653
|
+
(typo'd agent, no CAN_CALL grant, resource not governed by the policy,
|
|
654
|
+
or a wildcard exception with no toolβpolicy path are all caught at
|
|
655
|
+
ingest with a clear warning).
|
|
656
|
+
|
|
657
|
+
**Still deferred** (no plan to start until use proves them out):
|
|
658
|
+
LLM-inferred dependency/version edges, vector search, an official-registry
|
|
659
|
+
importer, and runtime enforcement. toolgraph remains an analyzer; the
|
|
660
|
+
runtime-enforcement wedge belongs to gateways like Kong.
|
|
661
|
+
|
|
662
|
+
## Contributing, security, and license
|
|
663
|
+
|
|
664
|
+
- **Contributing:** [CONTRIBUTING.md](CONTRIBUTING.md) β development setup,
|
|
665
|
+
pull-request conventions, and the Contributor License Agreement.
|
|
666
|
+
- **CLA:** [CLA.md](CLA.md). Signing is one-time per GitHub account per
|
|
667
|
+
repository; the workflow comments on your first pull request with
|
|
668
|
+
instructions.
|
|
669
|
+
- **Code of conduct:** [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) β applies to
|
|
670
|
+
issues, pull requests and discussions.
|
|
671
|
+
- **Security:** [SECURITY.md](SECURITY.md) β please use private vulnerability
|
|
672
|
+
reporting rather than a public issue.
|
|
673
|
+
- **License:** Apache License 2.0 β see [LICENSE](LICENSE).
|
|
674
|
+
Copyright 2025-2026 DAPADA Inc. and memtomem contributors.
|