vsl-langchain 0.1.2__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Super Semantics
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: vsl-langchain
3
+ Version: 0.1.2
4
+ Summary: LangChain adapter for VSL (VERBA Specification Language) governance primitives -- implements vsl-core's VSLAdapter conformance contract via create_agent's AgentMiddleware.wrap_tool_call hook.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/supersemantics/VSL-LangChain
7
+ Project-URL: Repository, https://github.com/supersemantics/VSL-LangChain
8
+ Project-URL: Changelog, https://github.com/supersemantics/VSL-LangChain/blob/HEAD/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/supersemantics/VSL-LangChain/issues
10
+ Keywords: governance,ai-safety,guardrails,langchain,vsl,verba
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: super-semantics-vsl<0.4,>=0.3
19
+ Requires-Dist: langchain>=1.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # vsl-langchain
26
+
27
+ A [LangChain](https://github.com/langchain-ai/langchain) adapter for
28
+ [`vsl-core`](https://github.com/supersemantics/VSL-Core): implements
29
+ `vsl_core.conformance.protocol.VSLAdapter` and gates tool calls inside a
30
+ `create_agent()` agent via `AgentMiddleware.wrap_tool_call`.
31
+
32
+ **Is:**
33
+ - A `VSLAdapter` implementation (`LangChainAdapter`) that passes `vsl_core.conformance.suite.run_conformance_suite` with an empty result.
34
+ - `VSLToolMiddleware`: a real `langchain.agents.middleware.types.AgentMiddleware` subclass that gates named tools, keyed by tool name, inside `create_agent`'s middleware stack.
35
+
36
+ **Is not:**
37
+ - A replacement for `vsl-core` itself — the `PreNode`/`Invariant`/`Fallback`/`TerminalState` constructs, the ledger, and the governance vocabulary all live in `vsl-core`. This package only compiles and wires those constructs into LangChain.
38
+ - Compatible with the deprecated `AgentExecutor`/`initialize_agent`/`create_react_agent` APIs. Those predate the middleware system this adapter depends on — this package targets `create_agent` (LangChain >= 1.0) only.
39
+
40
+ ## Why `wrap_tool_call`, and why it's a real gate — not an assumption
41
+
42
+ `AgentMiddleware.wrap_tool_call`/`awrap_tool_call` is LangChain's own sanctioned interception point for tool execution. This was verified directly against the installed package's own docstring (`langchain==1.3.17`, not just documentation prose), which states outright:
43
+
44
+ > "Middleware can call the handler multiple times for retry logic, skip calling it to short-circuit, or modify the request/response."
45
+
46
+ That is a genuine F1 pre-commitment guarantee: the middleware runs strictly before the tool executes, and structurally controls whether the real call (`handler`) ever happens at all. If a gate denies, `handler` is never called — the tool genuinely never runs, proven directly in this repo's own tests (`test_integration.py` asserts the handler's call log stays empty on denial, not just that a particular return value came back).
47
+
48
+ This supersedes an earlier, weaker finding (see project memory) that LangChain's callback system (`on_tool_start`, etc.) had unconfirmed blocking behavior — `wrap_tool_call` is a different, confirmed mechanism, not the same uncertain one.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install vsl-langchain
54
+ ```
55
+
56
+ This installs vsl-core (`super-semantics-vsl`, imported as `vsl_core`) and LangChain with it.
57
+
58
+ ## Quick taste
59
+
60
+ ```python
61
+ from langchain.agents import create_agent
62
+ from vsl_core.constructs import PreNode
63
+ from vsl_core.metrics import AssuranceBasis, F2Modification, GammaEstimate
64
+ from vsl_langchain import LangChainAdapter, VSLToolMiddleware
65
+
66
+ adapter = LangChainAdapter()
67
+
68
+ async def monitor(candidate_input):
69
+ amount = candidate_input["args"].get("amount", 0.0)
70
+ return GammaEstimate(gamma_hat=2.0 if amount <= 1000.0 else 0.0)
71
+
72
+ pre_node = PreNode(
73
+ name="refund-cap",
74
+ description="Requires refund amount under the hard cap.",
75
+ monitor=monitor,
76
+ assurance_basis=AssuranceBasis(f1_pre_commitment=True, f2_modification=F2Modification.NONE),
77
+ )
78
+ gate = adapter.compile_pre_node(pre_node)
79
+
80
+ middleware = VSLToolMiddleware(gates_by_tool_name={"send_refund": gate})
81
+
82
+ agent = create_agent(
83
+ model="anthropic:claude-sonnet-5",
84
+ tools=[send_refund, lookup_order], # lookup_order stays ungated -- no entry in gates_by_tool_name
85
+ middleware=[middleware],
86
+ )
87
+ ```
88
+
89
+ ## `VSLToolMiddleware` doesn't consume `Fallback`
90
+
91
+ A `PreNode`'s `Fallback` (`on_failure`, `delta_factor`, `max_retries`, `on_max_retries`) is policy data only here — `LangChainAdapter.compile_pre_node`/`compile_invariant` never read `pre_node.fallback`. On denial, `VSLToolMiddleware.awrap_tool_call` returns an error `ToolMessage` once, immediately; there is no retry loop. This is the same inertness `vsl-langgraph`'s `LangGraphAdapter` has — `vsl-core`'s own `PlainPythonReferenceAdapter` doesn't consume `Fallback` either, so no adapter currently executes these fields as behavior.
92
+
93
+ ## `VSLToolMiddleware` doesn't write to the ledger either
94
+
95
+ Nothing in `awrap_tool_call` calls `VerbaLedger.write*` — a gate denial becomes an error `ToolMessage`, and that's it. If you want an audit trail of gate decisions, your own code (e.g. wrapping `VSLToolMiddleware` construction, or a separate middleware) has to call `ledger.write_monitor`/`ledger.write(LedgerEntryType.PRE_NODE, ...)`/`write_verification` explicitly, causally linked via `caused_by` — the same three entry types `vsl-core`'s own [building guide](https://github.com/supersemantics/VSL-Core/blob/main/docs/building-with-vsl-core.md#step-3-the-five-things-youll-actually-write) says to write around every gated call. Skip `PRE_NODE` and `VerbaLedger.audit()`'s `drift_flagged_monitor_has_pre_node` check fails on any denial — not a bug in `audit()`, just an incomplete write. An agent with no ledger calls anywhere produces real, working gating and zero audit trail — this package won't warn you, because from its point of view every gate did exactly what it was compiled to do.
96
+
97
+ Where those writes are kept is up to you: `VerbaLedger` accepts any `LedgerStore`, and vsl-core includes `InMemoryLedgerStore`, `JsonlLedgerStore` (a local append-only file) and an optional immudb store.
98
+
99
+ ## The F1/F2 assurance distinction — same caveat as `vsl-core` and `vsl-langgraph`
100
+
101
+ Every `PreNode`/`Invariant` here still requires an `AssuranceBasis`, and it's still **self-declared, not independently verified** — nothing in this package checks that a stated `F2Modification.FULL` actually matches what a `monitor`/`rule` does. See `vsl-core`'s README ("The F1/F2 assurance distinction") for the full explanation. `vsl-langchain` does not yet carry the hosted-API-detection heuristic `vsl-langgraph`'s `LangGraphAdapter` has — that's a known gap, not implemented in this initial version.
102
+
103
+ ## Status
104
+
105
+ Alpha. Conformance-suite green. Integration-tested against the real `ToolCallRequest`/`ToolMessage` types from the actually-installed `langchain`/`langgraph` packages (not mocks) — covering: `PreNode` allow/deny, `Invariant` allow/deny, and an ungated tool passing straight through. Not yet tested against a full live `create_agent()` loop with a real or fake chat model — only the middleware's `awrap_tool_call` hook in isolation.
106
+
107
+ ## Layout
108
+
109
+ ```
110
+ src/vsl_langchain/
111
+ └── adapter.py LangChainAdapter (VSLAdapter conformance contract), VSLToolMiddleware (create_agent wiring)
112
+ tests/
113
+ ├── test_conformance.py run_conformance_suite(LangChainAdapter()) == []
114
+ └── test_integration.py real ToolCallRequest/ToolMessage, PreNode + Invariant, allow/deny/ungated paths
115
+ ```
@@ -0,0 +1,91 @@
1
+ # vsl-langchain
2
+
3
+ A [LangChain](https://github.com/langchain-ai/langchain) adapter for
4
+ [`vsl-core`](https://github.com/supersemantics/VSL-Core): implements
5
+ `vsl_core.conformance.protocol.VSLAdapter` and gates tool calls inside a
6
+ `create_agent()` agent via `AgentMiddleware.wrap_tool_call`.
7
+
8
+ **Is:**
9
+ - A `VSLAdapter` implementation (`LangChainAdapter`) that passes `vsl_core.conformance.suite.run_conformance_suite` with an empty result.
10
+ - `VSLToolMiddleware`: a real `langchain.agents.middleware.types.AgentMiddleware` subclass that gates named tools, keyed by tool name, inside `create_agent`'s middleware stack.
11
+
12
+ **Is not:**
13
+ - A replacement for `vsl-core` itself — the `PreNode`/`Invariant`/`Fallback`/`TerminalState` constructs, the ledger, and the governance vocabulary all live in `vsl-core`. This package only compiles and wires those constructs into LangChain.
14
+ - Compatible with the deprecated `AgentExecutor`/`initialize_agent`/`create_react_agent` APIs. Those predate the middleware system this adapter depends on — this package targets `create_agent` (LangChain >= 1.0) only.
15
+
16
+ ## Why `wrap_tool_call`, and why it's a real gate — not an assumption
17
+
18
+ `AgentMiddleware.wrap_tool_call`/`awrap_tool_call` is LangChain's own sanctioned interception point for tool execution. This was verified directly against the installed package's own docstring (`langchain==1.3.17`, not just documentation prose), which states outright:
19
+
20
+ > "Middleware can call the handler multiple times for retry logic, skip calling it to short-circuit, or modify the request/response."
21
+
22
+ That is a genuine F1 pre-commitment guarantee: the middleware runs strictly before the tool executes, and structurally controls whether the real call (`handler`) ever happens at all. If a gate denies, `handler` is never called — the tool genuinely never runs, proven directly in this repo's own tests (`test_integration.py` asserts the handler's call log stays empty on denial, not just that a particular return value came back).
23
+
24
+ This supersedes an earlier, weaker finding (see project memory) that LangChain's callback system (`on_tool_start`, etc.) had unconfirmed blocking behavior — `wrap_tool_call` is a different, confirmed mechanism, not the same uncertain one.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install vsl-langchain
30
+ ```
31
+
32
+ This installs vsl-core (`super-semantics-vsl`, imported as `vsl_core`) and LangChain with it.
33
+
34
+ ## Quick taste
35
+
36
+ ```python
37
+ from langchain.agents import create_agent
38
+ from vsl_core.constructs import PreNode
39
+ from vsl_core.metrics import AssuranceBasis, F2Modification, GammaEstimate
40
+ from vsl_langchain import LangChainAdapter, VSLToolMiddleware
41
+
42
+ adapter = LangChainAdapter()
43
+
44
+ async def monitor(candidate_input):
45
+ amount = candidate_input["args"].get("amount", 0.0)
46
+ return GammaEstimate(gamma_hat=2.0 if amount <= 1000.0 else 0.0)
47
+
48
+ pre_node = PreNode(
49
+ name="refund-cap",
50
+ description="Requires refund amount under the hard cap.",
51
+ monitor=monitor,
52
+ assurance_basis=AssuranceBasis(f1_pre_commitment=True, f2_modification=F2Modification.NONE),
53
+ )
54
+ gate = adapter.compile_pre_node(pre_node)
55
+
56
+ middleware = VSLToolMiddleware(gates_by_tool_name={"send_refund": gate})
57
+
58
+ agent = create_agent(
59
+ model="anthropic:claude-sonnet-5",
60
+ tools=[send_refund, lookup_order], # lookup_order stays ungated -- no entry in gates_by_tool_name
61
+ middleware=[middleware],
62
+ )
63
+ ```
64
+
65
+ ## `VSLToolMiddleware` doesn't consume `Fallback`
66
+
67
+ A `PreNode`'s `Fallback` (`on_failure`, `delta_factor`, `max_retries`, `on_max_retries`) is policy data only here — `LangChainAdapter.compile_pre_node`/`compile_invariant` never read `pre_node.fallback`. On denial, `VSLToolMiddleware.awrap_tool_call` returns an error `ToolMessage` once, immediately; there is no retry loop. This is the same inertness `vsl-langgraph`'s `LangGraphAdapter` has — `vsl-core`'s own `PlainPythonReferenceAdapter` doesn't consume `Fallback` either, so no adapter currently executes these fields as behavior.
68
+
69
+ ## `VSLToolMiddleware` doesn't write to the ledger either
70
+
71
+ Nothing in `awrap_tool_call` calls `VerbaLedger.write*` — a gate denial becomes an error `ToolMessage`, and that's it. If you want an audit trail of gate decisions, your own code (e.g. wrapping `VSLToolMiddleware` construction, or a separate middleware) has to call `ledger.write_monitor`/`ledger.write(LedgerEntryType.PRE_NODE, ...)`/`write_verification` explicitly, causally linked via `caused_by` — the same three entry types `vsl-core`'s own [building guide](https://github.com/supersemantics/VSL-Core/blob/main/docs/building-with-vsl-core.md#step-3-the-five-things-youll-actually-write) says to write around every gated call. Skip `PRE_NODE` and `VerbaLedger.audit()`'s `drift_flagged_monitor_has_pre_node` check fails on any denial — not a bug in `audit()`, just an incomplete write. An agent with no ledger calls anywhere produces real, working gating and zero audit trail — this package won't warn you, because from its point of view every gate did exactly what it was compiled to do.
72
+
73
+ Where those writes are kept is up to you: `VerbaLedger` accepts any `LedgerStore`, and vsl-core includes `InMemoryLedgerStore`, `JsonlLedgerStore` (a local append-only file) and an optional immudb store.
74
+
75
+ ## The F1/F2 assurance distinction — same caveat as `vsl-core` and `vsl-langgraph`
76
+
77
+ Every `PreNode`/`Invariant` here still requires an `AssuranceBasis`, and it's still **self-declared, not independently verified** — nothing in this package checks that a stated `F2Modification.FULL` actually matches what a `monitor`/`rule` does. See `vsl-core`'s README ("The F1/F2 assurance distinction") for the full explanation. `vsl-langchain` does not yet carry the hosted-API-detection heuristic `vsl-langgraph`'s `LangGraphAdapter` has — that's a known gap, not implemented in this initial version.
78
+
79
+ ## Status
80
+
81
+ Alpha. Conformance-suite green. Integration-tested against the real `ToolCallRequest`/`ToolMessage` types from the actually-installed `langchain`/`langgraph` packages (not mocks) — covering: `PreNode` allow/deny, `Invariant` allow/deny, and an ungated tool passing straight through. Not yet tested against a full live `create_agent()` loop with a real or fake chat model — only the middleware's `awrap_tool_call` hook in isolation.
82
+
83
+ ## Layout
84
+
85
+ ```
86
+ src/vsl_langchain/
87
+ └── adapter.py LangChainAdapter (VSLAdapter conformance contract), VSLToolMiddleware (create_agent wiring)
88
+ tests/
89
+ ├── test_conformance.py run_conformance_suite(LangChainAdapter()) == []
90
+ └── test_integration.py real ToolCallRequest/ToolMessage, PreNode + Invariant, allow/deny/ungated paths
91
+ ```
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vsl-langchain"
7
+ version = "0.1.2"
8
+ description = "LangChain adapter for VSL (VERBA Specification Language) governance primitives -- implements vsl-core's VSLAdapter conformance contract via create_agent's AgentMiddleware.wrap_tool_call hook."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ # vsl-core, published on PyPI as super-semantics-vsl (imported as vsl_core).
14
+ "super-semantics-vsl>=0.3,<0.4",
15
+ # AgentMiddleware.wrap_tool_call ("skip calling it to short-circuit")
16
+ # is the confirmed F1 pre-commitment interception point this adapter
17
+ # compiles gates onto -- requires the current, non-deprecated
18
+ # langchain.agents.create_agent API, not the deprecated AgentExecutor.
19
+ "langchain>=1.0",
20
+ ]
21
+ keywords = ["governance", "ai-safety", "guardrails", "langchain", "vsl", "verba"]
22
+ classifiers = [
23
+ "License :: OSI Approved :: MIT License",
24
+ "Programming Language :: Python :: 3",
25
+ "Operating System :: OS Independent",
26
+ "Development Status :: 3 - Alpha",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/supersemantics/VSL-LangChain"
31
+ Repository = "https://github.com/supersemantics/VSL-LangChain"
32
+ Changelog = "https://github.com/supersemantics/VSL-LangChain/blob/HEAD/CHANGELOG.md"
33
+ Issues = "https://github.com/supersemantics/VSL-LangChain/issues"
34
+
35
+ [project.optional-dependencies]
36
+ dev = ["pytest>=7", "pytest-asyncio>=0.23"]
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ from .adapter import DEFAULT_DENIAL_KEY, LangChainAdapter, VSLToolMiddleware
2
+
3
+ __all__ = [
4
+ "LangChainAdapter",
5
+ "VSLToolMiddleware",
6
+ "DEFAULT_DENIAL_KEY",
7
+ ]
@@ -0,0 +1,132 @@
1
+ """LangChainAdapter + VSLToolMiddleware: a VSLAdapter conformant with
2
+ vsl-core's conformance contract, plus the LangChain-specific wiring to gate
3
+ tool calls inside a create_agent() agent via AgentMiddleware.wrap_tool_call.
4
+
5
+ wrap_tool_call is a confirmed F1 pre-commitment interception point --
6
+ verified directly against langchain's actual source
7
+ (langchain/agents/middleware/types.py) and langgraph's ToolCallRequest
8
+ (langgraph/prebuilt/tool_node.py), not assumed from docs prose. The
9
+ middleware receives the ToolCallRequest strictly before the tool executes,
10
+ and structurally controls whether the real call (`handler`) ever runs --
11
+ the framework's own docstring says outright you can "skip calling it to
12
+ short-circuit". This is the same F1 property vsl-langgraph's LangGraphAdapter
13
+ relies on, just reached through create_agent's own sanctioned middleware
14
+ hook instead of a hand-wrapped node function.
15
+
16
+ The compiled gate itself is the same framework-agnostic async callable
17
+ vsl-core's own PlainPythonReferenceAdapter produces -- LangChainAdapter's
18
+ compile_pre_node/compile_invariant are intentionally near-identical to
19
+ vsl-langgraph's LangGraphAdapter for that reason. What this package adds on
20
+ top is VSLToolMiddleware: the tool-name-keyed wiring for create_agent's
21
+ middleware list, and the ToolMessage-shaped denial response create_agent
22
+ actually expects back from wrap_tool_call.
23
+
24
+ Requires langchain>=1.0's create_agent API (AgentMiddleware.wrap_tool_call).
25
+ Does not support the deprecated AgentExecutor/create_react_agent path --
26
+ that API predates the middleware system this adapter depends on.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Any, Awaitable, Callable, Mapping
32
+
33
+ from langchain.agents.middleware.types import AgentMiddleware
34
+ from langchain_core.messages import ToolMessage
35
+ from vsl_core.constructs import Invariant, PreNode
36
+ from vsl_core.conformance.protocol import CompiledGate
37
+ from vsl_core.exceptions import AutomationDeniedException, InvariantViolation
38
+
39
+ DEFAULT_DENIAL_KEY = "vsl_denial"
40
+
41
+
42
+ class LangChainAdapter:
43
+ """VSLAdapter implementation. Gate-compilation logic is intentionally
44
+ identical in shape to vsl-langgraph's LangGraphAdapter -- the compiled
45
+ gate is framework-agnostic (vsl_core.conformance.protocol.CompiledGate);
46
+ only the wiring around it (VSLToolMiddleware, below) is LangChain-specific.
47
+ """
48
+
49
+ def compile_pre_node(self, pre_node: PreNode) -> CompiledGate:
50
+ async def gate(candidate_input: Any) -> None:
51
+ estimate = await pre_node.monitor(candidate_input)
52
+ if not estimate.sufficient(pre_node.gamma_threshold):
53
+ raise AutomationDeniedException(
54
+ reason=f"Pre-node '{pre_node.name}' insufficient Gamma",
55
+ identity_key="vsl-langchain",
56
+ )
57
+
58
+ return gate
59
+
60
+ def compile_invariant(self, invariant: Invariant) -> CompiledGate:
61
+ async def gate(candidate_input: Any) -> None:
62
+ holds = await invariant.rule(candidate_input)
63
+ if not holds:
64
+ reason = f"Invariant '{invariant.name}' violated"
65
+ terminal_state_name = invariant.on_violation.name if invariant.on_violation is not None else None
66
+ if terminal_state_name is not None:
67
+ reason += f" -- entering terminal state '{terminal_state_name}'"
68
+ raise InvariantViolation(
69
+ reason=reason,
70
+ identity_key="vsl-langchain",
71
+ invariant_name=invariant.name,
72
+ terminal_state_name=terminal_state_name,
73
+ )
74
+
75
+ return gate
76
+
77
+
78
+ class VSLToolMiddleware(AgentMiddleware):
79
+ """Gates specific tools inside a create_agent() agent, via
80
+ AgentMiddleware.awrap_tool_call.
81
+
82
+ `gates_by_tool_name` maps a tool's name (matched against
83
+ request.tool_call["name"]) to a CompiledGate produced by
84
+ LangChainAdapter.compile_pre_node/compile_invariant. Tools with no
85
+ entry in this mapping are ungated -- they pass straight to `handler`,
86
+ matching vsl-core's own "not every action needs a gate" posture (a
87
+ read-only lookup tool, for example, shouldn't be gated at all).
88
+
89
+ `candidate_input_fn` extracts whatever the gate should evaluate from the
90
+ ToolCallRequest; defaults to passing the request's tool_call dict
91
+ (name/args/id) through unchanged, matching vsl-langgraph's gated_node
92
+ default of passing the whole state through.
93
+
94
+ On denial, returns a ToolMessage (status="error") carrying the raised
95
+ exception's reason instead of letting it propagate -- the tool call
96
+ itself never fires, since `handler` is never called. This mirrors
97
+ gated_node's convention (denial becomes a state update, not a crash)
98
+ adapted to what create_agent's tool-call loop actually expects back:
99
+ a ToolMessage, not a raised exception.
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ gates_by_tool_name: Mapping[str, CompiledGate],
105
+ *,
106
+ candidate_input_fn: Callable[[Any], Any] | None = None,
107
+ denial_key: str = DEFAULT_DENIAL_KEY,
108
+ ) -> None:
109
+ self._gates = dict(gates_by_tool_name)
110
+ self._extract = candidate_input_fn or (lambda request: request.tool_call)
111
+ self._denial_key = denial_key
112
+
113
+ async def awrap_tool_call(
114
+ self,
115
+ request: Any,
116
+ handler: Callable[[Any], Awaitable[ToolMessage | Any]],
117
+ ) -> ToolMessage | Any:
118
+ tool_name = request.tool_call["name"]
119
+ gate = self._gates.get(tool_name)
120
+ if gate is None:
121
+ return await handler(request)
122
+
123
+ try:
124
+ await gate(self._extract(request))
125
+ except AutomationDeniedException as exc:
126
+ return ToolMessage(
127
+ content=f"Denied by VSL governance: {exc}",
128
+ tool_call_id=request.tool_call["id"],
129
+ status="error",
130
+ )
131
+
132
+ return await handler(request)
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: vsl-langchain
3
+ Version: 0.1.2
4
+ Summary: LangChain adapter for VSL (VERBA Specification Language) governance primitives -- implements vsl-core's VSLAdapter conformance contract via create_agent's AgentMiddleware.wrap_tool_call hook.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/supersemantics/VSL-LangChain
7
+ Project-URL: Repository, https://github.com/supersemantics/VSL-LangChain
8
+ Project-URL: Changelog, https://github.com/supersemantics/VSL-LangChain/blob/HEAD/CHANGELOG.md
9
+ Project-URL: Issues, https://github.com/supersemantics/VSL-LangChain/issues
10
+ Keywords: governance,ai-safety,guardrails,langchain,vsl,verba
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: super-semantics-vsl<0.4,>=0.3
19
+ Requires-Dist: langchain>=1.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # vsl-langchain
26
+
27
+ A [LangChain](https://github.com/langchain-ai/langchain) adapter for
28
+ [`vsl-core`](https://github.com/supersemantics/VSL-Core): implements
29
+ `vsl_core.conformance.protocol.VSLAdapter` and gates tool calls inside a
30
+ `create_agent()` agent via `AgentMiddleware.wrap_tool_call`.
31
+
32
+ **Is:**
33
+ - A `VSLAdapter` implementation (`LangChainAdapter`) that passes `vsl_core.conformance.suite.run_conformance_suite` with an empty result.
34
+ - `VSLToolMiddleware`: a real `langchain.agents.middleware.types.AgentMiddleware` subclass that gates named tools, keyed by tool name, inside `create_agent`'s middleware stack.
35
+
36
+ **Is not:**
37
+ - A replacement for `vsl-core` itself — the `PreNode`/`Invariant`/`Fallback`/`TerminalState` constructs, the ledger, and the governance vocabulary all live in `vsl-core`. This package only compiles and wires those constructs into LangChain.
38
+ - Compatible with the deprecated `AgentExecutor`/`initialize_agent`/`create_react_agent` APIs. Those predate the middleware system this adapter depends on — this package targets `create_agent` (LangChain >= 1.0) only.
39
+
40
+ ## Why `wrap_tool_call`, and why it's a real gate — not an assumption
41
+
42
+ `AgentMiddleware.wrap_tool_call`/`awrap_tool_call` is LangChain's own sanctioned interception point for tool execution. This was verified directly against the installed package's own docstring (`langchain==1.3.17`, not just documentation prose), which states outright:
43
+
44
+ > "Middleware can call the handler multiple times for retry logic, skip calling it to short-circuit, or modify the request/response."
45
+
46
+ That is a genuine F1 pre-commitment guarantee: the middleware runs strictly before the tool executes, and structurally controls whether the real call (`handler`) ever happens at all. If a gate denies, `handler` is never called — the tool genuinely never runs, proven directly in this repo's own tests (`test_integration.py` asserts the handler's call log stays empty on denial, not just that a particular return value came back).
47
+
48
+ This supersedes an earlier, weaker finding (see project memory) that LangChain's callback system (`on_tool_start`, etc.) had unconfirmed blocking behavior — `wrap_tool_call` is a different, confirmed mechanism, not the same uncertain one.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install vsl-langchain
54
+ ```
55
+
56
+ This installs vsl-core (`super-semantics-vsl`, imported as `vsl_core`) and LangChain with it.
57
+
58
+ ## Quick taste
59
+
60
+ ```python
61
+ from langchain.agents import create_agent
62
+ from vsl_core.constructs import PreNode
63
+ from vsl_core.metrics import AssuranceBasis, F2Modification, GammaEstimate
64
+ from vsl_langchain import LangChainAdapter, VSLToolMiddleware
65
+
66
+ adapter = LangChainAdapter()
67
+
68
+ async def monitor(candidate_input):
69
+ amount = candidate_input["args"].get("amount", 0.0)
70
+ return GammaEstimate(gamma_hat=2.0 if amount <= 1000.0 else 0.0)
71
+
72
+ pre_node = PreNode(
73
+ name="refund-cap",
74
+ description="Requires refund amount under the hard cap.",
75
+ monitor=monitor,
76
+ assurance_basis=AssuranceBasis(f1_pre_commitment=True, f2_modification=F2Modification.NONE),
77
+ )
78
+ gate = adapter.compile_pre_node(pre_node)
79
+
80
+ middleware = VSLToolMiddleware(gates_by_tool_name={"send_refund": gate})
81
+
82
+ agent = create_agent(
83
+ model="anthropic:claude-sonnet-5",
84
+ tools=[send_refund, lookup_order], # lookup_order stays ungated -- no entry in gates_by_tool_name
85
+ middleware=[middleware],
86
+ )
87
+ ```
88
+
89
+ ## `VSLToolMiddleware` doesn't consume `Fallback`
90
+
91
+ A `PreNode`'s `Fallback` (`on_failure`, `delta_factor`, `max_retries`, `on_max_retries`) is policy data only here — `LangChainAdapter.compile_pre_node`/`compile_invariant` never read `pre_node.fallback`. On denial, `VSLToolMiddleware.awrap_tool_call` returns an error `ToolMessage` once, immediately; there is no retry loop. This is the same inertness `vsl-langgraph`'s `LangGraphAdapter` has — `vsl-core`'s own `PlainPythonReferenceAdapter` doesn't consume `Fallback` either, so no adapter currently executes these fields as behavior.
92
+
93
+ ## `VSLToolMiddleware` doesn't write to the ledger either
94
+
95
+ Nothing in `awrap_tool_call` calls `VerbaLedger.write*` — a gate denial becomes an error `ToolMessage`, and that's it. If you want an audit trail of gate decisions, your own code (e.g. wrapping `VSLToolMiddleware` construction, or a separate middleware) has to call `ledger.write_monitor`/`ledger.write(LedgerEntryType.PRE_NODE, ...)`/`write_verification` explicitly, causally linked via `caused_by` — the same three entry types `vsl-core`'s own [building guide](https://github.com/supersemantics/VSL-Core/blob/main/docs/building-with-vsl-core.md#step-3-the-five-things-youll-actually-write) says to write around every gated call. Skip `PRE_NODE` and `VerbaLedger.audit()`'s `drift_flagged_monitor_has_pre_node` check fails on any denial — not a bug in `audit()`, just an incomplete write. An agent with no ledger calls anywhere produces real, working gating and zero audit trail — this package won't warn you, because from its point of view every gate did exactly what it was compiled to do.
96
+
97
+ Where those writes are kept is up to you: `VerbaLedger` accepts any `LedgerStore`, and vsl-core includes `InMemoryLedgerStore`, `JsonlLedgerStore` (a local append-only file) and an optional immudb store.
98
+
99
+ ## The F1/F2 assurance distinction — same caveat as `vsl-core` and `vsl-langgraph`
100
+
101
+ Every `PreNode`/`Invariant` here still requires an `AssuranceBasis`, and it's still **self-declared, not independently verified** — nothing in this package checks that a stated `F2Modification.FULL` actually matches what a `monitor`/`rule` does. See `vsl-core`'s README ("The F1/F2 assurance distinction") for the full explanation. `vsl-langchain` does not yet carry the hosted-API-detection heuristic `vsl-langgraph`'s `LangGraphAdapter` has — that's a known gap, not implemented in this initial version.
102
+
103
+ ## Status
104
+
105
+ Alpha. Conformance-suite green. Integration-tested against the real `ToolCallRequest`/`ToolMessage` types from the actually-installed `langchain`/`langgraph` packages (not mocks) — covering: `PreNode` allow/deny, `Invariant` allow/deny, and an ungated tool passing straight through. Not yet tested against a full live `create_agent()` loop with a real or fake chat model — only the middleware's `awrap_tool_call` hook in isolation.
106
+
107
+ ## Layout
108
+
109
+ ```
110
+ src/vsl_langchain/
111
+ └── adapter.py LangChainAdapter (VSLAdapter conformance contract), VSLToolMiddleware (create_agent wiring)
112
+ tests/
113
+ ├── test_conformance.py run_conformance_suite(LangChainAdapter()) == []
114
+ └── test_integration.py real ToolCallRequest/ToolMessage, PreNode + Invariant, allow/deny/ungated paths
115
+ ```
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vsl_langchain/__init__.py
5
+ src/vsl_langchain/adapter.py
6
+ src/vsl_langchain.egg-info/PKG-INFO
7
+ src/vsl_langchain.egg-info/SOURCES.txt
8
+ src/vsl_langchain.egg-info/dependency_links.txt
9
+ src/vsl_langchain.egg-info/requires.txt
10
+ src/vsl_langchain.egg-info/top_level.txt
11
+ tests/test_conformance.py
12
+ tests/test_integration.py
@@ -0,0 +1,6 @@
1
+ super-semantics-vsl<0.4,>=0.3
2
+ langchain>=1.0
3
+
4
+ [dev]
5
+ pytest>=7
6
+ pytest-asyncio>=0.23
@@ -0,0 +1 @@
1
+ vsl_langchain
@@ -0,0 +1,8 @@
1
+ from vsl_core.conformance.suite import run_conformance_suite
2
+
3
+ from vsl_langchain import LangChainAdapter
4
+
5
+
6
+ def test_langchain_adapter_is_conformant():
7
+ failures = run_conformance_suite(LangChainAdapter())
8
+ assert failures == []
@@ -0,0 +1,119 @@
1
+ """Exercises VSLToolMiddleware against the real langchain/langgraph types
2
+ (ToolCallRequest, ToolMessage), not mocks -- same discipline as
3
+ vsl-langgraph's test_integration.py.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from langgraph.prebuilt.tool_node import ToolCallRequest
9
+ from vsl_core.constructs import Invariant, PreNode
10
+ from vsl_core.exceptions import InvariantViolation
11
+ from vsl_core.metrics import AssuranceBasis, F2Modification, GammaEstimate
12
+
13
+ from vsl_langchain import LangChainAdapter, VSLToolMiddleware
14
+
15
+ _ASSURANCE_BASIS = AssuranceBasis(f1_pre_commitment=True, f2_modification=F2Modification.NONE)
16
+
17
+ _HARD_CAP = 1000.0
18
+
19
+
20
+ def _make_request(name: str, args: dict[str, Any], call_id: str = "call_1") -> ToolCallRequest:
21
+ return ToolCallRequest(
22
+ tool_call={"name": name, "args": args, "id": call_id, "type": "tool_call"},
23
+ tool=None,
24
+ state={},
25
+ runtime=None,
26
+ )
27
+
28
+
29
+ async def _under_hard_cap(candidate_input: Any) -> GammaEstimate:
30
+ amount = candidate_input["args"].get("amount", 0.0)
31
+ return GammaEstimate(gamma_hat=2.0 if amount <= _HARD_CAP else 0.0)
32
+
33
+
34
+ async def _refund_amount_ok(candidate_input: Any) -> bool:
35
+ amount = candidate_input["args"].get("amount", 0.0)
36
+ return amount <= _HARD_CAP
37
+
38
+
39
+ async def _handler_records_call(calls: list[str]):
40
+ async def handler(request: ToolCallRequest):
41
+ calls.append(request.tool_call["name"])
42
+ return f"executed {request.tool_call['name']}"
43
+
44
+ return handler
45
+
46
+
47
+ def _build_pre_node_middleware() -> VSLToolMiddleware:
48
+ adapter = LangChainAdapter()
49
+ pre_node = PreNode(
50
+ name="refund-amount-pre-node",
51
+ description="Requires refund amount under the hard cap before send_refund runs.",
52
+ monitor=_under_hard_cap,
53
+ assurance_basis=_ASSURANCE_BASIS,
54
+ )
55
+ gate = adapter.compile_pre_node(pre_node)
56
+ return VSLToolMiddleware(gates_by_tool_name={"send_refund": gate})
57
+
58
+
59
+ def _build_invariant_middleware() -> VSLToolMiddleware:
60
+ adapter = LangChainAdapter()
61
+ invariant = Invariant(
62
+ name="refund-amount-invariant",
63
+ description="Refund amount must never exceed the hard cap -- non-bypassable.",
64
+ rule=_refund_amount_ok,
65
+ assurance_basis=_ASSURANCE_BASIS,
66
+ )
67
+ gate = adapter.compile_invariant(invariant)
68
+ return VSLToolMiddleware(gates_by_tool_name={"send_refund": gate})
69
+
70
+
71
+ async def test_pre_node_allows_call_under_cap_and_handler_actually_runs():
72
+ calls: list[str] = []
73
+ middleware = _build_pre_node_middleware()
74
+ handler = await _handler_records_call(calls)
75
+
76
+ request = _make_request("send_refund", {"amount": 250.0})
77
+ result = await middleware.awrap_tool_call(request, handler)
78
+
79
+ assert calls == ["send_refund"]
80
+ assert result == "executed send_refund"
81
+
82
+
83
+ async def test_pre_node_denies_call_over_cap_and_handler_never_runs():
84
+ calls: list[str] = []
85
+ middleware = _build_pre_node_middleware()
86
+ handler = await _handler_records_call(calls)
87
+
88
+ request = _make_request("send_refund", {"amount": 5000.0}, call_id="call_2")
89
+ result = await middleware.awrap_tool_call(request, handler)
90
+
91
+ # The real proof of F1 pre-commitment: handler was never called at all.
92
+ assert calls == []
93
+ assert result.tool_call_id == "call_2"
94
+ assert result.status == "error"
95
+ assert "Denied by VSL governance" in result.content
96
+
97
+
98
+ async def test_invariant_denies_call_over_cap_and_handler_never_runs():
99
+ calls: list[str] = []
100
+ middleware = _build_invariant_middleware()
101
+ handler = await _handler_records_call(calls)
102
+
103
+ request = _make_request("send_refund", {"amount": 5000.0}, call_id="call_3")
104
+ result = await middleware.awrap_tool_call(request, handler)
105
+
106
+ assert calls == []
107
+ assert result.status == "error"
108
+
109
+
110
+ async def test_ungated_tool_passes_straight_through():
111
+ calls: list[str] = []
112
+ middleware = _build_pre_node_middleware() # only gates "send_refund"
113
+ handler = await _handler_records_call(calls)
114
+
115
+ request = _make_request("lookup_order", {"order_id": "ORD-1"})
116
+ result = await middleware.awrap_tool_call(request, handler)
117
+
118
+ assert calls == ["lookup_order"]
119
+ assert result == "executed lookup_order"