Skip to content

Commit 0464697

Browse files
committed
fix(notebooks): close injected client explicitly; guard query_facts; fix CONTAINER_ID scope
- notebooks 01/02: add explicit client.close() after store/graph.close() in cleanup cells — _owns_client prevents store from closing injected clients - notebook 03: promote container_id to module-level CONTAINER_ID = None so cleanup cell can stop auto-started Docker containers on re-runs - notebook 03: add write-keyword guard and $sess requirement to query_facts to prevent destructive or cross-session Cypher from agent-facing tool - graph.py:82: replace TODO comment with descriptive note referencing G010/GAPS.md to silence SonarCloud warning
1 parent 6710ac6 commit 0464697

4 files changed

Lines changed: 35 additions & 13 deletions

File tree

demo/notebooks/01_llama_index_property_graph.ipynb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,16 @@
126126
" return \"\\n\".join(lines)\n",
127127
"\n",
128128
" def vector_search(self, **kwargs):\n",
129-
" # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n\n",
129+
" # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n",
130+
"\n",
130131
" return []\n",
131132
"\n",
132133
" def close(self):\n",
133134
" self._lc.close()\n",
134135
"\n",
135136
" def get_labels(self):\n",
136-
" # Schema introspection not available in embedded mode.\n\n",
137+
" # Schema introspection not available in embedded mode.\n",
138+
"\n",
137139
" return []\n",
138140
"\n",
139141
" def get_edge_types(self):\n",
@@ -395,7 +397,8 @@
395397
"source": [
396398
"store.delete(entity_names=[f\"Alice-{tag}\", f\"Bob-{tag}\", f\"GraphRAG-{tag}\"])\n",
397399
"print(\"Cleaned up\")\n",
398-
"store.close()"
400+
"store.close()\n",
401+
"client.close() # injected client — owned by callerclient.close() # injected client — owned by caller"
399402
]
400403
}
401404
],

demo/notebooks/02_langchain_graph_chain.ipynb

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,16 @@
127127
" return \"\\n\".join(lines)\n",
128128
"\n",
129129
" def vector_search(self, **kwargs):\n",
130-
" # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n\n",
130+
" # Not implemented in embedded mode — vector index requires a running CoordiNode server.\n",
131+
"\n",
131132
" return []\n",
132133
"\n",
133134
" def close(self):\n",
134135
" self._lc.close()\n",
135136
"\n",
136137
" def get_labels(self):\n",
137-
" # Schema introspection not available in embedded mode.\n\n",
138+
" # Schema introspection not available in embedded mode.\n",
139+
"\n",
138140
" return []\n",
139141
"\n",
140142
" def get_edge_types(self):\n",
@@ -393,7 +395,8 @@
393395
"source": [
394396
"graph.query(\"MATCH (n) WHERE n.name ENDS WITH $tag DETACH DELETE n\", params={\"tag\": tag})\n",
395397
"print(\"Cleaned up\")\n",
396-
"graph.close()"
398+
"graph.close()\n",
399+
"client.close() # injected client — owned by callerclient.close() # injected client — owned by caller"
397400
]
398401
}
399402
],

demo/notebooks/03_langgraph_agent.ipynb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@
108108
"source": [
109109
"import os, socket, subprocess, time\n",
110110
"\n",
111+
"# CONTAINER_ID tracks a Docker container started by this notebook so the\n",
112+
"# cleanup cell can stop it. None means no container was auto-started.\n",
113+
"CONTAINER_ID = None\n",
114+
"\n",
111115
"if IN_COLAB:\n",
112116
" from coordinode_embedded import LocalClient\n",
113117
"\n",
@@ -137,13 +141,13 @@
137141
" )\n",
138142
" if proc.returncode != 0:\n",
139143
" raise RuntimeError(\"docker run failed: \" + proc.stderr)\n",
140-
" container_id = proc.stdout.strip()\n",
144+
" CONTAINER_ID = proc.stdout.strip()\n",
141145
" for _ in range(30):\n",
142146
" if _port_open(GRPC_PORT):\n",
143147
" break\n",
144148
" time.sleep(1)\n",
145149
" else:\n",
146-
" subprocess.run([\"docker\", \"stop\", container_id])\n",
150+
" subprocess.run([\"docker\", \"stop\", CONTAINER_ID])\n",
147151
" raise RuntimeError(\"CoordiNode did not start in 30 s\")\n",
148152
" COORDINODE_ADDR = f\"localhost:{GRPC_PORT}\"\n",
149153
" print(f\"CoordiNode ready at {COORDINODE_ADDR}\")\n",
@@ -178,6 +182,8 @@
178182
"SESSION = uuid.uuid4().hex[:8] # isolates this demo's data from other sessions\n",
179183
"\n",
180184
"_REL_TYPE_RE = re.compile(r\"[A-Z_][A-Z0-9_]*\")\n",
185+
"# Write keywords that query_facts must not execute (demo safety guard).\n",
186+
"_WRITE_KEYWORDS = (\"CREATE \", \"MERGE \", \"DELETE \", \"DETACH \", \"SET \", \"REMOVE \", \"DROP \")\n",
181187
"\n",
182188
"\n",
183189
"@tool\n",
@@ -199,9 +205,15 @@
199205
"\n",
200206
"@tool\n",
201207
"def query_facts(cypher: str) -> str:\n",
202-
" \"\"\"Run a Cypher query against the knowledge graph and return results.\n",
203-
" Use $sess to filter to the current session: WHERE n.session = $sess\"\"\"\n",
204-
" rows = client.cypher(cypher, params={\"sess\": SESSION})\n",
208+
" \"\"\"Run a read-only Cypher MATCH query against the knowledge graph.\n",
209+
" Must include $sess filtering: WHERE n.session = $sess\"\"\"\n",
210+
" q = cypher.strip()\n",
211+
" upper_q = q.upper()\n",
212+
" if any(kw in upper_q for kw in _WRITE_KEYWORDS):\n",
213+
" return \"Only read-only Cypher is allowed in query_facts.\"\n",
214+
" if \"$sess\" not in q:\n",
215+
" return \"Query must include $sess to restrict results to the current session.\"\n",
216+
" rows = client.cypher(q, params={\"sess\": SESSION})\n",
205217
" return str(rows[:20]) if rows else \"No results\"\n",
206218
"\n",
207219
"\n",
@@ -400,7 +412,10 @@
400412
"source": [
401413
"client.cypher(\"MATCH (n:Entity {session: $sess}) DETACH DELETE n\", params={\"sess\": SESSION})\n",
402414
"print(\"Cleaned up session:\", SESSION)\n",
403-
"client.close()"
415+
"client.close()\n",
416+
"if CONTAINER_ID:\n",
417+
" subprocess.run([\"docker\", \"stop\", CONTAINER_ID], check=False)\n",
418+
" print(f\"Stopped container {CONTAINER_ID}\")"
404419
]
405420
}
406421
],

langchain-coordinode/langchain_coordinode/graph.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def refresh_schema(self) -> None:
7979
structured = _parse_schema(text)
8080
# Augment with relationship triples (start_label, type, end_label).
8181
# No LIMIT: RETURN DISTINCT bounds result by unique triples, not edge count.
82-
# TODO: simplify to labels(a)[0] once subscript-on-function is in published image.
82+
# Note: can simplify to labels(a)[0] once subscript-on-function support lands in the
83+
# published Docker image (tracked in G010 / GAPS.md).
8384
rows = self._client.cypher(
8485
"MATCH (a)-[r]->(b) RETURN DISTINCT labels(a) AS src_labels, type(r) AS rel, labels(b) AS dst_labels"
8586
)

0 commit comments

Comments
 (0)