agentforge-py 0.2.1__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.
Files changed (157) hide show
  1. agentforge/__init__.py +114 -0
  2. agentforge/_testing/__init__.py +19 -0
  3. agentforge/_testing/fake_llm.py +126 -0
  4. agentforge/_testing/fake_tool.py +122 -0
  5. agentforge/_tools/__init__.py +14 -0
  6. agentforge/_tools/calculator.py +102 -0
  7. agentforge/_tools/decorator.py +300 -0
  8. agentforge/_tools/file_read.py +112 -0
  9. agentforge/_tools/shell.py +134 -0
  10. agentforge/_tools/web_search.py +207 -0
  11. agentforge/agent.py +817 -0
  12. agentforge/auth.py +42 -0
  13. agentforge/cli/__init__.py +18 -0
  14. agentforge/cli/_build.py +323 -0
  15. agentforge/cli/_scaffold_state.py +250 -0
  16. agentforge/cli/_shared_scaffold.py +174 -0
  17. agentforge/cli/config_cmd.py +174 -0
  18. agentforge/cli/db_cmd.py +262 -0
  19. agentforge/cli/debug_cmd.py +168 -0
  20. agentforge/cli/docs_cmd.py +217 -0
  21. agentforge/cli/eval_cmd.py +181 -0
  22. agentforge/cli/health_cmd.py +139 -0
  23. agentforge/cli/list_modules.py +85 -0
  24. agentforge/cli/main.py +81 -0
  25. agentforge/cli/manifest_apply.py +368 -0
  26. agentforge/cli/module_cmd.py +247 -0
  27. agentforge/cli/new_cmd.py +171 -0
  28. agentforge/cli/run_cmd.py +234 -0
  29. agentforge/cli/upgrade_cmd.py +230 -0
  30. agentforge/config/__init__.py +45 -0
  31. agentforge/eval/__init__.py +18 -0
  32. agentforge/eval/consistency.py +107 -0
  33. agentforge/eval/coverage.py +100 -0
  34. agentforge/eval/format_compliance.py +107 -0
  35. agentforge/eval/regression.py +143 -0
  36. agentforge/findings.py +166 -0
  37. agentforge/guardrails/__init__.py +32 -0
  38. agentforge/guardrails/allowlist.py +49 -0
  39. agentforge/guardrails/capability_check.py +58 -0
  40. agentforge/guardrails/engine.py +289 -0
  41. agentforge/guardrails/pii_redact_basic.py +61 -0
  42. agentforge/guardrails/prompt_injection_basic.py +90 -0
  43. agentforge/memory/__init__.py +16 -0
  44. agentforge/memory/in_memory.py +130 -0
  45. agentforge/memory/in_memory_graph.py +262 -0
  46. agentforge/memory/in_memory_vector.py +167 -0
  47. agentforge/pipeline/__init__.py +26 -0
  48. agentforge/pipeline/engine.py +189 -0
  49. agentforge/pipeline/errors.py +19 -0
  50. agentforge/pipeline/tool.py +93 -0
  51. agentforge/py.typed +0 -0
  52. agentforge/recording.py +189 -0
  53. agentforge/renderers/__init__.py +28 -0
  54. agentforge/renderers/_defaults.py +32 -0
  55. agentforge/renderers/markdown.py +44 -0
  56. agentforge/renderers/patch_applier.py +46 -0
  57. agentforge/renderers/registry.py +108 -0
  58. agentforge/renderers/scorecard.py +59 -0
  59. agentforge/renderers/span_table.py +71 -0
  60. agentforge/replay.py +260 -0
  61. agentforge/resolver_register.py +41 -0
  62. agentforge/retrieval.py +410 -0
  63. agentforge/runtime.py +63 -0
  64. agentforge/strategies/__init__.py +27 -0
  65. agentforge/strategies/_base.py +280 -0
  66. agentforge/strategies/_plan.py +93 -0
  67. agentforge/strategies/multi_agent.py +541 -0
  68. agentforge/strategies/plan_execute.py +506 -0
  69. agentforge/strategies/react.py +237 -0
  70. agentforge/strategies/tot.py +472 -0
  71. agentforge/templates/_shared/.cursorrules +12 -0
  72. agentforge/templates/_shared/.github/copilot-instructions.md +13 -0
  73. agentforge/templates/_shared/.gitkeep +0 -0
  74. agentforge/templates/_shared/AGENTS.md.tmpl +123 -0
  75. agentforge/templates/_shared/CLAUDE.md +13 -0
  76. agentforge/templates/_shared/docs/runbooks/01-set-up-new-agent.md.tmpl +67 -0
  77. agentforge/templates/_shared/docs/runbooks/02-add-a-tool.md +67 -0
  78. agentforge/templates/_shared/docs/runbooks/03-add-a-pipeline-task.md +69 -0
  79. agentforge/templates/_shared/docs/runbooks/04-pick-reasoning-strategy.md +67 -0
  80. agentforge/templates/_shared/docs/runbooks/05-write-prompts.md +75 -0
  81. agentforge/templates/_shared/docs/runbooks/06-test-your-agent.md +75 -0
  82. agentforge/templates/_shared/docs/runbooks/07-debug-a-run.md +70 -0
  83. agentforge/templates/_shared/docs/runbooks/08-add-memory.md +75 -0
  84. agentforge/templates/_shared/docs/runbooks/09-add-mcp.md +78 -0
  85. agentforge/templates/_shared/docs/runbooks/10-add-evaluators.md +76 -0
  86. agentforge/templates/_shared/docs/runbooks/11-add-safety-guardrails.md +83 -0
  87. agentforge/templates/_shared/docs/runbooks/12-add-observability.md +77 -0
  88. agentforge/templates/_shared/docs/runbooks/13-configure-multi-provider.md +91 -0
  89. agentforge/templates/_shared/docs/runbooks/14-deploy-your-agent.md +70 -0
  90. agentforge/templates/_shared/docs/runbooks/15-upgrade-your-agent.md +67 -0
  91. agentforge/templates/_shared/docs/runbooks/16-configuration-reference.md +81 -0
  92. agentforge/templates/_shared/docs/runbooks/17-add-reranker.md +78 -0
  93. agentforge/templates/_shared/docs/runbooks/18-add-hybrid-search.md +78 -0
  94. agentforge/templates/_shared/docs/runbooks/19-add-graphrag.md +83 -0
  95. agentforge/templates/_shared/docs/runbooks/20-apply-schema-migrations.md +92 -0
  96. agentforge/templates/_shared/docs/runbooks/21-use-streaming-guardrails.md +82 -0
  97. agentforge/templates/_shared/docs/runbooks/README.md.tmpl +68 -0
  98. agentforge/templates/code-reviewer/.env.example +8 -0
  99. agentforge/templates/code-reviewer/.gitignore +7 -0
  100. agentforge/templates/code-reviewer/README.md +12 -0
  101. agentforge/templates/code-reviewer/agentforge.yaml +23 -0
  102. agentforge/templates/code-reviewer/copier.yml +34 -0
  103. agentforge/templates/code-reviewer/pyproject.toml +18 -0
  104. agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  105. agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
  106. agentforge/templates/docs-qa/.env.example +8 -0
  107. agentforge/templates/docs-qa/.gitignore +7 -0
  108. agentforge/templates/docs-qa/README.md +14 -0
  109. agentforge/templates/docs-qa/agentforge.yaml +19 -0
  110. agentforge/templates/docs-qa/copier.yml +31 -0
  111. agentforge/templates/docs-qa/pyproject.toml +18 -0
  112. agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  113. agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
  114. agentforge/templates/minimal/.env.example +11 -0
  115. agentforge/templates/minimal/.gitignore +10 -0
  116. agentforge/templates/minimal/README.md +28 -0
  117. agentforge/templates/minimal/agentforge.yaml +10 -0
  118. agentforge/templates/minimal/copier.yml +52 -0
  119. agentforge/templates/minimal/pyproject.toml +18 -0
  120. agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  121. agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/main.py +34 -0
  122. agentforge/templates/patch-bot/.env.example +8 -0
  123. agentforge/templates/patch-bot/.gitignore +7 -0
  124. agentforge/templates/patch-bot/README.md +13 -0
  125. agentforge/templates/patch-bot/agentforge.yaml +15 -0
  126. agentforge/templates/patch-bot/copier.yml +31 -0
  127. agentforge/templates/patch-bot/pyproject.toml +18 -0
  128. agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  129. agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
  130. agentforge/templates/research/.env.example +8 -0
  131. agentforge/templates/research/.gitignore +7 -0
  132. agentforge/templates/research/README.md +14 -0
  133. agentforge/templates/research/agentforge.yaml +17 -0
  134. agentforge/templates/research/copier.yml +31 -0
  135. agentforge/templates/research/pyproject.toml +18 -0
  136. agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  137. agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/main.py +31 -0
  138. agentforge/templates/triage/.env.example +8 -0
  139. agentforge/templates/triage/.gitignore +7 -0
  140. agentforge/templates/triage/README.md +14 -0
  141. agentforge/templates/triage/agentforge.yaml +25 -0
  142. agentforge/templates/triage/copier.yml +31 -0
  143. agentforge/templates/triage/pyproject.toml +18 -0
  144. agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
  145. agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/main.py +30 -0
  146. agentforge/testing/__init__.py +69 -0
  147. agentforge/testing/conformance.py +40 -0
  148. agentforge/testing/factory.py +89 -0
  149. agentforge/testing/fixtures.py +42 -0
  150. agentforge/testing/llm.py +235 -0
  151. agentforge/testing/recording.py +177 -0
  152. agentforge/tools/__init__.py +41 -0
  153. agentforge_py-0.2.1.dist-info/METADATA +158 -0
  154. agentforge_py-0.2.1.dist-info/RECORD +157 -0
  155. agentforge_py-0.2.1.dist-info/WHEEL +4 -0
  156. agentforge_py-0.2.1.dist-info/entry_points.txt +2 -0
  157. agentforge_py-0.2.1.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,289 @@
1
+ """Guardrail engine — runs validators + emits audit events (feat-018).
2
+
3
+ Glues `InputValidator` / `OutputValidator` / `ToolCallGate` lists to
4
+ the agent's lifecycle. `GuardrailEngine` is constructed once per
5
+ `Agent` from the config + the framework-wide policy, then:
6
+
7
+ - `await engine.check_input(content, ctx)` is invoked once at the
8
+ start of `agent.run(task)`.
9
+ - The engine wraps the LLM client (`engine.wrap_llm(llm)`) so every
10
+ `.call(...)` output flows through `OutputValidator`s.
11
+ - The engine wraps each tool (`engine.wrap_tool(tool)`) so every
12
+ `.run(...)` is gated through `ToolCallGate`s.
13
+
14
+ All decisions append to `engine.events` (a list of dicts conforming
15
+ to `RunResult.guardrail_events` shape) and emit a structured log
16
+ record on the `agentforge.audit` channel.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import hashlib
22
+ import logging
23
+ from collections.abc import Awaitable, Callable
24
+ from typing import Any
25
+
26
+ from agentforge_core.config.schema import GuardrailPolicy
27
+ from agentforge_core.contracts.guardrails import (
28
+ InputValidator,
29
+ OutputValidator,
30
+ ToolCallGate,
31
+ )
32
+ from agentforge_core.contracts.llm import LLMClient
33
+ from agentforge_core.contracts.tool import Tool
34
+ from agentforge_core.production.exceptions import GuardrailViolation, ModuleError
35
+ from agentforge_core.values.guardrails import ValidationResult
36
+ from agentforge_core.values.messages import LLMResponse, Message, ToolSpec
37
+
38
+ _audit_log = logging.getLogger("agentforge.audit")
39
+
40
+
41
+ class GuardrailEngine:
42
+ """Per-Agent engine that runs validators + records decisions."""
43
+
44
+ def __init__(
45
+ self,
46
+ *,
47
+ input_validators: list[InputValidator],
48
+ output_validators: list[OutputValidator],
49
+ tool_gates: list[ToolCallGate],
50
+ policy: GuardrailPolicy,
51
+ ) -> None:
52
+ self._inputs = list(input_validators)
53
+ self._outputs = list(output_validators)
54
+ self._gates = list(tool_gates)
55
+ self._policy = policy
56
+ self.events: list[dict[str, Any]] = []
57
+
58
+ # ------------------------------------------------------------------
59
+ # Input
60
+ # ------------------------------------------------------------------
61
+
62
+ async def check_input(self, content: str, context: dict[str, Any]) -> str:
63
+ """Run every input validator. Returns the (possibly redacted)
64
+ content for downstream use; raises `GuardrailViolation` on
65
+ block."""
66
+ return await self._run_stage(
67
+ stage="input",
68
+ validators=self._inputs,
69
+ content=content,
70
+ context=context,
71
+ action=self._policy.on_input_violation,
72
+ call=lambda v, c, ctx: v.validate(c, ctx),
73
+ )
74
+
75
+ async def check_output(self, content: str, context: dict[str, Any]) -> str:
76
+ """Run every output validator post-LLM call."""
77
+ return await self._run_stage(
78
+ stage="output",
79
+ validators=self._outputs,
80
+ content=content,
81
+ context=context,
82
+ action=self._policy.on_output_violation,
83
+ call=lambda v, c, ctx: v.validate(c, ctx),
84
+ )
85
+
86
+ # ------------------------------------------------------------------
87
+ # Wrappers
88
+ # ------------------------------------------------------------------
89
+
90
+ def wrap_llm(self, real: LLMClient, context_factory: Callable[[], dict[str, Any]]) -> LLMClient:
91
+ """Wrap a real `LLMClient` so every `.call()` result flows
92
+ through the configured `OutputValidator`s.
93
+
94
+ Validators see the model's text content; tool_calls are
95
+ passed through untouched (those go through `ToolCallGate`s
96
+ on dispatch).
97
+ """
98
+ return _GuardedLLMClient(real=real, engine=self, context_factory=context_factory)
99
+
100
+ def wrap_tool(self, real: Tool, context_factory: Callable[[], dict[str, Any]]) -> Tool:
101
+ """Wrap a real `Tool` so every `.run(...)` is gated."""
102
+ return _wrap_tool(real, self, context_factory)
103
+
104
+ # ------------------------------------------------------------------
105
+ # Internals
106
+ # ------------------------------------------------------------------
107
+
108
+ async def _run_stage(
109
+ self,
110
+ *,
111
+ stage: str,
112
+ validators: list[Any],
113
+ content: str,
114
+ context: dict[str, Any],
115
+ action: str,
116
+ call: Callable[[Any, str, dict[str, Any]], Awaitable[ValidationResult]],
117
+ ) -> str:
118
+ current = content
119
+ for v in validators:
120
+
121
+ async def _call(
122
+ validator: Any = v,
123
+ content_value: str = current,
124
+ ctx: dict[str, Any] = context,
125
+ ) -> ValidationResult:
126
+ return await call(validator, content_value, ctx)
127
+
128
+ result = await self._safe_invoke(v, _call)
129
+ self._emit(stage=stage, validator=v, result=result, content=current, action=action)
130
+ if result.passed:
131
+ continue
132
+ if action == "block":
133
+ msg = (
134
+ f"guardrail block: validator={v.name!r} stage={stage} "
135
+ f"violations={list(result.violations)!r}"
136
+ )
137
+ raise GuardrailViolation(msg)
138
+ if action == "redact" and result.redacted_content is not None:
139
+ current = result.redacted_content
140
+ # warn / allow: continue without modification
141
+ return current
142
+
143
+ async def authorize_tool(
144
+ self,
145
+ tool_name: str,
146
+ tool: Tool,
147
+ args: dict[str, Any],
148
+ context: dict[str, Any],
149
+ ) -> None:
150
+ action = self._policy.on_tool_violation
151
+ for gate in self._gates:
152
+
153
+ async def _call(g: ToolCallGate = gate) -> ValidationResult:
154
+ return await g.authorize(tool_name, tool, args, context)
155
+
156
+ result = await self._safe_invoke(gate, _call)
157
+ self._emit(
158
+ stage="tool",
159
+ validator=gate,
160
+ result=result,
161
+ content=f"{tool_name}:{sorted(args)}",
162
+ action=action,
163
+ )
164
+ if result.passed:
165
+ continue
166
+ if action == "block":
167
+ msg = (
168
+ f"guardrail block: tool gate={gate.name!r} tool={tool_name!r} "
169
+ f"violations={list(result.violations)!r}"
170
+ )
171
+ raise GuardrailViolation(msg)
172
+
173
+ async def _safe_invoke(
174
+ self,
175
+ validator: Any,
176
+ call: Callable[[], Awaitable[ValidationResult]],
177
+ ) -> ValidationResult:
178
+ try:
179
+ return await call()
180
+ except (RuntimeError, ModuleError, ValueError) as exc:
181
+ if self._policy.fail_open:
182
+ _audit_log.warning(
183
+ "validator %s raised; fail_open=True so the call proceeds: %s",
184
+ validator.name,
185
+ exc,
186
+ )
187
+ return ValidationResult.ok()
188
+ return ValidationResult(
189
+ passed=False,
190
+ score=0.0,
191
+ violations=("validator_error",),
192
+ metadata={"error": str(exc)},
193
+ )
194
+
195
+ def _emit(
196
+ self,
197
+ *,
198
+ stage: str,
199
+ validator: Any,
200
+ result: ValidationResult,
201
+ content: str,
202
+ action: str,
203
+ ) -> None:
204
+ event = {
205
+ "stage": stage,
206
+ "validator": validator.name,
207
+ "passed": result.passed,
208
+ "violations": list(result.violations),
209
+ "score": result.score,
210
+ "action": action,
211
+ "content_hash": _hash(content),
212
+ }
213
+ self.events.append(event)
214
+ log = _audit_log.info if result.passed else _audit_log.warning
215
+ log(
216
+ "guardrail %s: %s passed=%s violations=%s action=%s",
217
+ stage,
218
+ validator.name,
219
+ result.passed,
220
+ list(result.violations),
221
+ action,
222
+ )
223
+
224
+
225
+ def _hash(content: str) -> str:
226
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
227
+
228
+
229
+ class _GuardedLLMClient(LLMClient):
230
+ """LLM wrapper that runs output validators after each `.call(...)`."""
231
+
232
+ def __init__(
233
+ self,
234
+ *,
235
+ real: LLMClient,
236
+ engine: GuardrailEngine,
237
+ context_factory: Callable[[], dict[str, Any]],
238
+ ) -> None:
239
+ self._real = real
240
+ self._engine = engine
241
+ self._ctx = context_factory
242
+
243
+ async def call(
244
+ self,
245
+ system: str,
246
+ messages: list[Message],
247
+ tools: list[ToolSpec] | None = None,
248
+ ) -> LLMResponse:
249
+ response = await self._real.call(system, messages, tools)
250
+ if not response.content:
251
+ return response
252
+ validated = await self._engine.check_output(response.content, self._ctx())
253
+ if validated == response.content:
254
+ return response
255
+ return response.model_copy(update={"content": validated})
256
+
257
+ async def close(self) -> None:
258
+ await self._real.close()
259
+
260
+
261
+ def _wrap_tool(
262
+ real: Tool,
263
+ engine: GuardrailEngine,
264
+ context_factory: Callable[[], dict[str, Any]],
265
+ ) -> Tool:
266
+ """Build a per-instance Tool wrapper that consults `engine` before
267
+ forwarding to `real.run(...)`."""
268
+
269
+ real_name = type(real).name
270
+ real_description = type(real).description
271
+ real_input_schema = type(real).input_schema
272
+ real_caps = type(real).capabilities
273
+
274
+ async def _run(_self: Tool, **kwargs: Any) -> Any:
275
+ await engine.authorize_tool(real_name, real, kwargs, context_factory())
276
+ return await real.run(**kwargs)
277
+
278
+ cls_namespace: dict[str, Any] = {
279
+ "name": real_name,
280
+ "description": real_description,
281
+ "input_schema": real_input_schema,
282
+ "capabilities": real_caps,
283
+ "run": _run,
284
+ }
285
+ synthesized = type(f"Guarded_{type(real).__name__}", (Tool,), cls_namespace)
286
+ return synthesized() # type: ignore[no-any-return]
287
+
288
+
289
+ __all__ = ["GuardrailEngine"]
@@ -0,0 +1,61 @@
1
+ """`pii_redact_basic` — regex-based PII detection + redaction
2
+ (feat-018).
3
+
4
+ Output validator. Replaces detected PII with `<redacted:KIND>`
5
+ placeholders so downstream consumers can still parse around the
6
+ redactions.
7
+
8
+ Patterns are conservative — they catch obvious cases (RFC-822
9
+ email, US-shaped SSN / phone, common credit-card and IPv4
10
+ formats) without trying to be exhaustive. For richer coverage,
11
+ install `agentforge-guard-presidio`.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import Any, ClassVar
18
+
19
+ from agentforge_core.contracts.guardrails import OutputValidator
20
+ from agentforge_core.resolver import register
21
+ from agentforge_core.values.guardrails import ValidationResult
22
+
23
+ _PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
24
+ ("email", re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")),
25
+ ("phone_us", re.compile(r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b")),
26
+ ("ssn", re.compile(r"\b\d{3}-\d{2}-\d{4}\b")),
27
+ ("credit_card", re.compile(r"\b(?:\d[ -]*?){13,16}\b")),
28
+ ("ipv4", re.compile(r"\b\d{1,3}(?:\.\d{1,3}){3}\b")),
29
+ )
30
+
31
+
32
+ @register("guardrails.output", "pii_redact_basic")
33
+ class PIIRedactBasic(OutputValidator):
34
+ """Regex-based PII detector + redactor (basic tier)."""
35
+
36
+ name: ClassVar[str] = "pii_redact_basic"
37
+ description: ClassVar[str] = (
38
+ "Regex-based PII detector (email / phone / SSN / credit-card / IPv4) "
39
+ "that emits `<redacted:KIND>` placeholders in the output."
40
+ )
41
+ cost_estimate_ms: ClassVar[int] = 2
42
+
43
+ async def validate(self, content: str, context: dict[str, Any]) -> ValidationResult:
44
+ del context
45
+ violations: list[str] = []
46
+ redacted = content
47
+ for kind, pattern in _PATTERNS:
48
+ if pattern.search(redacted):
49
+ violations.append(kind)
50
+ redacted = pattern.sub(f"<redacted:{kind}>", redacted)
51
+ if not violations:
52
+ return ValidationResult.ok()
53
+ return ValidationResult(
54
+ passed=False,
55
+ score=0.0,
56
+ violations=tuple(violations),
57
+ redacted_content=redacted,
58
+ )
59
+
60
+
61
+ __all__ = ["PIIRedactBasic"]
@@ -0,0 +1,90 @@
1
+ """`prompt_injection_basic` — regex pattern matching for the most
2
+ common prompt-injection phrases (feat-018).
3
+
4
+ This is the *basic* tier — it catches the obvious cases ("ignore
5
+ previous instructions") and almost nothing else. Production
6
+ deployments install `agentforge-guard-llmguard` or
7
+ `agentforge-guard-llamaguard` for richer coverage.
8
+
9
+ The pattern set is intentionally conservative: false positives are
10
+ expensive (block user) so we only flag phrases with very high
11
+ prior probability of malicious intent.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import Any, ClassVar
18
+
19
+ from agentforge_core.contracts.guardrails import InputValidator
20
+ from agentforge_core.resolver import register
21
+ from agentforge_core.values.guardrails import ValidationResult
22
+
23
+ _PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
24
+ (
25
+ "ignore_previous",
26
+ re.compile(
27
+ r"\bignore\s+(?:all\s+)?(?:previous|prior)\s+(?:instructions?|prompts?|messages?)\b",
28
+ re.IGNORECASE,
29
+ ),
30
+ ),
31
+ (
32
+ "disregard_instructions",
33
+ re.compile(r"\bdisregard\s+(?:all\s+)?(?:your\s+)?instructions?\b", re.IGNORECASE),
34
+ ),
35
+ (
36
+ "system_prompt_leak",
37
+ re.compile(
38
+ r"\b(?:reveal|print|show|repeat|leak|dump)\s+(?:your|the)\s+(?:system\s+)?(?:prompt|instructions)\b",
39
+ re.IGNORECASE,
40
+ ),
41
+ ),
42
+ (
43
+ "act_as_jailbreak",
44
+ re.compile(
45
+ r"\bact\s+as\s+(?:dan|jailbroken|developer\s+mode|do\s+anything\s+now)\b", re.IGNORECASE
46
+ ),
47
+ ),
48
+ (
49
+ "new_persona",
50
+ re.compile(
51
+ r"\byou\s+are\s+now\s+(?:dan|jailbroken|in\s+developer\s+mode)\b", re.IGNORECASE
52
+ ),
53
+ ),
54
+ (
55
+ "bypass_safety",
56
+ re.compile(
57
+ r"\b(?:bypass|disable|turn\s+off)\s+(?:safety|guardrails?|filter|moderation)\b",
58
+ re.IGNORECASE,
59
+ ),
60
+ ),
61
+ )
62
+
63
+
64
+ @register("guardrails.input", "prompt_injection_basic")
65
+ class PromptInjectionBasic(InputValidator):
66
+ """Regex-based prompt-injection detector (basic tier)."""
67
+
68
+ name: ClassVar[str] = "prompt_injection_basic"
69
+ description: ClassVar[str] = (
70
+ "Regex-based detector for the most common prompt-injection phrases. "
71
+ "Conservative pattern set — false positives are expensive."
72
+ )
73
+ cost_estimate_ms: ClassVar[int] = 1
74
+
75
+ async def validate(self, content: str, context: dict[str, Any]) -> ValidationResult:
76
+ del context
77
+ violations: list[str] = []
78
+ for rule, pattern in _PATTERNS:
79
+ if pattern.search(content):
80
+ violations.append(rule)
81
+ if violations:
82
+ return ValidationResult(
83
+ passed=False,
84
+ score=0.0,
85
+ violations=tuple(violations),
86
+ )
87
+ return ValidationResult.ok()
88
+
89
+
90
+ __all__ = ["PromptInjectionBasic"]
@@ -0,0 +1,16 @@
1
+ """Default in-process memory implementations.
2
+
3
+ `InMemoryStore` (claim audit log) and `InMemoryVectorStore` (semantic
4
+ search) ship with `agentforge` so a fresh `Agent(...)` has
5
+ durable-shaped state out of the box without external infra. Both are
6
+ safe defaults; production deployments swap to real drivers
7
+ (`agentforge-memory-sqlite`, `-postgres`) via `agentforge.yaml`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from agentforge.memory.in_memory import InMemoryStore
13
+ from agentforge.memory.in_memory_graph import InMemoryGraphStore
14
+ from agentforge.memory.in_memory_vector import InMemoryVectorStore
15
+
16
+ __all__ = ["InMemoryGraphStore", "InMemoryStore", "InMemoryVectorStore"]
@@ -0,0 +1,130 @@
1
+ """`InMemoryStore` — process-local `MemoryStore` implementation.
2
+
3
+ Used as the default when no persistence module is configured. Loses
4
+ data on process exit (by design — for tests, demos, ephemeral runs).
5
+ For durable storage swap to a feat-005 driver via `agentforge.yaml`.
6
+
7
+ Implementation notes:
8
+
9
+ - Storage is an `OrderedDict[str, Claim]` keyed by claim id, so
10
+ insertion order is preserved (sortable ULID ids already provide
11
+ monotonic ordering, but the dict guarantees it after deletes /
12
+ supersedes).
13
+ - All operations are ``async def`` to match the `MemoryStore`
14
+ contract; they don't actually do I/O so they complete on the
15
+ current event-loop tick.
16
+ - Thread safety is NOT a goal — this is for in-process single-loop
17
+ use. Multi-process / multi-worker deployments use feat-005 drivers.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections import OrderedDict
23
+ from collections.abc import AsyncIterator
24
+ from datetime import datetime
25
+
26
+ from agentforge_core.contracts.memory import MemoryStore
27
+ from agentforge_core.production.exceptions import ModuleError
28
+ from agentforge_core.values.claim import Claim
29
+
30
+
31
+ class InMemoryStore(MemoryStore):
32
+ """Process-local `MemoryStore` backed by an in-memory ``OrderedDict``."""
33
+
34
+ def __init__(self) -> None:
35
+ self._items: OrderedDict[str, Claim] = OrderedDict()
36
+
37
+ async def put(self, claim: Claim) -> str:
38
+ self._items[claim.id] = claim
39
+ return claim.id
40
+
41
+ async def get(self, claim_id: str) -> Claim | None:
42
+ return self._items.get(claim_id)
43
+
44
+ async def query(
45
+ self,
46
+ *,
47
+ project: str | None = None,
48
+ agent: str | None = None,
49
+ category: str | None = None,
50
+ run_id: str | None = None,
51
+ limit: int = 100,
52
+ ) -> list[Claim]:
53
+ results: list[Claim] = []
54
+ for claim in self._items.values():
55
+ if project is not None and claim.project != project:
56
+ continue
57
+ if agent is not None and claim.agent != agent:
58
+ continue
59
+ if category is not None and claim.category != category:
60
+ continue
61
+ if run_id is not None and claim.run_id != run_id:
62
+ continue
63
+ results.append(claim)
64
+ if len(results) >= limit:
65
+ break
66
+ return results
67
+
68
+ async def supersede(self, old_id: str, new_claim: Claim) -> str:
69
+ if old_id not in self._items:
70
+ raise ModuleError(f"Cannot supersede unknown claim id: {old_id!r}")
71
+ if new_claim.supersedes is None:
72
+ new_claim = new_claim.model_copy(update={"supersedes": old_id})
73
+ elif new_claim.supersedes != old_id:
74
+ raise ModuleError(
75
+ f"new_claim.supersedes={new_claim.supersedes!r} does not match old_id={old_id!r}"
76
+ )
77
+ self._items[new_claim.id] = new_claim
78
+ return new_claim.id
79
+
80
+ def stream(
81
+ self,
82
+ *,
83
+ project: str | None = None,
84
+ agent: str | None = None,
85
+ category: str | None = None,
86
+ run_id: str | None = None,
87
+ ) -> AsyncIterator[Claim]:
88
+ async def _agen() -> AsyncIterator[Claim]:
89
+ for claim in list(self._items.values()):
90
+ if project is not None and claim.project != project:
91
+ continue
92
+ if agent is not None and claim.agent != agent:
93
+ continue
94
+ if category is not None and claim.category != category:
95
+ continue
96
+ if run_id is not None and claim.run_id != run_id:
97
+ continue
98
+ yield claim
99
+
100
+ return _agen()
101
+
102
+ async def delete(
103
+ self,
104
+ *,
105
+ run_id: str | None = None,
106
+ older_than: datetime | None = None,
107
+ category: str | None = None,
108
+ ) -> int:
109
+ if run_id is None and older_than is None and category is None:
110
+ raise ModuleError(
111
+ "delete() requires at least one filter; refusing to wipe every claim."
112
+ )
113
+ keep: OrderedDict[str, Claim] = OrderedDict()
114
+ removed = 0
115
+ for cid, claim in self._items.items():
116
+ if run_id is not None and claim.run_id != run_id:
117
+ keep[cid] = claim
118
+ continue
119
+ if category is not None and claim.category != category:
120
+ keep[cid] = claim
121
+ continue
122
+ if older_than is not None and claim.created_at >= older_than:
123
+ keep[cid] = claim
124
+ continue
125
+ removed += 1
126
+ self._items = keep
127
+ return removed
128
+
129
+ async def close(self) -> None:
130
+ self._items.clear()