bylaw-python 0.4.0__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.
@@ -0,0 +1,114 @@
1
+ # Ledgix ALCV — Python SDK
2
+ # Agent-agnostic compliance shim for SOX 404 policy enforcement
3
+ #
4
+ # Recommended usage:
5
+ # import bylaw_python as ledgix
6
+ #
7
+ # ledgix.configure(agent_id="finance-agent")
8
+ #
9
+ # import tools
10
+ #
11
+ # ledgix.configure(agent_id="finance-agent")
12
+ # ledgix.auto_instrument(tools)
13
+ #
14
+ # Explicit API (advanced):
15
+ # from bylaw_python import LedgixClient, vault_enforce, VaultConfig
16
+ #
17
+ # client = LedgixClient()
18
+ #
19
+ # @vault_enforce(client, tool_name="stripe_refund")
20
+ # def process_refund(amount: float, reason: str, **kwargs):
21
+ # token = kwargs.get("_clearance").token
22
+ # ...
23
+
24
+ """Ledgix ALCV — agent-agnostic compliance shim for SOX 404 enforcement."""
25
+
26
+ from .client import LedgixClient
27
+ from .config import VaultConfig
28
+ from .enforce import (
29
+ VaultContext,
30
+ auto_instrument,
31
+ configure,
32
+ current_clearance,
33
+ current_token,
34
+ enforce,
35
+ tool,
36
+ vault_enforce,
37
+ )
38
+ from .manifest import Manifest, ManifestRule, load_manifest
39
+ from .exceptions import (
40
+ ClearanceDeniedError,
41
+ ManualReviewTimeoutError,
42
+ PolicyRegistrationError,
43
+ LedgixError,
44
+ QueueSaturatedError,
45
+ ReplayDetectedError,
46
+ ReviewPendingError,
47
+ TokenVerificationError,
48
+ VaultConnectionError,
49
+ )
50
+ from .pending import PendingApproval
51
+ from .webhook import verify_webhook
52
+ from .models import (
53
+ ClearanceRequest,
54
+ ClearanceResponse,
55
+ ConsistencyProof,
56
+ InclusionProof,
57
+ LedgerCheckpoint,
58
+ LedgerEntry,
59
+ LedgerKeyVersion,
60
+ LedgerManifest,
61
+ LedgerProofBundle,
62
+ LedgerVerificationResult,
63
+ PolicyRegistration,
64
+ PolicyRegistrationResponse,
65
+ )
66
+
67
+ __version__ = "0.4.0"
68
+
69
+ __all__ = [
70
+ # Core
71
+ "LedgixClient",
72
+ "VaultConfig",
73
+ # Low-code API
74
+ "configure",
75
+ "enforce",
76
+ "current_clearance",
77
+ "current_token",
78
+ # Manifest / auto-instrumentation
79
+ "auto_instrument",
80
+ "tool",
81
+ "load_manifest",
82
+ "Manifest",
83
+ "ManifestRule",
84
+ # Explicit API
85
+ "vault_enforce",
86
+ "VaultContext",
87
+ # Detach-mode / async approvals
88
+ "PendingApproval",
89
+ # Webhook verification
90
+ "verify_webhook",
91
+ # Models
92
+ "ClearanceRequest",
93
+ "ClearanceResponse",
94
+ "ConsistencyProof",
95
+ "InclusionProof",
96
+ "LedgerCheckpoint",
97
+ "LedgerEntry",
98
+ "LedgerKeyVersion",
99
+ "LedgerManifest",
100
+ "LedgerProofBundle",
101
+ "LedgerVerificationResult",
102
+ "PolicyRegistration",
103
+ "PolicyRegistrationResponse",
104
+ # Exceptions
105
+ "LedgixError",
106
+ "ClearanceDeniedError",
107
+ "ManualReviewTimeoutError",
108
+ "QueueSaturatedError",
109
+ "ReviewPendingError",
110
+ "VaultConnectionError",
111
+ "TokenVerificationError",
112
+ "ReplayDetectedError",
113
+ "PolicyRegistrationError",
114
+ ]
@@ -0,0 +1 @@
1
+ # Ledgix ALCV — Framework Adapters
@@ -0,0 +1,58 @@
1
+ # Ledgix ALCV — Adapter Core Helpers
2
+ # Shared scaffolding used by the LangChain, LlamaIndex, and CrewAI adapters.
3
+ # Framework-specific glue (callback handlers, sync vs async, error-translation
4
+ # policy) stays per-adapter — only the genuinely identical pieces live here.
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from ..client import LedgixClient
11
+ from ..enforce import _get_default_client
12
+ from ..models import ClearanceRequest
13
+
14
+
15
+ def resolve_client(client: LedgixClient | None) -> LedgixClient:
16
+ """Return the explicit client or fall back to the module-level default
17
+ configured via :func:`bylaw_python.configure`.
18
+ """
19
+ if client is not None:
20
+ return client
21
+ return _get_default_client()
22
+
23
+
24
+ def build_clearance_request(
25
+ *,
26
+ tool_name: str,
27
+ tool_args: dict[str, Any],
28
+ client: LedgixClient,
29
+ policy_id: str | None = None,
30
+ extra_context: dict[str, Any] | None = None,
31
+ data_categories: list[str] | None = None,
32
+ purpose: str | None = None,
33
+ processing_register_ref: str | None = None,
34
+ dataset_ref: str | None = None,
35
+ ) -> ClearanceRequest:
36
+ """Build a ClearanceRequest with adapter-agnostic defaults pulled from
37
+ ``client.config``. ``policy_id``, when set, is merged into ``context``
38
+ after ``extra_context`` so it always wins for that key.
39
+
40
+ Phase 2/6 fields (``data_categories``, ``purpose``,
41
+ ``processing_register_ref``, ``dataset_ref``) are forwarded as top-level
42
+ fields so the Vault's processing-register / dataset-lineage validators
43
+ can match them.
44
+ """
45
+ ctx: dict[str, Any] = dict(extra_context or {})
46
+ if policy_id:
47
+ ctx["policy_id"] = policy_id
48
+ return ClearanceRequest(
49
+ tool_name=tool_name,
50
+ tool_args=tool_args,
51
+ agent_id=client.config.agent_id,
52
+ session_id=client.config.session_id,
53
+ context=ctx,
54
+ data_categories=data_categories,
55
+ purpose=purpose,
56
+ processing_register_ref=processing_register_ref,
57
+ dataset_ref=dataset_ref,
58
+ )
@@ -0,0 +1,99 @@
1
+ # Ledgix ALCV — CrewAI Adapter
2
+ # Wraps CrewAI tools with Vault clearance enforcement
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any, Type
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from ..client import LedgixClient
11
+ from ..exceptions import ClearanceDeniedError
12
+ from ._core import build_clearance_request, resolve_client
13
+
14
+ try:
15
+ from crewai.tools import BaseTool as CrewAIBaseTool
16
+ except ImportError as exc:
17
+ raise ImportError(
18
+ "CrewAI adapter requires crewai. "
19
+ "Install with: pip install bylaw-python[crewai]"
20
+ ) from exc
21
+
22
+
23
+ class LedgixCrewAITool(CrewAIBaseTool):
24
+ """Wraps a CrewAI tool with Vault clearance enforcement.
25
+
26
+ Usage with explicit client::
27
+
28
+ from crewai.tools import BaseTool
29
+ from bylaw_python.adapters.crewai import LedgixCrewAITool
30
+
31
+ guarded = LedgixCrewAITool.wrap(client, MyTool())
32
+
33
+ Usage after :func:`bylaw_python.configure`::
34
+
35
+ guarded = LedgixCrewAITool.wrap(MyTool())
36
+ """
37
+
38
+ name: str = ""
39
+ description: str = ""
40
+ _inner_tool: CrewAIBaseTool
41
+ _client: LedgixClient | None
42
+ _policy_id: str | None
43
+
44
+ class Config:
45
+ arbitrary_types_allowed = True
46
+ underscore_attrs_are_private = True
47
+
48
+ def __init__(
49
+ self,
50
+ inner_tool: CrewAIBaseTool,
51
+ client: LedgixClient | None = None,
52
+ *,
53
+ policy_id: str | None = None,
54
+ ) -> None:
55
+ super().__init__(
56
+ name=f"ledgix_{inner_tool.name}",
57
+ description=inner_tool.description,
58
+ )
59
+ self._inner_tool = inner_tool
60
+ self._client = client
61
+ self._policy_id = policy_id
62
+
63
+ def _resolve_client(self) -> LedgixClient:
64
+ return resolve_client(self._client)
65
+
66
+ @classmethod
67
+ def wrap(
68
+ cls,
69
+ client_or_tool: LedgixClient | CrewAIBaseTool,
70
+ tool: CrewAIBaseTool | None = None,
71
+ *,
72
+ policy_id: str | None = None,
73
+ ) -> LedgixCrewAITool:
74
+ """Convenience factory to wrap a CrewAI tool.
75
+
76
+ Supports two call signatures:
77
+
78
+ - ``LedgixCrewAITool.wrap(client, tool, policy_id=...)`` — explicit client
79
+ - ``LedgixCrewAITool.wrap(tool, policy_id=...)`` — uses global client from :func:`bylaw_python.configure`
80
+ """
81
+ if isinstance(client_or_tool, LedgixClient):
82
+ return cls(inner_tool=tool, client=client_or_tool, policy_id=policy_id) # type: ignore[arg-type]
83
+ return cls(inner_tool=client_or_tool, client=None, policy_id=policy_id)
84
+
85
+ def _run(self, **kwargs: Any) -> Any:
86
+ client = self._resolve_client()
87
+ request = build_clearance_request(
88
+ tool_name=self._inner_tool.name,
89
+ tool_args=kwargs,
90
+ client=client,
91
+ policy_id=self._policy_id,
92
+ )
93
+
94
+ try:
95
+ client.request_clearance(request)
96
+ except ClearanceDeniedError as exc:
97
+ return f"BLOCKED: Vault denied this action — {exc.reason}"
98
+
99
+ return self._inner_tool._run(**kwargs)
@@ -0,0 +1,167 @@
1
+ # Ledgix ALCV — LangChain Adapter
2
+ # Provides a callback handler and tool wrapper for LangChain integration
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ from ..client import LedgixClient
9
+ from ..exceptions import ClearanceDeniedError
10
+ from ._core import build_clearance_request, resolve_client
11
+
12
+ try:
13
+ from langchain_core.callbacks import BaseCallbackHandler
14
+ from langchain_core.tools import BaseTool, ToolException
15
+ except ImportError as exc:
16
+ raise ImportError(
17
+ "LangChain adapter requires langchain-core. "
18
+ "Install with: pip install bylaw-python[langchain]"
19
+ ) from exc
20
+
21
+
22
+ class LedgixCallbackHandler(BaseCallbackHandler):
23
+ """LangChain callback handler that intercepts tool calls for Vault clearance.
24
+
25
+ Usage::
26
+
27
+ from bylaw_python.adapters.langchain import LedgixCallbackHandler
28
+
29
+ handler = LedgixCallbackHandler(client)
30
+ agent = create_agent(callbacks=[handler])
31
+
32
+ If :func:`bylaw_python.configure` has been called, *client* may be omitted::
33
+
34
+ handler = LedgixCallbackHandler()
35
+ """
36
+
37
+ def __init__(self, client: LedgixClient | None = None, *, policy_id: str | None = None) -> None:
38
+ self._client = client
39
+ self.policy_id = policy_id
40
+
41
+ @property
42
+ def client(self) -> LedgixClient:
43
+ return resolve_client(self._client)
44
+
45
+ def on_tool_start(
46
+ self,
47
+ serialized: dict[str, Any],
48
+ input_str: str,
49
+ *,
50
+ run_id: Any = None,
51
+ parent_run_id: Any = None,
52
+ tags: list[str] | None = None,
53
+ metadata: dict[str, Any] | None = None,
54
+ inputs: dict[str, Any] | None = None,
55
+ **kwargs: Any,
56
+ ) -> None:
57
+ """Intercept tool start and request Vault clearance."""
58
+ tool_name = serialized.get("name", "unknown_tool")
59
+ tool_args = inputs or {"input": input_str}
60
+
61
+ extra_context = {"langchain_metadata": metadata} if metadata else None
62
+ request = build_clearance_request(
63
+ tool_name=tool_name,
64
+ tool_args=tool_args,
65
+ client=self.client,
66
+ policy_id=self.policy_id,
67
+ extra_context=extra_context,
68
+ )
69
+
70
+ # This will raise ClearanceDeniedError if denied
71
+ self.client.request_clearance(request)
72
+
73
+
74
+ class LedgixTool(BaseTool):
75
+ """Wraps an existing LangChain tool with Vault clearance enforcement.
76
+
77
+ Usage with explicit client::
78
+
79
+ from langchain_community.tools import SomeTool
80
+ from bylaw_python.adapters.langchain import LedgixTool
81
+
82
+ guarded_tool = LedgixTool.wrap(client, SomeTool(), policy_id="refund-policy")
83
+
84
+ Usage after :func:`bylaw_python.configure`::
85
+
86
+ guarded_tool = LedgixTool.wrap(SomeTool(), policy_id="refund-policy")
87
+ """
88
+
89
+ name: str = ""
90
+ description: str = ""
91
+ _inner_tool: BaseTool
92
+ _client: LedgixClient | None
93
+ _policy_id: str | None
94
+
95
+ class Config:
96
+ arbitrary_types_allowed = True
97
+ underscore_attrs_are_private = True
98
+
99
+ def __init__(
100
+ self,
101
+ inner_tool: BaseTool,
102
+ client: LedgixClient | None = None,
103
+ *,
104
+ policy_id: str | None = None,
105
+ ) -> None:
106
+ super().__init__(
107
+ name=f"ledgix_{inner_tool.name}",
108
+ description=inner_tool.description,
109
+ )
110
+ self._inner_tool = inner_tool
111
+ self._client = client
112
+ self._policy_id = policy_id
113
+
114
+ def _resolve_client(self) -> LedgixClient:
115
+ return resolve_client(self._client)
116
+
117
+ @classmethod
118
+ def wrap(
119
+ cls,
120
+ client_or_tool: LedgixClient | BaseTool,
121
+ tool: BaseTool | None = None,
122
+ *,
123
+ policy_id: str | None = None,
124
+ ) -> LedgixTool:
125
+ """Convenience factory to wrap a tool.
126
+
127
+ Supports two call signatures:
128
+
129
+ - ``LedgixTool.wrap(client, tool, policy_id=...)`` — explicit client
130
+ - ``LedgixTool.wrap(tool, policy_id=...)`` — uses global client from :func:`bylaw_python.configure`
131
+ """
132
+ if isinstance(client_or_tool, LedgixClient):
133
+ return cls(inner_tool=tool, client=client_or_tool, policy_id=policy_id) # type: ignore[arg-type]
134
+ # client_or_tool is actually the tool; no explicit client
135
+ return cls(inner_tool=client_or_tool, client=None, policy_id=policy_id)
136
+
137
+ def _run(self, *args: Any, **kwargs: Any) -> Any:
138
+ client = self._resolve_client()
139
+ request = build_clearance_request(
140
+ tool_name=self._inner_tool.name,
141
+ tool_args=kwargs or ({"input": args[0]} if args else {}),
142
+ client=client,
143
+ policy_id=self._policy_id,
144
+ )
145
+
146
+ try:
147
+ client.request_clearance(request)
148
+ except ClearanceDeniedError as exc:
149
+ raise ToolException(f"Vault denied: {exc.reason}") from exc
150
+
151
+ return self._inner_tool._run(*args, **kwargs)
152
+
153
+ async def _arun(self, *args: Any, **kwargs: Any) -> Any:
154
+ client = self._resolve_client()
155
+ request = build_clearance_request(
156
+ tool_name=self._inner_tool.name,
157
+ tool_args=kwargs or ({"input": args[0]} if args else {}),
158
+ client=client,
159
+ policy_id=self._policy_id,
160
+ )
161
+
162
+ try:
163
+ await client.arequest_clearance(request)
164
+ except ClearanceDeniedError as exc:
165
+ raise ToolException(f"Vault denied: {exc.reason}") from exc
166
+
167
+ return await self._inner_tool._arun(*args, **kwargs)
@@ -0,0 +1,90 @@
1
+ # Ledgix ALCV — LlamaIndex Adapter
2
+ # Wraps LlamaIndex tools with Vault clearance enforcement
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
7
+
8
+ from ..client import LedgixClient
9
+ from ._core import build_clearance_request, resolve_client
10
+
11
+ try:
12
+ from llama_index.core.tools import FunctionTool, ToolMetadata, ToolOutput
13
+ except ImportError as exc:
14
+ raise ImportError(
15
+ "LlamaIndex adapter requires llama-index-core. "
16
+ "Install with: pip install bylaw-python[llamaindex]"
17
+ ) from exc
18
+
19
+
20
+ class LedgixToolWrapper:
21
+ """Wraps a LlamaIndex tool with Vault clearance enforcement.
22
+
23
+ Usage with explicit client::
24
+
25
+ from llama_index.core.tools import FunctionTool
26
+ from bylaw_python.adapters.llamaindex import LedgixToolWrapper
27
+
28
+ tool = FunctionTool.from_defaults(fn=my_tool, name="search")
29
+ guarded = LedgixToolWrapper(client, tool)
30
+
31
+ Usage after :func:`bylaw_python.configure`::
32
+
33
+ guarded = LedgixToolWrapper(tool=tool)
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ client: LedgixClient | None = None,
39
+ tool: FunctionTool | None = None,
40
+ *,
41
+ policy_id: str | None = None,
42
+ ) -> None:
43
+ self._client = client
44
+ self._inner_tool = tool
45
+ self._policy_id = policy_id
46
+
47
+ # Create the wrapped tool
48
+ self.tool = FunctionTool.from_defaults(
49
+ fn=self._guarded_call,
50
+ name=f"ledgix_{tool.metadata.name}", # type: ignore[union-attr]
51
+ description=tool.metadata.description or "", # type: ignore[union-attr]
52
+ )
53
+
54
+ def _resolve_client(self) -> LedgixClient:
55
+ return resolve_client(self._client)
56
+
57
+ def _guarded_call(self, **kwargs: Any) -> Any:
58
+ """Wrapper that requests clearance before calling the inner tool."""
59
+ client = self._resolve_client()
60
+ request = build_clearance_request(
61
+ tool_name=self._inner_tool.metadata.name, # type: ignore[union-attr]
62
+ tool_args=kwargs,
63
+ client=client,
64
+ policy_id=self._policy_id,
65
+ )
66
+
67
+ client.request_clearance(request)
68
+ return self._inner_tool.call(**kwargs) # type: ignore[union-attr]
69
+
70
+
71
+ def wrap_tool(
72
+ client_or_tool: LedgixClient | FunctionTool,
73
+ tool: FunctionTool | None = None,
74
+ *,
75
+ policy_id: str | None = None,
76
+ ) -> FunctionTool:
77
+ """Wrap a LlamaIndex tool with Vault clearance enforcement.
78
+
79
+ Returns the guarded FunctionTool ready for use in an agent.
80
+
81
+ Supports two call signatures:
82
+
83
+ - ``wrap_tool(client, tool, policy_id=...)`` — explicit client
84
+ - ``wrap_tool(tool, policy_id=...)`` — uses global client from :func:`bylaw_python.configure`
85
+ """
86
+ if isinstance(client_or_tool, LedgixClient):
87
+ wrapper = LedgixToolWrapper(client_or_tool, tool, policy_id=policy_id)
88
+ else:
89
+ wrapper = LedgixToolWrapper(None, client_or_tool, policy_id=policy_id)
90
+ return wrapper.tool