Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 23 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,15 @@ This means a single relatum can carry multiple policies with different callbacks

#### Demo example

The demo ships with a `protected_file_delete` callback (`demo/policies.py`) that inspects `rm` commands across three detection modes: literal filename match, glob expansion against the filesystem, and recursive directory inspection. It demonstrates how a callback can make nuanced, context-aware decisions by inspecting both the command arguments and the actual filesystem state:
The demo ships with a `protected_file_delete` callback (`examples/langchain_ollama/policies.py`) that inspects `rm` commands across three detection modes: literal filename match, glob expansion against the filesystem, and recursive directory inspection. It demonstrates how a callback can make nuanced, context-aware decisions by inspecting both the command arguments and the actual filesystem state:

```python
# Registered on the delete → file APPLIES_TO relatum via seed_all.py
Policy(
name="protected_file_delete",
requires_confirmation=False,
trigger_cardinality=None,
callback="demo.policies.protected_file_delete",
callback="examples.langchain_ollama.policies.protected_file_delete",
confirmation_message="Deletion of {action} blocked — protected files at risk.",
)
```
Expand Down Expand Up @@ -337,7 +337,7 @@ vre_guard(
**`concepts`** can be static or dynamic. Static is appropriate when a function always touches the same concept domain. Dynamic is appropriate when the concepts depend on the actual arguments — for example, a shell tool that must inspect the command string to know what it touches. Any callable that accepts the same arguments as the decorated function and returns `list[str]` works:

```python
concepts = ConceptExtractor() # LLM-based — see demo/callbacks.py
concepts = ConceptExtractor() # LLM-based — see examples/langchain_ollama/callbacks.py

@vre_guard(vre, concepts=concepts)
def shell_tool(command: str) -> str:
Expand Down Expand Up @@ -541,13 +541,13 @@ The demo's `DemoLearner` uses ChatOllama structured output to fill templates and
The demo ships a complete LangChain + Ollama agent that exercises all of VRE's enforcement layers against a sandboxed filesystem.

```bash
poetry run python -m demo.main \
poetry run python -m examples.langchain_ollama.main \
--neo4j-uri neo4j://localhost:7687 \
--neo4j-user neo4j \
--neo4j-password password \
--model qwen3:8b \
--concepts-model qwen2.5-coder:7b \
--sandbox demo/workspace
--sandbox examples/langchain_ollama/workspace
```

The agent exposes a single `shell_tool` — a sandboxed subprocess executor — guarded by `vre_guard`. Every shell command the LLM decides to run is intercepted before execution:
Expand All @@ -562,7 +562,7 @@ The agent exposes a single `shell_tool` — a sandboxed subprocess executor —

### Concept extraction

The demo uses `ConceptExtractor` (`demo/callbacks.py`) — a callable class that sends each command segment to a local Ollama model and collects the conceptual primitives it identifies. The prompt includes few-shot flag-to-concept examples (e.g. `rm -rf dir/` → delete + directory + file) and an explicit instruction to never return flag names as primitives.
The demo uses `ConceptExtractor` (`examples/langchain_ollama/callbacks.py`) — a callable class that sends each command segment to a local Ollama model and collects the conceptual primitives it identifies. The prompt includes few-shot flag-to-concept examples (e.g. `rm -rf dir/` → delete + directory + file) and an explicit instruction to never return flag names as primitives.

`ConceptExtractor` is constructed once at startup and reused across calls. It splits compound commands (pipes, `&&`, `;`) into segments and extracts concepts from each independently. The model is configurable via `--concepts-model` (default `qwen2.5-coder:7b`).

Expand All @@ -571,7 +571,7 @@ The demo uses `ConceptExtractor` (`demo/callbacks.py`) — a callable class that
### Wiring it together

```python
# demo/tools.py
# examples/langchain_ollama/tools.py
from vre.guard import vre_guard

concepts = ConceptExtractor() # LLM-based concept extraction (Ollama)
Expand Down Expand Up @@ -617,10 +617,9 @@ VRE ships with a [PreToolUse hook](https://docs.anthropic.com/en/docs/claude-cod

### Install the hook

```python
from vre.integrations.claude_code import install

install("neo4j://localhost:7687", "neo4j", "password")
```bash
python examples/claude-code/claude_code.py install \
--uri neo4j://localhost:7687 --user neo4j --password password
```

This does two things:
Expand Down Expand Up @@ -659,10 +658,8 @@ The hook fails open when the VRE config file is absent. Empty commands are allow

### Remove the hook

```python
from vre.integrations.claude_code import uninstall

uninstall()
```bash
python examples/claude-code/claude_code.py uninstall
```

This removes the VRE hook entry from `~/.claude/settings.json` and leaves `~/.vre/config.json` in place.
Expand Down Expand Up @@ -753,22 +750,23 @@ src/vre/
models.py # Candidate models, CandidateDecision, LearningResult
templates.py # TemplateFactory — gap → structured candidate template
engine.py # LearningEngine — template → callback → validate → persist
integrations/
claude_code.py # Claude Code PreToolUse hook — two-pass concept protocol

scripts/
clear_graph.py # Clear all primitives from the Neo4j graph
seed_all.py # Seed fully grounded graph (16 primitives)
seed_gaps.py # Seed gap-demonstration graph (10 primitives)

demo/
main.py # Entry point — argparse + agent setup
agent.py # ToolAgent — LangChain + Ollama streaming loop
tools.py # shell_tool with vre_guard applied
callbacks.py # ConceptExtractor, on_trace, on_policy, get_cardinality, make_on_learn
policies.py # Demo PolicyCallback — protected file deletion guard
learner.py # DemoLearner — ChatOllama structured output + Rich UI
repl.py # Streaming REPL with Rich Live display
examples/
claude-code/
claude_code.py # Claude Code PreToolUse hook — two-pass concept protocol
langchain_ollama/
main.py # Entry point — argparse + agent setup
agent.py # ToolAgent — LangChain + Ollama streaming loop
tools.py # shell_tool with vre_guard applied
callbacks.py # ConceptExtractor, on_trace, on_policy, get_cardinality, make_on_learn
policies.py # Demo PolicyCallback — protected file deletion guard
learner.py # DemoLearner — ChatOllama structured output + Rich UI
repl.py # Streaming REPL with Rich Live display
```

---
Expand Down
26 changes: 5 additions & 21 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1029,27 +1029,11 @@ <h2 id="panel-title"></h2>
<span class="tg">✓ Grounded — EPISTEMIC PERMISSION GRANTED</span>
</div>
</div>
<p style="margin-top:1.5rem;margin-bottom:0.5rem">
<strong style="color:var(--tb);font-family:'JetBrains Mono',monospace;font-size:0.65rem;letter-spacing:0.1rem">
CLAUDE CODE INTEGRATION
</strong>
</p>
<p>
VRE ships with a <code>PreToolUse</code> hook for Claude Code that
uses a two-pass protocol: the first call blocks and asks Claude to
identify the conceptual primitives; the second call carries a
<code># vre:concept1,concept2</code> prefix that the hook extracts,
grounds, and strips via <code>updatedInput</code> before execution.
</p>
<div class="code-block">
<span class="ck">from</span> vre.integrations.claude_code <span class="ck">import</span> install<br><br>
install(<span class="cs">"neo4j://localhost:7687"</span>, <span class="cs">"neo4j"</span>, <span class="cs">"password"</span>)
</div>
<p>
Claude proposes the concepts itself — no static mapping needed.
When blocked, it reads the epistemic trace, identifies exact gap types,
and adjusts. A frontier model, structurally constrained by a graph
it cannot modify.
<p style="margin-top:1.5rem">
VRE does not own concept extraction, cardinality detection, or trace rendering.
The integrator decides how to bridge their agent's actions to VRE's epistemic
contract. The <code>examples/</code> directory ships working integrations for
Claude Code and LangChain + Ollama.
</p>
`
}
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@

Setup::

from vre.integrations.claude_code import install
install("neo4j://localhost:7687", "neo4j", "password")
python examples/claude-code/claude_code.py install \
--uri neo4j://localhost:7687 --user neo4j --password password

Removal::

from vre.integrations.claude_code import uninstall
uninstall()
python examples/claude-code/claude_code.py uninstall

Hook protocol::

Expand All @@ -45,7 +44,7 @@
_VRE_CONFIG_PATH = Path.home() / ".vre" / "config.json"


_MODULE = "vre.integrations.claude_code"
_HOOK_MARKER = "examples/claude-code/claude_code.py"

# Matches a leading `# vre:concept1,concept2` comment line.
_VRE_PREFIX_RE = re.compile(r"^#\s*vre:\s*(.+)")
Expand All @@ -65,19 +64,21 @@

def _hook_command() -> str:
"""
Build the hook command string using the current interpreter's absolute path.
Build the hook command string using the current interpreter and this script's
absolute path.

This ensures the hook runs in the same virtualenv where VRE is installed,
regardless of what `python` resolves to in Claude Code's shell.
"""
return f"{shlex.quote(sys.executable)} -m {_MODULE}"
script = Path(__file__).resolve()
return f"{shlex.quote(sys.executable)} {shlex.quote(str(script))}"


def _is_vre_hook(hook_entry: dict) -> bool:
"""
Check whether a hook entry belongs to VRE, regardless of interpreter path.
"""
return _MODULE in json.dumps(hook_entry)
return _HOOK_MARKER in json.dumps(hook_entry)


_EXIT_ALLOW = 0
Expand Down Expand Up @@ -278,4 +279,25 @@ def _run_hook() -> None:


if __name__ == "__main__":
_run_hook()
import argparse

parser = argparse.ArgumentParser(description="VRE Claude Code hook")
sub = parser.add_subparsers(dest="command")

inst = sub.add_parser("install", help="Install the VRE PreToolUse hook")
inst.add_argument("--uri", required=True)
inst.add_argument("--user", required=True)
inst.add_argument("--password", required=True)
inst.add_argument("--database", default="neo4j")

sub.add_parser("uninstall", help="Remove the VRE PreToolUse hook")

args = parser.parse_args()

if args.command == "install":
install(args.uri, args.user, args.password, args.database)
elif args.command == "uninstall":
uninstall()
else:
# No subcommand — invoked as hook by Claude Code
_run_hook()
Empty file.
File renamed without changes.
6 changes: 3 additions & 3 deletions demo/callbacks.py → examples/langchain_ollama/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from rich.prompt import Confirm
from rich.tree import Tree

from demo.learner import DemoLearner
from demo.repl import console
from examples.langchain_ollama.learner import DemoLearner
from examples.langchain_ollama.repl import console
from vre.core.policy.models import PolicyViolation

if TYPE_CHECKING:
Expand Down Expand Up @@ -43,7 +43,7 @@ class ConceptExtractor:

Mirrors the DemoLearner pattern: ChatOllama chain constructed once in
__init__ and reused across calls. Callable via __call__ so it can be
passed directly as vre_guard's ``concepts`` parameter.
passed directly as vre_guard's `concepts` parameter.
"""

def __init__(self, model: str = "qwen2.5-coder:7b") -> None:
Expand Down
2 changes: 1 addition & 1 deletion demo/learner.py → examples/langchain_ollama/learner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from rich.panel import Panel
from rich.prompt import Confirm, Prompt

from demo.repl import console
from examples.langchain_ollama.repl import console
from vre.learning.callback import LearningCallback
from vre.learning.models import (
CandidateDecision,
Expand Down
12 changes: 6 additions & 6 deletions demo/main.py → examples/langchain_ollama/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
VRE Demo Agent — entry point.

Usage:
python -m demo.main [--neo4j-uri ...] [--model ...] [--sandbox ...]
python -m examples.langchain_ollama.main [--neo4j-uri ...] [--model ...] [--sandbox ...]
"""

from __future__ import annotations
Expand All @@ -15,10 +15,10 @@
from vre import VRE
from vre.core.graph import PrimitiveRepository

from demo.agent import make_agent
from demo.callbacks import ConceptExtractor, get_cardinality, make_on_learn, on_policy, on_trace
from demo.repl import run
from demo.tools import init_tools
from examples.langchain_ollama.agent import make_agent
from examples.langchain_ollama.callbacks import ConceptExtractor, get_cardinality, make_on_learn, on_policy, on_trace
from examples.langchain_ollama.repl import run
from examples.langchain_ollama.tools import init_tools


def main() -> None:
Expand All @@ -27,7 +27,7 @@ def main() -> None:
parser.add_argument("--neo4j-user", default="neo4j")
parser.add_argument("--neo4j-password", default="password")
parser.add_argument("--model", default="qwen3.5:latest")
parser.add_argument("--sandbox", default="demo/workspace")
parser.add_argument("--sandbox", default="examples/langchain_ollama/workspace")
parser.add_argument("--concepts-model", default="qwen2.5-coder:7b")
args = parser.parse_args()

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "vre"
version = "0.3.1"
version = "0.3.3"
description = "Volute Reasoning Engine — decorator-based epistemic enforcement"
authors = [
{name = "Andrew Greene",email = "anormang@gmail.com"}
Expand All @@ -14,7 +14,7 @@ dependencies = [
]

[project.optional-dependencies]
demo = [
examples = [
"rich>=14.3.2",
"langchain-core>=1.2.16",
"langchain-ollama>=1.0.1",
Expand Down
2 changes: 1 addition & 1 deletion scripts/seed_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ def seed_delete(repo: PrimitiveRepository, file: Primitive, directory: Primitive
Policy(
name="ProtectedFileDeletePolicy",
requires_confirmation=True,
callback="demo.policies.protected_file_delete",
callback="examples.langchain_ollama.policies.protected_file_delete",
confirmation_message="Deletion may affect protected files. Proceed?",
),
],
Expand Down
2 changes: 0 additions & 2 deletions src/vre/integrations/__init__.py

This file was deleted.

Loading
Loading