agenticrail 0.1.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,6 @@
1
+ """AgenticRail SDK — deterministic sequence enforcement for AI agents."""
2
+
3
+ from agenticrail.client import RailClient, RailDecision, RailDenied, RailSequence
4
+
5
+ __all__ = ["RailClient", "RailDecision", "RailDenied", "RailSequence"]
6
+ __version__ = "0.1.0"
agenticrail/client.py ADDED
@@ -0,0 +1,184 @@
1
+ """AgenticRail gate client — core HTTP implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import uuid
7
+ from dataclasses import dataclass, field
8
+ from typing import List, Optional
9
+
10
+ import requests
11
+
12
+
13
+ class RailDenied(Exception):
14
+ """Raised when the gate returns DENY or HALT."""
15
+
16
+ def __init__(self, decision: str, reasons: List[str], pack_id: str, step: str):
17
+ self.decision = decision
18
+ self.reasons = reasons
19
+ self.pack_id = pack_id
20
+ self.step = step
21
+ super().__init__(f"{decision} at '{step}': {reasons}")
22
+
23
+
24
+ @dataclass
25
+ class RailDecision:
26
+ decision: str # ALLOW | DENY | HALT
27
+ pack_id: str
28
+ reasons: List[str]
29
+ executed: bool
30
+ sequence_id: str
31
+ step: str
32
+ receipt: dict = field(default_factory=dict)
33
+
34
+ @property
35
+ def allowed(self) -> bool:
36
+ return self.decision == "ALLOW"
37
+
38
+
39
+ class RailSequence:
40
+ """
41
+ Stateful sequence helper. Tracks step_order and sends it only on the first
42
+ call — removes boilerplate from multi-step workflows.
43
+
44
+ Usage:
45
+ seq = client.sequence("my-seq-001", step_order=["step_a", "step_b", "step_c"])
46
+ seq.next("step_a")
47
+ seq.next("step_b")
48
+ seq.next("step_c") # seals sequence
49
+ """
50
+
51
+ def __init__(self, client: "RailClient", sequence_id: str, step_order: List[str]):
52
+ self._client = client
53
+ self.sequence_id = sequence_id
54
+ self.step_order = step_order
55
+
56
+ def next(
57
+ self,
58
+ step: str,
59
+ *,
60
+ action_type: str = "CHECK_STATE",
61
+ action: Optional[str] = None,
62
+ inputs: Optional[dict] = None,
63
+ raise_on_deny: bool = True,
64
+ ) -> RailDecision:
65
+ # step_order must be sent on every call — the gate reads it from each
66
+ # payload to resolve the step index; it does not persist it server-side.
67
+ return self._client.evaluate(
68
+ self.sequence_id,
69
+ step,
70
+ action_type=action_type,
71
+ action=action,
72
+ inputs=inputs,
73
+ step_order=self.step_order,
74
+ raise_on_deny=raise_on_deny,
75
+ )
76
+
77
+
78
+ class RailClient:
79
+ """
80
+ Thin client for the AgenticRail gate API.
81
+
82
+ Usage:
83
+ client = RailClient(api_key="DEMO-AGENTICRAIL-PUBLIC-2026")
84
+
85
+ # Direct calls — manage step_order yourself
86
+ client.evaluate("seq-001", "verify_identity", step_order=[...])
87
+ client.evaluate("seq-001", "assess_risk")
88
+
89
+ # Or use RailSequence helper
90
+ seq = client.sequence("seq-001", step_order=["verify_identity", "assess_risk"])
91
+ seq.next("verify_identity")
92
+ seq.next("assess_risk")
93
+ """
94
+
95
+ DEFAULT_BASE_URL = "https://api.agenticrail.nz/v1/evaluate"
96
+
97
+ def __init__(
98
+ self,
99
+ api_key: str,
100
+ *,
101
+ model_id: str = "agent",
102
+ base_url: Optional[str] = None,
103
+ timeout: int = 10,
104
+ ):
105
+ self.api_key = api_key
106
+ self.model_id = model_id
107
+ self.base_url = (base_url or self.DEFAULT_BASE_URL).rstrip("/")
108
+ self.timeout = timeout
109
+
110
+ def evaluate(
111
+ self,
112
+ sequence_id: str,
113
+ step: str,
114
+ *,
115
+ action_type: str = "CHECK_STATE",
116
+ action: Optional[str] = None,
117
+ inputs: Optional[dict] = None,
118
+ step_order: Optional[List[str]] = None,
119
+ raise_on_deny: bool = True,
120
+ ) -> RailDecision:
121
+ """
122
+ Evaluate one step of a sequence against the AgenticRail gate.
123
+
124
+ Args:
125
+ sequence_id: Unique identifier for this agent run.
126
+ step: Step name (must equal function; must match step_order).
127
+ action_type: One of CHECK_STATE, VALIDATE_INPUT, RECORD_RESULT, etc.
128
+ action: Human-readable action label (defaults to step name).
129
+ inputs: Arbitrary metadata attached to this step.
130
+ step_order: Full ordered list of step names — send only on the first step.
131
+ raise_on_deny: If True (default), raise RailDenied on DENY or HALT.
132
+
133
+ Returns:
134
+ RailDecision with decision, pack_id, receipt, and reasons.
135
+
136
+ Raises:
137
+ RailDenied: If decision is DENY or HALT and raise_on_deny is True.
138
+ requests.HTTPError: On non-2xx HTTP responses.
139
+ """
140
+ payload: dict = {
141
+ "schema_version": "1.0",
142
+ "model_id": self.model_id,
143
+ "sequence_id": sequence_id,
144
+ "step": step,
145
+ "function": step,
146
+ "action_type": action_type,
147
+ "action": action if action is not None else step,
148
+ "inputs": inputs or {},
149
+ "nonce": str(uuid.uuid4()),
150
+ "ts_ms": int(time.time() * 1000),
151
+ }
152
+ if step_order is not None:
153
+ payload["step_order"] = step_order
154
+
155
+ resp = requests.post(
156
+ self.base_url,
157
+ json=payload,
158
+ headers={
159
+ "Authorization": f"Bearer {self.api_key}",
160
+ "Content-Type": "application/json",
161
+ },
162
+ timeout=self.timeout,
163
+ )
164
+ resp.raise_for_status()
165
+ data = resp.json()
166
+
167
+ decision = RailDecision(
168
+ decision=data["decision"],
169
+ pack_id=data.get("pack_id", ""),
170
+ reasons=data.get("reasons", []),
171
+ executed=data.get("executed", False),
172
+ sequence_id=data.get("sequence_id", sequence_id),
173
+ step=data.get("step", step),
174
+ receipt=data.get("receipt", {}),
175
+ )
176
+
177
+ if raise_on_deny and not decision.allowed:
178
+ raise RailDenied(decision.decision, decision.reasons, decision.pack_id, step)
179
+
180
+ return decision
181
+
182
+ def sequence(self, sequence_id: str, step_order: List[str]) -> RailSequence:
183
+ """Create a stateful sequence helper that manages step_order automatically."""
184
+ return RailSequence(self, sequence_id, step_order)
@@ -0,0 +1,3 @@
1
+ # Framework integrations are imported explicitly:
2
+ # from agenticrail.integrations.langgraph import LangGraphRail
3
+ # from agenticrail.integrations.crewai import CrewRailGuard
@@ -0,0 +1,141 @@
1
+ """CrewAI integration for AgenticRail."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any, List, Optional
7
+
8
+ from agenticrail.client import RailClient, RailDenied
9
+
10
+
11
+ class CrewRailGuard:
12
+ """
13
+ Guards a CrewAI Crew by calling the AgenticRail gate before each task executes.
14
+
15
+ Usage:
16
+ from agenticrail import RailClient
17
+ from agenticrail.integrations.crewai import CrewRailGuard
18
+
19
+ client = RailClient(api_key="YOUR_KEY")
20
+ guard = CrewRailGuard(
21
+ client,
22
+ step_order=["research_task", "analysis_task", "write_task"],
23
+ )
24
+
25
+ crew = Crew(
26
+ agents=[researcher, analyst, writer],
27
+ tasks=[research_task, analysis_task, write_task],
28
+ )
29
+
30
+ # Call guard.kickoff() instead of crew.kickoff()
31
+ result = guard.kickoff(crew, inputs={"topic": "EU AI Act enforcement"})
32
+
33
+ How it works:
34
+ 1. guard.kickoff() gates step[0] before crew starts. Raises RailDenied on DENY.
35
+ 2. A task_callback is injected into the crew. After each task completes, the
36
+ callback gates the NEXT step before CrewAI moves to it. If the gate denies,
37
+ a flag is set and a RailDenied is raised after kickoff returns.
38
+ 3. The sequence is sealed when all tasks complete (the step_order's last step
39
+ is evaluated at the start of the last task).
40
+
41
+ Note on timing:
42
+ CrewAI's task_callback fires synchronously between tasks. However, if CrewAI
43
+ swallows the exception from task_callback, the denied task may still execute.
44
+ In that case, guard.kickoff() raises RailDenied after crew.kickoff() returns,
45
+ preserving the gate decision in the audit trail even if execution wasn't blocked.
46
+
47
+ For hard blocking, wrap task execution directly via guard.gate() before each
48
+ task in a custom orchestration loop (see examples/crewai_example.py).
49
+
50
+ New sequence per run:
51
+ Each call to guard.kickoff() generates a fresh sequence_id unless you pass
52
+ sequence_id= explicitly. This ensures each crew run is independently tracked.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ client: RailClient,
58
+ step_order: List[str],
59
+ *,
60
+ sequence_id: Optional[str] = None,
61
+ action_type: str = "CHECK_STATE",
62
+ ):
63
+ self._client = client
64
+ self.step_order = step_order
65
+ self._action_type = action_type
66
+ self._fixed_sequence_id = sequence_id
67
+ # Runtime state (reset on each kickoff)
68
+ self._sequence_id: str = ""
69
+ self._step_idx: int = 0
70
+ self._denied: Optional[RailDenied] = None
71
+
72
+ def _gate(self, step_name: str) -> None:
73
+ # step_order sent on every call — gate reads it from each payload
74
+ self._client.evaluate(
75
+ self._sequence_id,
76
+ step_name,
77
+ action_type=self._action_type,
78
+ action=f"execute {step_name}",
79
+ step_order=self.step_order,
80
+ raise_on_deny=True,
81
+ )
82
+
83
+ def gate(self, step_name: str) -> None:
84
+ """
85
+ Explicitly gate a single step. Use this for custom orchestration loops
86
+ where you control task execution directly.
87
+ """
88
+ if not self._sequence_id:
89
+ self._sequence_id = self._fixed_sequence_id or str(uuid.uuid4())
90
+ self._gate(step_name)
91
+ self._step_idx += 1
92
+
93
+ def reset(self, sequence_id: Optional[str] = None) -> None:
94
+ """Reset guard state for a new crew run."""
95
+ self._sequence_id = sequence_id or self._fixed_sequence_id or str(uuid.uuid4())
96
+ self._step_idx = 0
97
+ self._denied = None
98
+
99
+ def kickoff(self, crew: Any, **kwargs: Any) -> Any:
100
+ """
101
+ Gate-enabled replacement for crew.kickoff().
102
+
103
+ Gates step[0] before the crew starts. Injects a task_callback that gates
104
+ each subsequent step between tasks. Raises RailDenied if any step is denied.
105
+ """
106
+ self.reset()
107
+
108
+ # Gate the first step — raises immediately on DENY/HALT
109
+ first_step = self.step_order[self._step_idx]
110
+ self._gate(first_step)
111
+ self._step_idx += 1
112
+
113
+ # Capture any existing callback so we can chain it
114
+ existing_callback = getattr(crew, "task_callback", None)
115
+
116
+ def _task_callback(task_output: Any) -> None:
117
+ if self._denied:
118
+ return
119
+
120
+ # Gate the next step (if any remain)
121
+ if self._step_idx < len(self.step_order):
122
+ next_step = self.step_order[self._step_idx]
123
+ self._step_idx += 1
124
+ try:
125
+ self._gate(next_step)
126
+ except RailDenied as e:
127
+ self._denied = e
128
+ # Cannot reliably stop CrewAI from inside a callback,
129
+ # but we record the denial and raise after kickoff.
130
+
131
+ if existing_callback is not None:
132
+ existing_callback(task_output)
133
+
134
+ crew.task_callback = _task_callback
135
+
136
+ result = crew.kickoff(**kwargs)
137
+
138
+ if self._denied:
139
+ raise self._denied
140
+
141
+ return result
@@ -0,0 +1,115 @@
1
+ """LangGraph integration for AgenticRail."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import uuid
7
+ from functools import wraps
8
+ from typing import Any, Callable, Dict, List, Optional
9
+
10
+ from agenticrail.client import RailClient
11
+
12
+
13
+ class LangGraphRail:
14
+ """
15
+ Guards a LangGraph StateGraph by calling the AgenticRail gate before each
16
+ node executes. Each graph invocation gets its own sequence_id derived from
17
+ LangGraph's thread_id, so concurrent runs are isolated.
18
+
19
+ Usage:
20
+ from agenticrail import RailClient
21
+ from agenticrail.integrations.langgraph import LangGraphRail
22
+
23
+ client = RailClient(api_key="YOUR_KEY")
24
+ rail = LangGraphRail(client, step_order=["research", "analyze", "write"])
25
+
26
+ # Wrap your nodes at add_node time
27
+ builder.add_node("research", rail.wrap("research", research_fn))
28
+ builder.add_node("analyze", rail.wrap("analyze", analyze_fn))
29
+ builder.add_node("write", rail.wrap("write", write_fn))
30
+
31
+ graph = builder.compile()
32
+
33
+ # thread_id becomes the sequence_id — each run is isolated
34
+ result = graph.invoke(
35
+ {"messages": [...]},
36
+ config={"configurable": {"thread_id": "run-abc-001"}},
37
+ )
38
+
39
+ Gate enforcement:
40
+ - ALLOW → node function executes normally.
41
+ - DENY → RailDenied raised; LangGraph halts the graph.
42
+ - HALT → RailDenied raised; LangGraph halts the graph.
43
+
44
+ Sequence isolation:
45
+ Each unique thread_id gets its own AgenticRail sequence. If you invoke
46
+ the same graph twice with different thread_ids, both are tracked
47
+ independently. If thread_id is not provided in config, a random UUID is
48
+ used (not recommended — you lose the audit trail correlation).
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ client: RailClient,
54
+ step_order: List[str],
55
+ *,
56
+ fallback_sequence_id: Optional[str] = None,
57
+ ):
58
+ self._client = client
59
+ self.step_order = step_order
60
+ self._fallback_sequence_id = fallback_sequence_id or str(uuid.uuid4())
61
+
62
+ def _resolve_sequence_id(self, config: Any) -> str:
63
+ if config and isinstance(config, dict):
64
+ configurable = config.get("configurable") or {}
65
+ thread_id = (
66
+ configurable.get("thread_id")
67
+ or configurable.get("sequence_id")
68
+ )
69
+ if thread_id:
70
+ return str(thread_id)
71
+ return self._fallback_sequence_id
72
+
73
+ def wrap(
74
+ self,
75
+ step_name: str,
76
+ fn: Callable,
77
+ *,
78
+ action_type: str = "CHECK_STATE",
79
+ ) -> Callable:
80
+ """
81
+ Wrap a LangGraph node function with AgenticRail pre-execution gate enforcement.
82
+
83
+ Args:
84
+ step_name: Must match the corresponding entry in step_order.
85
+ fn: The original node function (state -> dict, or state, config -> dict).
86
+ action_type: Gate action type (default CHECK_STATE).
87
+
88
+ Returns:
89
+ Wrapped node function with identical signature.
90
+ """
91
+ # Check if the original function accepts a config argument
92
+ try:
93
+ sig = inspect.signature(fn)
94
+ _fn_takes_config = len(sig.parameters) > 1
95
+ except (ValueError, TypeError):
96
+ _fn_takes_config = False
97
+
98
+ @wraps(fn)
99
+ def guarded(state: Any, config: Any = None) -> Any:
100
+ seq_id = self._resolve_sequence_id(config)
101
+ # step_order sent on every call — gate reads it from each payload
102
+ self._client.evaluate(
103
+ seq_id,
104
+ step_name,
105
+ action_type=action_type,
106
+ action=f"execute {step_name}",
107
+ step_order=self.step_order,
108
+ raise_on_deny=True,
109
+ )
110
+
111
+ if _fn_takes_config and config is not None:
112
+ return fn(state, config)
113
+ return fn(state)
114
+
115
+ return guarded
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.4
2
+ Name: agenticrail
3
+ Version: 0.1.0
4
+ Summary: AgenticRail SDK — deterministic sequence enforcement for AI agents
5
+ Project-URL: Homepage, https://agenticrail.nz
6
+ Project-URL: Documentation, https://agenticrail.nz/docs
7
+ Project-URL: Repository, https://github.com/MSMD-RUA/agenticrail-sdk
8
+ Project-URL: Bug Tracker, https://github.com/MSMD-RUA/agenticrail-sdk/issues
9
+ License: MIT
10
+ Keywords: ai-agents,compliance,crewai,enforcement,langgraph,sequence
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: requests>=2.28
22
+ Provides-Extra: all
23
+ Requires-Dist: crewai>=0.60; extra == 'all'
24
+ Requires-Dist: langchain-core>=0.1; extra == 'all'
25
+ Requires-Dist: langgraph>=0.1; extra == 'all'
26
+ Provides-Extra: crewai
27
+ Requires-Dist: crewai>=0.60; extra == 'crewai'
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-mock; extra == 'dev'
30
+ Requires-Dist: pytest>=7; extra == 'dev'
31
+ Requires-Dist: responses; extra == 'dev'
32
+ Provides-Extra: langgraph
33
+ Requires-Dist: langchain-core>=0.1; extra == 'langgraph'
34
+ Requires-Dist: langgraph>=0.1; extra == 'langgraph'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # agenticrail
38
+
39
+ AgenticRail Python SDK — deterministic sequence enforcement for AI agents.
40
+
41
+ Gate every step of your agent workflow before it executes. Cryptographic receipts. Zero enforcement failures.
42
+
43
+ ```
44
+ pip install agenticrail
45
+ ```
46
+
47
+ ---
48
+
49
+ ## What it does
50
+
51
+ AgenticRail sits beneath your agent and enforces that steps run in the right order, with no skips, no replays, and no re-entry after a sequence is sealed. Every decision — ALLOW, DENY, or HALT — produces a cryptographically signed receipt stored at the edge.
52
+
53
+ Three verdicts:
54
+ - **ALLOW** — step is clear, your agent code executes
55
+ - **DENY** — enforcement violation (wrong order, replay, sealed), execution blocked
56
+ - **HALT** — hard stop (injection attempt, poison payload detected), execution blocked
57
+
58
+ ---
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ # Core client only
64
+ pip install agenticrail
65
+
66
+ # With LangGraph support
67
+ pip install "agenticrail[langgraph]"
68
+
69
+ # With CrewAI support
70
+ pip install "agenticrail[crewai]"
71
+
72
+ # Everything
73
+ pip install "agenticrail[all]"
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Quick start — any framework
79
+
80
+ ```python
81
+ from agenticrail import RailClient
82
+
83
+ client = RailClient(api_key="DEMO-AGENTICRAIL-PUBLIC-2026")
84
+
85
+ # RailSequence sends step_order on every call — the gate reads it from each payload
86
+ seq = client.sequence(
87
+ sequence_id="my-agent-run-001",
88
+ step_order=["verify_identity", "assess_risk", "execute_transfer", "audit_ledger"],
89
+ )
90
+
91
+ seq.next("verify_identity") # → ALLOW
92
+ seq.next("assess_risk") # → ALLOW
93
+ seq.next("execute_transfer") # → ALLOW
94
+ seq.next("audit_ledger") # → ALLOW (seals sequence)
95
+
96
+ # Any out-of-order, replay, or post-seal call raises RailDenied
97
+ seq.next("verify_identity") # → raises RailDenied: SEALED_SEQUENCE
98
+ ```
99
+
100
+ Get a production API key at [agenticrail.nz](https://agenticrail.nz).
101
+
102
+ ---
103
+
104
+ ## LangGraph integration
105
+
106
+ Wrap each node at `add_node` time. Uses `thread_id` from LangGraph config as the sequence_id — each graph invocation is isolated.
107
+
108
+ ```python
109
+ from langgraph.graph import StateGraph, END
110
+ from agenticrail import RailClient
111
+ from agenticrail.integrations.langgraph import LangGraphRail
112
+
113
+ client = RailClient(api_key="YOUR_KEY")
114
+ rail = LangGraphRail(
115
+ client,
116
+ step_order=["research", "analyze", "write_report", "review"],
117
+ )
118
+
119
+ builder = StateGraph(AgentState)
120
+
121
+ # Wrap each node — gate fires before node execution
122
+ builder.add_node("research", rail.wrap("research", research_fn))
123
+ builder.add_node("analyze", rail.wrap("analyze", analyze_fn))
124
+ builder.add_node("write_report", rail.wrap("write_report", write_report_fn))
125
+ builder.add_node("review", rail.wrap("review", review_fn))
126
+
127
+ builder.set_entry_point("research")
128
+ builder.add_edge("research", "analyze")
129
+ builder.add_edge("analyze", "write_report")
130
+ builder.add_edge("write_report", "review")
131
+ builder.add_edge("review", END)
132
+
133
+ graph = builder.compile()
134
+
135
+ # thread_id → sequence_id: each invocation is independently tracked and verifiable
136
+ result = graph.invoke(
137
+ {"query": "EU AI Act compliance checklist"},
138
+ config={"configurable": {"thread_id": "run-2026-001"}},
139
+ )
140
+ ```
141
+
142
+ **What happens on DENY or HALT:**
143
+ `rail.wrap()` raises `RailDenied` inside the node. LangGraph catches unhandled node exceptions and halts graph execution. No subsequent nodes run.
144
+
145
+ **Concurrent runs:**
146
+ Each `thread_id` is a separate sequence. Concurrent graph invocations with different thread IDs do not interfere — the gate tracks them independently via Durable Objects.
147
+
148
+ ---
149
+
150
+ ## CrewAI integration
151
+
152
+ Use `guard.kickoff()` instead of `crew.kickoff()`. Step[0] is gated before the crew starts; subsequent steps are gated via a task callback injected between tasks.
153
+
154
+ ```python
155
+ from crewai import Crew
156
+ from agenticrail import RailClient
157
+ from agenticrail.integrations.crewai import CrewRailGuard
158
+
159
+ client = RailClient(api_key="YOUR_KEY")
160
+ guard = CrewRailGuard(
161
+ client,
162
+ step_order=["research_task", "analysis_task", "write_task"],
163
+ )
164
+
165
+ crew = Crew(
166
+ agents=[researcher, analyst, writer],
167
+ tasks=[research_task, analysis_task, write_task],
168
+ )
169
+
170
+ # Drop-in replacement for crew.kickoff()
171
+ # Each call generates a fresh sequence_id — each run is independently tracked
172
+ result = guard.kickoff(crew, inputs={"topic": "AI governance"})
173
+ ```
174
+
175
+ **Note on blocking:** CrewAI's `task_callback` fires synchronously between tasks, so the gate can block task N before task N starts. However, if CrewAI swallows exceptions in callbacks, a denied task may still execute. In that case, `guard.kickoff()` raises `RailDenied` after the crew finishes, preserving the denial in the audit trail.
176
+
177
+ For hard blocking (guaranteed pre-execution), use `guard.gate()` in a custom orchestration loop:
178
+
179
+ ```python
180
+ guard.reset()
181
+ for step, task_fn in zip(STEP_ORDER, task_functions):
182
+ guard.gate(step) # raises RailDenied immediately on DENY
183
+ task_fn()
184
+ ```
185
+
186
+ ---
187
+
188
+ ## API reference
189
+
190
+ ### `RailClient`
191
+
192
+ ```python
193
+ client = RailClient(
194
+ api_key="...", # Required. Bearer token for the wrapper API.
195
+ model_id="agent", # Optional. Identifies your agent in receipts.
196
+ base_url=None, # Optional. Defaults to api.agenticrail.nz/v1/evaluate.
197
+ timeout=10, # Optional. Request timeout in seconds.
198
+ )
199
+ ```
200
+
201
+ #### `client.evaluate(sequence_id, step, *, ...)`
202
+
203
+ ```python
204
+ decision = client.evaluate(
205
+ sequence_id="my-run-001",
206
+ step="verify_identity",
207
+ action_type="CHECK_STATE", # Optional. Default: CHECK_STATE.
208
+ action="verify user identity", # Optional. Human-readable label.
209
+ inputs={"user_id": "u123"}, # Optional. Metadata attached to receipt.
210
+ step_order=[...], # Required on every call — gate reads it from each payload.
211
+ raise_on_deny=True, # Optional. Default True.
212
+ )
213
+ # decision.decision → "ALLOW" | "DENY" | "HALT"
214
+ # decision.allowed → True if ALLOW
215
+ # decision.pack_id → SHA-256 receipt hash
216
+ # decision.receipt → full signed receipt dict
217
+ # decision.reasons → list of reason codes on DENY/HALT
218
+ ```
219
+
220
+ #### `client.sequence(sequence_id, step_order)`
221
+
222
+ Returns a `RailSequence` that tracks `step_order` state — call `.next(step)` for each step without managing `step_order` yourself.
223
+
224
+ ### `RailDenied`
225
+
226
+ ```python
227
+ try:
228
+ seq.next("execute_transfer")
229
+ except RailDenied as e:
230
+ print(e.decision) # "DENY" or "HALT"
231
+ print(e.reasons) # ["SEQUENCE_VIOLATION"]
232
+ print(e.pack_id) # receipt hash for audit
233
+ print(e.step) # "execute_transfer"
234
+ ```
235
+
236
+ ### `LangGraphRail`
237
+
238
+ ```python
239
+ rail = LangGraphRail(
240
+ client,
241
+ step_order=["step_a", "step_b", "step_c"],
242
+ fallback_sequence_id=None, # Used when thread_id not in config
243
+ )
244
+ wrapped_fn = rail.wrap("step_a", original_fn, action_type="CHECK_STATE")
245
+ ```
246
+
247
+ ### `CrewRailGuard`
248
+
249
+ ```python
250
+ guard = CrewRailGuard(
251
+ client,
252
+ step_order=["task_1", "task_2", "task_3"],
253
+ sequence_id=None, # Optional. Auto-generated per kickoff() call if None.
254
+ action_type="CHECK_STATE",
255
+ )
256
+ result = guard.kickoff(crew, inputs={...})
257
+ guard.gate("task_1") # Explicit gate for custom loops
258
+ guard.reset() # Reset state for a new run
259
+ ```
260
+
261
+ ---
262
+
263
+ ## Compliance reports
264
+
265
+ Every `pack_id` in a `RailDecision` is a verifiable receipt. Paste your sequence ID into the report generator or call it programmatically:
266
+
267
+ ```bash
268
+ curl -X POST https://api.agenticrail.nz/v1/report \
269
+ -H "Authorization: Bearer YOUR_KEY" \
270
+ -H "Content-Type: application/json" \
271
+ -d '{"sequence_id": "my-run-001", "format": "json"}'
272
+ ```
273
+
274
+ Returns: chain proof, HMAC verification, enforcement log, AI-written compliance narrative (EU AI Act Article 11).
275
+
276
+ Demo sequences (using `DEMO-AGENTICRAIL-PUBLIC-2026`) are verifiable at [report.agenticrail.nz](https://report.agenticrail.nz) — no auth needed.
277
+
278
+ ---
279
+
280
+ ## Action types
281
+
282
+ | Type | Use when |
283
+ |------|----------|
284
+ | `CHECK_STATE` | Reading or observing state (default) |
285
+ | `VALIDATE_INPUT` | Verifying inputs before acting |
286
+ | `RECORD_RESULT` | Writing or committing an outcome |
287
+ | `CLARIFY_NEXT_STEP` | Asking for clarification |
288
+ | `SELECT_NEXT_STEP` | Choosing a path |
289
+ | `WAIT_FOR_SIGNAL` | Pausing for an external trigger |
290
+ | `PAUSE_CYCLE` | Deliberate suspension |
291
+ | `REDUCE_STIMULUS` | Backing off |
292
+
293
+ ---
294
+
295
+ ## Links
296
+
297
+ - **API docs:** [agenticrail.nz/docs](https://agenticrail.nz/docs)
298
+ - **Interactive demo:** [agenticrail.nz/demo](https://agenticrail.nz/demo)
299
+ - **Compliance matrix (60+ frameworks):** [agenticrail.nz/compliance](https://agenticrail.nz/compliance)
300
+ - **Verify a sequence:** [report.agenticrail.nz](https://report.agenticrail.nz)
301
+
302
+ ---
303
+
304
+ *TUARA KURI LIMITED — trading as AgenticRail. Hokianga, New Zealand.*
@@ -0,0 +1,8 @@
1
+ agenticrail/__init__.py,sha256=LxWNeoEQj5e1bWAG52099Dj2G12q0uAmg3epsRkNvv0,253
2
+ agenticrail/client.py,sha256=ADtKEUuMBtJqRSP9qhtMDCYMHjKNWG4dKChChFV8E2s,5854
3
+ agenticrail/integrations/__init__.py,sha256=kDk9Dna5jzlt8-MY_djjEmb3cfOPvmQiqbAnFijHfrk,177
4
+ agenticrail/integrations/crewai.py,sha256=tOyFEUkpMkK26N_0jGGHwFQ2qCubuRI4evQxaUGC3WY,5114
5
+ agenticrail/integrations/langgraph.py,sha256=bakC2At2vo2B3NYdBEi7O8DcYBNxqdvZ50YE-fxuxSQ,3970
6
+ agenticrail-0.1.0.dist-info/METADATA,sha256=FHb-bYNOf7sNrTuYWSIgYHXx_J6ntPUlMg6NldQiQxU,10100
7
+ agenticrail-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
8
+ agenticrail-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any