From 8a8c62bc5fd26089886c7cfca7a3c1af18aae953 Mon Sep 17 00:00:00 2001 From: antejavor Date: Tue, 5 May 2026 13:43:29 +0200 Subject: [PATCH 1/2] Improve codex setup. --- .gitignore | 1 + context-graph/agent-context-graph/README.md | 84 ++++++++++++--- .../agent-context-graph/pyproject.toml | 4 +- .../src/agent_context_graph/cli.py | 6 ++ .../src/agent_context_graph/hooks/cli.py | 69 +++++++++++- .../tests/test_hook_cli.py | 100 ++++++++++++++++++ .../src/memgraph_toolbox/api/memgraph.py | 53 +++++++++- .../tests/test_memgraph_config.py | 40 +++++++ uv.lock | 4 + 9 files changed, 342 insertions(+), 19 deletions(-) create mode 100644 memgraph-toolbox/src/memgraph_toolbox/tests/test_memgraph_config.py diff --git a/.gitignore b/.gitignore index f3ee7667..83dce8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,7 @@ cython_debug/ # Local agent hook configuration /.codex/ /.claude/settings.local.json +/.private-specs/ # Project specific files /enterprise-context/sic-agent/sic-scrapper/output/* diff --git a/context-graph/agent-context-graph/README.md b/context-graph/agent-context-graph/README.md index 7176a425..ef025791 100644 --- a/context-graph/agent-context-graph/README.md +++ b/context-graph/agent-context-graph/README.md @@ -138,6 +138,78 @@ Planned: Codex hook configuration is local environment wiring, so this repository ignores `.codex/`. Each developer should create their own local `.codex` files or use a user-level Codex config. +Prerequisites: + +- Memgraph running and reachable over Bolt. Defaults are `bolt://localhost:7687`, empty user/password, and database `memgraph`. +- A Python environment that contains `agent-context-graph` and `skills-graph[agent-context-graph]`. +- Codex CLI or IDE extension running in a project that trusts the project-local `.codex/` layer. + +The streamlined setup only needs two pieces of local information: + +- where to write the Codex project config, usually your repo root +- where Memgraph is, plus optional auth/database values + +If Memgraph is running locally with defaults: + +```bash +agent-context-graph setup codex --project-dir "$PWD" --setup-schema +``` + +`--setup-schema` connects to Memgraph immediately and runs `SkillGraph().setup()`. + +If you need non-default Memgraph connection values: + +```bash +agent-context-graph setup codex \ + --project-dir /path/to/your/repo \ + --memgraph-url bolt://localhost:7687 \ + --memgraph-user "" \ + --memgraph-password "" \ + --memgraph-database memgraph \ + --setup-schema +``` + +The `--memgraph-*` options are used for `--setup-schema`, but they are not written into `.codex/hooks.json`. + +For source development in this workspace: + +```bash +uv run --package skills-graph --extra agent-context-graph \ + python -m agent_context_graph.cli setup codex \ + --project-dir /path/to/your/repo \ + --memgraph-url bolt://localhost:7687 \ + --setup-schema +``` + +The command writes local, ignored files: + +```text +.codex/config.toml +.codex/hooks.json +``` + +It refuses to overwrite existing generated files unless you pass `--force`. + +The generated hook command does not embed any Memgraph connection values. At runtime, Codex must run with the needed `MEMGRAPH_*` variables in its process environment, or the hooks will use `memgraph-toolbox` defaults. + +If Memgraph requires a password, provide `MEMGRAPH_PASSWORD` to the Codex process environment. `.codex/hooks.json` should not contain Memgraph credentials. + +Keep the Python environment used by the generated hook command around. Codex will run that absolute command path for every hook event. + +To smoke test the generated command, copy the `"command"` value from `.codex/hooks.json` and run: + +```bash +printf '{"hook_event_name":"Stop","session_id":"test"}' | COMMAND +``` + +The expected output is: + +```json +{"continue": true} +``` + +If you prefer manual setup: + 1. Make `skills-graph` able to reach Memgraph, then initialize and seed your skill graph once: ```bash @@ -242,18 +314,6 @@ The resulting `.codex/hooks.json` has this shape: The adapter records Codex `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PermissionRequest`, and `Stop` payloads. MCP tool names such as `mcp__skills__get_skill` are normalized by `skills-graph` to the underlying `get_skill` operation. -Smoke test the command: - -```bash -printf '{"hook_event_name":"Stop","session_id":"test"}' | COMMAND -``` - -The expected output is: - -```json -{"continue": true} -``` - ### Multiple Graph Components ```python diff --git a/context-graph/agent-context-graph/pyproject.toml b/context-graph/agent-context-graph/pyproject.toml index 6d6e41fc..5d1fb5bd 100644 --- a/context-graph/agent-context-graph/pyproject.toml +++ b/context-graph/agent-context-graph/pyproject.toml @@ -11,7 +11,9 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", ] -dependencies = [] +dependencies = [ + "memgraph-toolbox", +] [project.scripts] agent-context-graph = "agent_context_graph.cli:main" diff --git a/context-graph/agent-context-graph/src/agent_context_graph/cli.py b/context-graph/agent-context-graph/src/agent_context_graph/cli.py index 0e326c3f..155f166a 100644 --- a/context-graph/agent-context-graph/src/agent_context_graph/cli.py +++ b/context-graph/agent-context-graph/src/agent_context_graph/cli.py @@ -11,6 +11,7 @@ _HELP = """usage: agent-context-graph [options] Commands: + setup Configure an agent runtime. hook Configure or run command hooks. """ @@ -31,6 +32,11 @@ def main(argv: Sequence[str] | None = None) -> int: return hook_main(args[1:]) + if command == "setup": + from agent_context_graph.hooks.cli import main as hook_main + + return hook_main(["init", *args[1:]]) + print(f"Unknown command: {command}", file=sys.stderr) print(_HELP) return 2 diff --git a/context-graph/agent-context-graph/src/agent_context_graph/hooks/cli.py b/context-graph/agent-context-graph/src/agent_context_graph/hooks/cli.py index c104c977..652063c4 100644 --- a/context-graph/agent-context-graph/src/agent_context_graph/hooks/cli.py +++ b/context-graph/agent-context-graph/src/agent_context_graph/hooks/cli.py @@ -4,12 +4,15 @@ import argparse import json +import os import shlex import shutil import sys from pathlib import Path from typing import TYPE_CHECKING +from memgraph_toolbox.api.memgraph import MEMGRAPH_ENV_KEYS, memgraph_env + if TYPE_CHECKING: from collections.abc import Sequence @@ -105,6 +108,31 @@ def _init_codex(argv: list[str]) -> int: default=None, help="Full command to place in hooks.json. Defaults to this installed CLI.", ) + parser.add_argument( + "--memgraph-url", + default=None, + help="Memgraph Bolt URL for the hook command. Defaults to MEMGRAPH_URL or bolt://localhost:7687.", + ) + parser.add_argument( + "--memgraph-user", + default=None, + help="Memgraph username for the hook command. Defaults to MEMGRAPH_USER or empty.", + ) + parser.add_argument( + "--memgraph-password", + default=None, + help="Memgraph password for --setup-schema. Never written to hooks.json.", + ) + parser.add_argument( + "--memgraph-database", + default=None, + help="Memgraph database for the hook command. Defaults to MEMGRAPH_DATABASE or memgraph.", + ) + parser.add_argument( + "--setup-schema", + action="store_true", + help="Connect to Memgraph now and initialize the skills-graph schema.", + ) parser.add_argument( "--timeout", type=int, @@ -133,6 +161,7 @@ def _init_codex(argv: list[str]) -> int: from agent_context_graph.adapters.codex import build_hooks_config + memgraph_values = _memgraph_env_from_args(args) hook_command = args.hook_command or _default_hook_command("codex", connectors) codex_dir.mkdir(parents=True, exist_ok=True) config_path.write_text("[features]\ncodex_hooks = true\n", encoding="utf-8") @@ -140,10 +169,14 @@ def _init_codex(argv: list[str]) -> int: json.dumps({"hooks": build_hooks_config(hook_command, timeout=args.timeout)}, indent=2) + "\n", encoding="utf-8", ) + if args.setup_schema: + _setup_skills_graph_schema(memgraph_values) print(f"Wrote {config_path}") print(f"Wrote {hooks_path}") - print(f"Hook command: {hook_command}") + print(f"Memgraph URL: {memgraph_values['MEMGRAPH_URL']}") + print(f"Memgraph database: {memgraph_values['MEMGRAPH_DATABASE']}") + print(f"Hook command: {_mask_secret(hook_command, memgraph_values['MEMGRAPH_PASSWORD'])}") return 0 @@ -156,5 +189,39 @@ def _default_hook_command(runtime: str, connectors: list[str]) -> str: return shlex.join(args) +def _memgraph_env_from_args(args: argparse.Namespace) -> dict[str, str]: + return memgraph_env( + url=args.memgraph_url, + username=args.memgraph_user, + password=args.memgraph_password, + database=args.memgraph_database, + ) + + +def _setup_skills_graph_schema(memgraph_env: dict[str, str]) -> None: + try: + from skills_graph import SkillGraph + except ImportError as exc: + msg = "skills-graph is required to initialize the skills-graph schema" + raise ImportError(msg) from exc + + previous = {key: os.environ.get(key) for key in MEMGRAPH_ENV_KEYS} + os.environ.update(memgraph_env) + try: + SkillGraph().setup() + finally: + for key, value in previous.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + +def _mask_secret(value: str, secret: str) -> str: + if not secret: + return value + return value.replace(shlex.quote(secret), "'****'").replace(secret, "****") + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/context-graph/agent-context-graph/tests/test_hook_cli.py b/context-graph/agent-context-graph/tests/test_hook_cli.py index c6f72ca1..b019e271 100644 --- a/context-graph/agent-context-graph/tests/test_hook_cli.py +++ b/context-graph/agent-context-graph/tests/test_hook_cli.py @@ -2,6 +2,7 @@ import io import json +import os from agent_context_graph.cli import main as top_level_main from agent_context_graph.hooks.cli import main @@ -23,6 +24,14 @@ def test_top_level_cli_dispatches_hook_run(monkeypatch, capsys): assert capsys.readouterr().out.strip() == '{"continue": true}' +def test_top_level_cli_setup_aliases_codex_init(tmp_path, monkeypatch): + monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph") + + assert top_level_main(["setup", "codex", "--project-dir", str(tmp_path)]) == 0 + + assert (tmp_path / ".codex" / "config.toml").read_text() == "[features]\ncodex_hooks = true\n" + + def test_generic_cli_requires_runtime(capsys): assert main([]) == 2 @@ -54,6 +63,97 @@ def test_init_codex_writes_private_config(tmp_path, capsys): assert "Wrote" in capsys.readouterr().out +def test_init_codex_does_not_bake_memgraph_connection_into_hook_command(tmp_path, monkeypatch, capsys): + monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph") + + assert ( + main( + [ + "init", + "codex", + "--project-dir", + str(tmp_path), + "--memgraph-url", + "bolt://memgraph.example:7687", + "--memgraph-user", + "neo", + "--memgraph-password", + "secret", + "--memgraph-database", + "skills", + ] + ) + == 0 + ) + + hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text()) + command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + assert command == "/bin/agent-context-graph hook run codex --connector skills-graph" + assert "MEMGRAPH_URL" not in command + assert "MEMGRAPH_USER" not in command + assert "MEMGRAPH_PASSWORD" not in command + assert "MEMGRAPH_DATABASE" not in command + assert "secret" not in capsys.readouterr().out + + +def test_init_codex_uses_memgraph_env_only_for_setup_schema(tmp_path, monkeypatch): + monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph") + captured = {} + + class _SkillGraph: + def setup(self): + captured["url"] = os.environ.get("MEMGRAPH_URL") + captured["user"] = os.environ.get("MEMGRAPH_USER") + captured["password"] = os.environ.get("MEMGRAPH_PASSWORD") + captured["database"] = os.environ.get("MEMGRAPH_DATABASE") + + monkeypatch.setattr("skills_graph.SkillGraph", _SkillGraph) + + assert ( + main( + [ + "init", + "codex", + "--project-dir", + str(tmp_path), + "--memgraph-url", + "bolt://memgraph.example:7687", + "--memgraph-user", + "neo", + "--memgraph-password", + "secret", + "--memgraph-database", + "skills", + "--setup-schema", + ] + ) + == 0 + ) + + hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text()) + command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + assert "MEMGRAPH" not in command + assert captured == { + "url": "bolt://memgraph.example:7687", + "user": "neo", + "password": "secret", + "database": "skills", + } + + +def test_init_codex_uses_memgraph_toolbox_env_helper(tmp_path, monkeypatch): + monkeypatch.setattr("agent_context_graph.hooks.cli.shutil.which", lambda _: "/bin/agent-context-graph") + monkeypatch.setenv("MEMGRAPH_URL", "bolt://env-memgraph:7687") + monkeypatch.setenv("MEMGRAPH_DATABASE", "env-skills") + + assert main(["init", "codex", "--project-dir", str(tmp_path)]) == 0 + + hooks = json.loads((tmp_path / ".codex" / "hooks.json").read_text()) + command = hooks["hooks"]["PreToolUse"][0]["hooks"][0]["command"] + assert "MEMGRAPH_URL" not in command + assert "MEMGRAPH_DATABASE" not in command + + def test_init_codex_refuses_to_overwrite_without_force(tmp_path, capsys): codex_dir = tmp_path / ".codex" codex_dir.mkdir() diff --git a/memgraph-toolbox/src/memgraph_toolbox/api/memgraph.py b/memgraph-toolbox/src/memgraph_toolbox/api/memgraph.py index 615dee82..10b9bacc 100644 --- a/memgraph-toolbox/src/memgraph_toolbox/api/memgraph.py +++ b/memgraph-toolbox/src/memgraph_toolbox/api/memgraph.py @@ -1,10 +1,52 @@ import os +from collections.abc import Mapping from typing import Any from neo4j import GraphDatabase from ..utils.serialization import serialize_record_data +MEMGRAPH_ENV_DEFAULTS = { + "MEMGRAPH_URL": "bolt://localhost:7687", + "MEMGRAPH_USER": "", + "MEMGRAPH_PASSWORD": "", + "MEMGRAPH_DATABASE": "memgraph", +} +MEMGRAPH_ENV_KEYS = tuple(MEMGRAPH_ENV_DEFAULTS) + + +def memgraph_env( + *, + url: str | None = None, + username: str | None = None, + password: str | None = None, + database: str | None = None, + environ: Mapping[str, str] | None = None, +) -> dict[str, str]: + """Return canonical Memgraph connection environment values. + + Explicit values win, then environment variables, then Memgraph defaults. + The returned keys are suitable for passing through command hooks or other + subprocess boundaries. + """ + env = os.environ if environ is None else environ + return { + "MEMGRAPH_URL": url if url is not None else env.get("MEMGRAPH_URL", MEMGRAPH_ENV_DEFAULTS["MEMGRAPH_URL"]), + "MEMGRAPH_USER": ( + username if username is not None else env.get("MEMGRAPH_USER", MEMGRAPH_ENV_DEFAULTS["MEMGRAPH_USER"]) + ), + "MEMGRAPH_PASSWORD": ( + password + if password is not None + else env.get("MEMGRAPH_PASSWORD", MEMGRAPH_ENV_DEFAULTS["MEMGRAPH_PASSWORD"]) + ), + "MEMGRAPH_DATABASE": ( + database + if database is not None + else env.get("MEMGRAPH_DATABASE", MEMGRAPH_ENV_DEFAULTS["MEMGRAPH_DATABASE"]) + ), + } + class Memgraph: """ @@ -40,17 +82,18 @@ def __init__( user_agent: Client name sent to the server (e.g. "mcp-memgraph", "langchain-memgraph", "sql2graph", etc.) """ - # Load from environment variables with fallbacks - url = url or os.environ.get("MEMGRAPH_URL", "bolt://localhost:7687") - username = username or os.environ.get("MEMGRAPH_USER", "") - password = password or os.environ.get("MEMGRAPH_PASSWORD", "") + # Load from the shared Memgraph environment contract with fallbacks. + env = memgraph_env(url=url, username=username, password=password, database=database) + url = env["MEMGRAPH_URL"] + username = env["MEMGRAPH_USER"] + password = env["MEMGRAPH_PASSWORD"] config = dict(driver_config or {}) config.setdefault("user_agent", user_agent or self.DEFAULT_USER_AGENT) self.driver = GraphDatabase.driver(url, auth=(username, password), **config) - self.database = database or os.environ.get("MEMGRAPH_DATABASE", "memgraph") + self.database = env["MEMGRAPH_DATABASE"] try: import neo4j diff --git a/memgraph-toolbox/src/memgraph_toolbox/tests/test_memgraph_config.py b/memgraph-toolbox/src/memgraph_toolbox/tests/test_memgraph_config.py new file mode 100644 index 00000000..0719d388 --- /dev/null +++ b/memgraph-toolbox/src/memgraph_toolbox/tests/test_memgraph_config.py @@ -0,0 +1,40 @@ +from memgraph_toolbox.api.memgraph import MEMGRAPH_ENV_DEFAULTS, MEMGRAPH_ENV_KEYS, memgraph_env + + +def test_memgraph_env_uses_defaults_without_environment(): + assert memgraph_env(environ={}) == MEMGRAPH_ENV_DEFAULTS + assert tuple(MEMGRAPH_ENV_DEFAULTS) == MEMGRAPH_ENV_KEYS + + +def test_memgraph_env_prefers_explicit_values_over_environment(): + env = { + "MEMGRAPH_URL": "bolt://env:7687", + "MEMGRAPH_USER": "env-user", + "MEMGRAPH_PASSWORD": "env-password", + "MEMGRAPH_DATABASE": "env-db", + } + + assert memgraph_env( + url="bolt://arg:7687", + username="arg-user", + password="arg-password", + database="arg-db", + environ=env, + ) == { + "MEMGRAPH_URL": "bolt://arg:7687", + "MEMGRAPH_USER": "arg-user", + "MEMGRAPH_PASSWORD": "arg-password", + "MEMGRAPH_DATABASE": "arg-db", + } + + +def test_memgraph_env_allows_empty_explicit_auth_values(): + env = { + "MEMGRAPH_USER": "env-user", + "MEMGRAPH_PASSWORD": "env-password", + } + + values = memgraph_env(username="", password="", environ=env) + + assert values["MEMGRAPH_USER"] == "" + assert values["MEMGRAPH_PASSWORD"] == "" diff --git a/uv.lock b/uv.lock index b4111790..eaada3bd 100644 --- a/uv.lock +++ b/uv.lock @@ -181,6 +181,9 @@ wheels = [ name = "agent-context-graph" version = "0.1.0" source = { editable = "context-graph/agent-context-graph" } +dependencies = [ + { name = "memgraph-toolbox" }, +] [package.optional-dependencies] claude = [ @@ -197,6 +200,7 @@ test = [ [package.metadata] requires-dist = [ { name = "claude-agent-sdk", marker = "extra == 'claude'", specifier = ">=0.1.0" }, + { name = "memgraph-toolbox", editable = "memgraph-toolbox" }, { name = "openai-agents", marker = "extra == 'openai'", specifier = ">=0.1.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.3" }, { name = "pytest-asyncio", marker = "extra == 'test'", specifier = ">=0.24.0" }, From f52a21c03b800760d014a1b45445d5f082f4790c Mon Sep 17 00:00:00 2001 From: antejavor Date: Tue, 5 May 2026 14:03:47 +0200 Subject: [PATCH 2/2] Update test hook cli. --- context-graph/agent-context-graph/tests/test_hook_cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/context-graph/agent-context-graph/tests/test_hook_cli.py b/context-graph/agent-context-graph/tests/test_hook_cli.py index b019e271..cfeac72a 100644 --- a/context-graph/agent-context-graph/tests/test_hook_cli.py +++ b/context-graph/agent-context-graph/tests/test_hook_cli.py @@ -3,6 +3,8 @@ import io import json import os +import sys +from types import ModuleType from agent_context_graph.cli import main as top_level_main from agent_context_graph.hooks.cli import main @@ -107,7 +109,9 @@ def setup(self): captured["password"] = os.environ.get("MEMGRAPH_PASSWORD") captured["database"] = os.environ.get("MEMGRAPH_DATABASE") - monkeypatch.setattr("skills_graph.SkillGraph", _SkillGraph) + fake_skills_graph = ModuleType("skills_graph") + fake_skills_graph.SkillGraph = _SkillGraph + monkeypatch.setitem(sys.modules, "skills_graph", fake_skills_graph) assert ( main(