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.
- agentforge/__init__.py +114 -0
- agentforge/_testing/__init__.py +19 -0
- agentforge/_testing/fake_llm.py +126 -0
- agentforge/_testing/fake_tool.py +122 -0
- agentforge/_tools/__init__.py +14 -0
- agentforge/_tools/calculator.py +102 -0
- agentforge/_tools/decorator.py +300 -0
- agentforge/_tools/file_read.py +112 -0
- agentforge/_tools/shell.py +134 -0
- agentforge/_tools/web_search.py +207 -0
- agentforge/agent.py +817 -0
- agentforge/auth.py +42 -0
- agentforge/cli/__init__.py +18 -0
- agentforge/cli/_build.py +323 -0
- agentforge/cli/_scaffold_state.py +250 -0
- agentforge/cli/_shared_scaffold.py +174 -0
- agentforge/cli/config_cmd.py +174 -0
- agentforge/cli/db_cmd.py +262 -0
- agentforge/cli/debug_cmd.py +168 -0
- agentforge/cli/docs_cmd.py +217 -0
- agentforge/cli/eval_cmd.py +181 -0
- agentforge/cli/health_cmd.py +139 -0
- agentforge/cli/list_modules.py +85 -0
- agentforge/cli/main.py +81 -0
- agentforge/cli/manifest_apply.py +368 -0
- agentforge/cli/module_cmd.py +247 -0
- agentforge/cli/new_cmd.py +171 -0
- agentforge/cli/run_cmd.py +234 -0
- agentforge/cli/upgrade_cmd.py +230 -0
- agentforge/config/__init__.py +45 -0
- agentforge/eval/__init__.py +18 -0
- agentforge/eval/consistency.py +107 -0
- agentforge/eval/coverage.py +100 -0
- agentforge/eval/format_compliance.py +107 -0
- agentforge/eval/regression.py +143 -0
- agentforge/findings.py +166 -0
- agentforge/guardrails/__init__.py +32 -0
- agentforge/guardrails/allowlist.py +49 -0
- agentforge/guardrails/capability_check.py +58 -0
- agentforge/guardrails/engine.py +289 -0
- agentforge/guardrails/pii_redact_basic.py +61 -0
- agentforge/guardrails/prompt_injection_basic.py +90 -0
- agentforge/memory/__init__.py +16 -0
- agentforge/memory/in_memory.py +130 -0
- agentforge/memory/in_memory_graph.py +262 -0
- agentforge/memory/in_memory_vector.py +167 -0
- agentforge/pipeline/__init__.py +26 -0
- agentforge/pipeline/engine.py +189 -0
- agentforge/pipeline/errors.py +19 -0
- agentforge/pipeline/tool.py +93 -0
- agentforge/py.typed +0 -0
- agentforge/recording.py +189 -0
- agentforge/renderers/__init__.py +28 -0
- agentforge/renderers/_defaults.py +32 -0
- agentforge/renderers/markdown.py +44 -0
- agentforge/renderers/patch_applier.py +46 -0
- agentforge/renderers/registry.py +108 -0
- agentforge/renderers/scorecard.py +59 -0
- agentforge/renderers/span_table.py +71 -0
- agentforge/replay.py +260 -0
- agentforge/resolver_register.py +41 -0
- agentforge/retrieval.py +410 -0
- agentforge/runtime.py +63 -0
- agentforge/strategies/__init__.py +27 -0
- agentforge/strategies/_base.py +280 -0
- agentforge/strategies/_plan.py +93 -0
- agentforge/strategies/multi_agent.py +541 -0
- agentforge/strategies/plan_execute.py +506 -0
- agentforge/strategies/react.py +237 -0
- agentforge/strategies/tot.py +472 -0
- agentforge/templates/_shared/.cursorrules +12 -0
- agentforge/templates/_shared/.github/copilot-instructions.md +13 -0
- agentforge/templates/_shared/.gitkeep +0 -0
- agentforge/templates/_shared/AGENTS.md.tmpl +123 -0
- agentforge/templates/_shared/CLAUDE.md +13 -0
- agentforge/templates/_shared/docs/runbooks/01-set-up-new-agent.md.tmpl +67 -0
- agentforge/templates/_shared/docs/runbooks/02-add-a-tool.md +67 -0
- agentforge/templates/_shared/docs/runbooks/03-add-a-pipeline-task.md +69 -0
- agentforge/templates/_shared/docs/runbooks/04-pick-reasoning-strategy.md +67 -0
- agentforge/templates/_shared/docs/runbooks/05-write-prompts.md +75 -0
- agentforge/templates/_shared/docs/runbooks/06-test-your-agent.md +75 -0
- agentforge/templates/_shared/docs/runbooks/07-debug-a-run.md +70 -0
- agentforge/templates/_shared/docs/runbooks/08-add-memory.md +75 -0
- agentforge/templates/_shared/docs/runbooks/09-add-mcp.md +78 -0
- agentforge/templates/_shared/docs/runbooks/10-add-evaluators.md +76 -0
- agentforge/templates/_shared/docs/runbooks/11-add-safety-guardrails.md +83 -0
- agentforge/templates/_shared/docs/runbooks/12-add-observability.md +77 -0
- agentforge/templates/_shared/docs/runbooks/13-configure-multi-provider.md +91 -0
- agentforge/templates/_shared/docs/runbooks/14-deploy-your-agent.md +70 -0
- agentforge/templates/_shared/docs/runbooks/15-upgrade-your-agent.md +67 -0
- agentforge/templates/_shared/docs/runbooks/16-configuration-reference.md +81 -0
- agentforge/templates/_shared/docs/runbooks/17-add-reranker.md +78 -0
- agentforge/templates/_shared/docs/runbooks/18-add-hybrid-search.md +78 -0
- agentforge/templates/_shared/docs/runbooks/19-add-graphrag.md +83 -0
- agentforge/templates/_shared/docs/runbooks/20-apply-schema-migrations.md +92 -0
- agentforge/templates/_shared/docs/runbooks/21-use-streaming-guardrails.md +82 -0
- agentforge/templates/_shared/docs/runbooks/README.md.tmpl +68 -0
- agentforge/templates/code-reviewer/.env.example +8 -0
- agentforge/templates/code-reviewer/.gitignore +7 -0
- agentforge/templates/code-reviewer/README.md +12 -0
- agentforge/templates/code-reviewer/agentforge.yaml +23 -0
- agentforge/templates/code-reviewer/copier.yml +34 -0
- agentforge/templates/code-reviewer/pyproject.toml +18 -0
- agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/code-reviewer/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
- agentforge/templates/docs-qa/.env.example +8 -0
- agentforge/templates/docs-qa/.gitignore +7 -0
- agentforge/templates/docs-qa/README.md +14 -0
- agentforge/templates/docs-qa/agentforge.yaml +19 -0
- agentforge/templates/docs-qa/copier.yml +31 -0
- agentforge/templates/docs-qa/pyproject.toml +18 -0
- agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/docs-qa/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
- agentforge/templates/minimal/.env.example +11 -0
- agentforge/templates/minimal/.gitignore +10 -0
- agentforge/templates/minimal/README.md +28 -0
- agentforge/templates/minimal/agentforge.yaml +10 -0
- agentforge/templates/minimal/copier.yml +52 -0
- agentforge/templates/minimal/pyproject.toml +18 -0
- agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/minimal/src/{{project_slug.replace('-', '_')}}/main.py +34 -0
- agentforge/templates/patch-bot/.env.example +8 -0
- agentforge/templates/patch-bot/.gitignore +7 -0
- agentforge/templates/patch-bot/README.md +13 -0
- agentforge/templates/patch-bot/agentforge.yaml +15 -0
- agentforge/templates/patch-bot/copier.yml +31 -0
- agentforge/templates/patch-bot/pyproject.toml +18 -0
- agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/patch-bot/src/{{project_slug.replace('-', '_')}}/main.py +32 -0
- agentforge/templates/research/.env.example +8 -0
- agentforge/templates/research/.gitignore +7 -0
- agentforge/templates/research/README.md +14 -0
- agentforge/templates/research/agentforge.yaml +17 -0
- agentforge/templates/research/copier.yml +31 -0
- agentforge/templates/research/pyproject.toml +18 -0
- agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/research/src/{{project_slug.replace('-', '_')}}/main.py +31 -0
- agentforge/templates/triage/.env.example +8 -0
- agentforge/templates/triage/.gitignore +7 -0
- agentforge/templates/triage/README.md +14 -0
- agentforge/templates/triage/agentforge.yaml +25 -0
- agentforge/templates/triage/copier.yml +31 -0
- agentforge/templates/triage/pyproject.toml +18 -0
- agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/__init__.py +5 -0
- agentforge/templates/triage/src/{{project_slug.replace('-', '_')}}/main.py +30 -0
- agentforge/testing/__init__.py +69 -0
- agentforge/testing/conformance.py +40 -0
- agentforge/testing/factory.py +89 -0
- agentforge/testing/fixtures.py +42 -0
- agentforge/testing/llm.py +235 -0
- agentforge/testing/recording.py +177 -0
- agentforge/tools/__init__.py +41 -0
- agentforge_py-0.2.1.dist-info/METADATA +158 -0
- agentforge_py-0.2.1.dist-info/RECORD +157 -0
- agentforge_py-0.2.1.dist-info/WHEEL +4 -0
- agentforge_py-0.2.1.dist-info/entry_points.txt +2 -0
- agentforge_py-0.2.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""`RegressionVsBaseline` — deterministic grader against a locked baseline.
|
|
2
|
+
|
|
3
|
+
The baseline file is a JSONL (one JSON object per line) where each
|
|
4
|
+
entry is keyed by `task`:
|
|
5
|
+
|
|
6
|
+
{"task": "Summarise PR #42", "expected": "PR #42 adds X..."}
|
|
7
|
+
{"task": "List failing tests", "expected": ["test_a", "test_b"]}
|
|
8
|
+
|
|
9
|
+
At construction the file is loaded into a `{task: expected_output}`
|
|
10
|
+
map. For each `evaluate` call the grader picks the baseline entry
|
|
11
|
+
matching `context["task"]` and compares `finding.output` against it.
|
|
12
|
+
|
|
13
|
+
Two modes pick the comparison:
|
|
14
|
+
- `mode="exact"` (default) — `output == expected`. Score 1.0 or 0.0.
|
|
15
|
+
- `mode="structural"` — output and expected are both dicts;
|
|
16
|
+
score = fraction of matching keys (case-sensitive on both keys
|
|
17
|
+
and values).
|
|
18
|
+
|
|
19
|
+
Result labels:
|
|
20
|
+
- `"improved"` — exact-match in exact mode; perfect structural match.
|
|
21
|
+
- `"regressed"` — mismatch in either mode.
|
|
22
|
+
- `"no_baseline"` — no entry in the baseline file matches the task.
|
|
23
|
+
Score is NaN; the evaluator does not claim regression in this case.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import math
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, ClassVar, Literal
|
|
32
|
+
|
|
33
|
+
from agentforge_core.contracts.evaluator import EvalResult, Evaluator
|
|
34
|
+
|
|
35
|
+
_Mode = Literal["exact", "structural"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RegressionVsBaseline(Evaluator):
|
|
39
|
+
"""Score the current run vs a locked baseline file."""
|
|
40
|
+
|
|
41
|
+
name: ClassVar[str] = "regression_vs_baseline"
|
|
42
|
+
cost_estimate_usd: ClassVar[float] = 0.0
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
baseline_path: str | Path,
|
|
48
|
+
mode: _Mode = "exact",
|
|
49
|
+
) -> None:
|
|
50
|
+
if mode not in ("exact", "structural"):
|
|
51
|
+
raise ValueError(f"mode must be 'exact' or 'structural'; got {mode!r}")
|
|
52
|
+
self._mode = mode
|
|
53
|
+
self._baselines: dict[str, Any] = self._load(Path(baseline_path))
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _load(path: Path) -> dict[str, Any]:
|
|
57
|
+
if not path.exists():
|
|
58
|
+
raise FileNotFoundError(f"baseline file not found: {path}")
|
|
59
|
+
out: dict[str, Any] = {}
|
|
60
|
+
with path.open(encoding="utf-8") as fh:
|
|
61
|
+
for lineno, raw in enumerate(fh, start=1):
|
|
62
|
+
line = raw.strip()
|
|
63
|
+
if not line:
|
|
64
|
+
continue
|
|
65
|
+
try:
|
|
66
|
+
entry = json.loads(line)
|
|
67
|
+
except json.JSONDecodeError as exc:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"baseline {path}:{lineno} is not valid JSON: {exc.msg}"
|
|
70
|
+
) from exc
|
|
71
|
+
if not isinstance(entry, dict) or "task" not in entry or "expected" not in entry:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"baseline {path}:{lineno} must have 'task' and 'expected' keys"
|
|
74
|
+
)
|
|
75
|
+
out[entry["task"]] = entry["expected"]
|
|
76
|
+
if not out:
|
|
77
|
+
raise ValueError(f"baseline file {path} is empty")
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
async def evaluate(self, finding: Any, context: dict[str, Any]) -> EvalResult:
|
|
81
|
+
task = context.get("task")
|
|
82
|
+
if not isinstance(task, str) or task not in self._baselines:
|
|
83
|
+
return EvalResult(
|
|
84
|
+
evaluator=self.name,
|
|
85
|
+
score=math.nan,
|
|
86
|
+
label="no_baseline",
|
|
87
|
+
reasoning=f"no baseline entry for task {task!r}",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
expected = self._baselines[task]
|
|
91
|
+
output = finding.output if hasattr(finding, "output") else finding
|
|
92
|
+
|
|
93
|
+
if self._mode == "exact":
|
|
94
|
+
matches = output == expected
|
|
95
|
+
return EvalResult(
|
|
96
|
+
evaluator=self.name,
|
|
97
|
+
score=1.0 if matches else 0.0,
|
|
98
|
+
label="improved" if matches else "regressed",
|
|
99
|
+
reasoning="exact match" if matches else "output differs from baseline",
|
|
100
|
+
raw={"expected": expected, "actual": output},
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Structural mode.
|
|
104
|
+
if not isinstance(expected, dict) or not isinstance(output, dict):
|
|
105
|
+
return EvalResult(
|
|
106
|
+
evaluator=self.name,
|
|
107
|
+
score=0.0,
|
|
108
|
+
label="regressed",
|
|
109
|
+
reasoning=(
|
|
110
|
+
f"structural mode requires dict output and dict baseline; "
|
|
111
|
+
f"got output={type(output).__name__}, expected={type(expected).__name__}"
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
return self._compare_structural(output, expected)
|
|
115
|
+
|
|
116
|
+
def _compare_structural(self, output: dict[str, Any], expected: dict[str, Any]) -> EvalResult:
|
|
117
|
+
all_keys = expected.keys() | output.keys()
|
|
118
|
+
matching = [
|
|
119
|
+
k for k in all_keys if k in expected and k in output and output[k] == expected[k]
|
|
120
|
+
]
|
|
121
|
+
missing = sorted(k for k in expected if k not in output)
|
|
122
|
+
extra = sorted(k for k in output if k not in expected)
|
|
123
|
+
mismatched = sorted(k for k in expected if k in output and output[k] != expected[k])
|
|
124
|
+
score = len(matching) / len(all_keys) if all_keys else 1.0
|
|
125
|
+
label = "improved" if score == 1.0 else "regressed"
|
|
126
|
+
return EvalResult(
|
|
127
|
+
evaluator=self.name,
|
|
128
|
+
score=score,
|
|
129
|
+
label=label,
|
|
130
|
+
reasoning=(
|
|
131
|
+
f"structural match {len(matching)}/{len(all_keys)} keys "
|
|
132
|
+
f"(missing={missing}, extra={extra}, mismatched={mismatched})"
|
|
133
|
+
),
|
|
134
|
+
raw={
|
|
135
|
+
"matched_keys": sorted(matching),
|
|
136
|
+
"missing_keys": missing,
|
|
137
|
+
"extra_keys": extra,
|
|
138
|
+
"mismatched_keys": mismatched,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
__all__ = ["RegressionVsBaseline"]
|
agentforge/findings.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Finding variants and helper value types (feat-008).
|
|
2
|
+
|
|
3
|
+
The `Finding` Protocol itself lives in `agentforge_core.contracts.finding`
|
|
4
|
+
(shipped under feat-001). This module ships the four built-in variants
|
|
5
|
+
plus the `Patch` and `Span` helpers two of them embed.
|
|
6
|
+
|
|
7
|
+
Variants are **frozen Pydantic v2 models** (per ADR-0014, deviating
|
|
8
|
+
from spec §4.2's `@dataclass` sketch). The external shape is the same;
|
|
9
|
+
the model framework gives us validation on construction, declarative
|
|
10
|
+
schema, and `model_dump` / `model_validate` round-trip — important
|
|
11
|
+
because findings cross persistence (`Claim.payload`), transport
|
|
12
|
+
(A2A / MCP / pipeline aggregation), and LLM-output (structured-output
|
|
13
|
+
emission) boundaries.
|
|
14
|
+
|
|
15
|
+
Each variant satisfies the `Finding` Protocol structurally — no
|
|
16
|
+
inheritance from the Protocol is required. The Protocol is
|
|
17
|
+
`runtime_checkable`, so `isinstance(x, Finding)` works.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any, Self
|
|
23
|
+
|
|
24
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Patch(BaseModel):
|
|
28
|
+
"""A structured diff embedded inside a `PatchFinding`.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
file: Path to the file the diff applies against, relative to
|
|
32
|
+
repo root (e.g. `"src/foo.py"`).
|
|
33
|
+
diff: Unified-diff text. Must be a complete, applyable hunk
|
|
34
|
+
(or set of hunks against a single file).
|
|
35
|
+
hunk_count: Number of `@@` headers in `diff`. Cached at
|
|
36
|
+
construction; renderers may use it for summary stats.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
40
|
+
|
|
41
|
+
file: str = Field(min_length=1)
|
|
42
|
+
diff: str = Field(min_length=1)
|
|
43
|
+
hunk_count: int = Field(default=1, ge=1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Span(BaseModel):
|
|
47
|
+
"""A single source-range citation inside a `MultiSpanFinding`.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
file: Path the span is in, relative to repo root.
|
|
51
|
+
start_line: 1-indexed first line of the span.
|
|
52
|
+
end_line: 1-indexed last line of the span (inclusive). Must
|
|
53
|
+
be `>= start_line`.
|
|
54
|
+
excerpt: The text on those lines (optional; useful for
|
|
55
|
+
renderers that can't read the source file themselves,
|
|
56
|
+
e.g. when findings are persisted and rendered later).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
60
|
+
|
|
61
|
+
file: str = Field(min_length=1)
|
|
62
|
+
start_line: int = Field(ge=1)
|
|
63
|
+
end_line: int = Field(ge=1)
|
|
64
|
+
excerpt: str = ""
|
|
65
|
+
|
|
66
|
+
@field_validator("end_line")
|
|
67
|
+
@classmethod
|
|
68
|
+
def _end_after_start(cls, end_line: int, info: Any) -> int:
|
|
69
|
+
start = info.data.get("start_line")
|
|
70
|
+
if start is not None and end_line < start:
|
|
71
|
+
raise ValueError(f"end_line ({end_line}) must be >= start_line ({start})")
|
|
72
|
+
return end_line
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _FindingBase(BaseModel):
|
|
76
|
+
"""Internal base — shared config + `to_dict` / `from_dict` plumbing.
|
|
77
|
+
|
|
78
|
+
Not part of the public surface. Each variant subclasses this to
|
|
79
|
+
inherit the round-trip helpers. Subclasses MUST declare the
|
|
80
|
+
Protocol-required attributes (`severity`, `category`, `message`)
|
|
81
|
+
themselves so `isinstance(x, Finding)` resolves correctly.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(frozen=True, strict=True)
|
|
85
|
+
|
|
86
|
+
def to_dict(self) -> dict[str, Any]:
|
|
87
|
+
"""JSON-compatible serialisation. Round-trips via `from_dict`."""
|
|
88
|
+
return self.model_dump(mode="json")
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
92
|
+
"""Reconstruct a typed variant from a `to_dict()` payload."""
|
|
93
|
+
return cls.model_validate(data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SimpleFinding(_FindingBase):
|
|
97
|
+
"""The default variant — a severity-tagged issue or observation.
|
|
98
|
+
|
|
99
|
+
Use for code-review-style output, lint hits, audit notes — anything
|
|
100
|
+
that maps to "one finding = one location + one recommendation".
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
severity: str = Field(min_length=1)
|
|
104
|
+
category: str = Field(min_length=1)
|
|
105
|
+
message: str = Field(min_length=1)
|
|
106
|
+
recommendation: str = ""
|
|
107
|
+
file: str = ""
|
|
108
|
+
line: int | None = None
|
|
109
|
+
rule_id: str = ""
|
|
110
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class PatchFinding(_FindingBase):
|
|
114
|
+
"""Finding that ships a structured patch the consumer can apply.
|
|
115
|
+
|
|
116
|
+
Use for refactor bots, codemod agents, and auto-fix suggestions.
|
|
117
|
+
`confidence` is a model-supplied estimate in `[0, 1]`; downstream
|
|
118
|
+
automation typically gates application on it.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
severity: str = Field(min_length=1)
|
|
122
|
+
category: str = Field(min_length=1)
|
|
123
|
+
message: str = Field(min_length=1)
|
|
124
|
+
patch: Patch
|
|
125
|
+
rationale: str = Field(min_length=1)
|
|
126
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class NarrativeFinding(_FindingBase):
|
|
130
|
+
"""Long-form prose answer with citations.
|
|
131
|
+
|
|
132
|
+
Use for docs-Q&A, research summaries, explanatory output. `body`
|
|
133
|
+
is markdown; `references` is a flat list of pointer strings (free
|
|
134
|
+
form — typical shapes are `"path:line"`, URLs, or section anchors).
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
severity: str = Field(min_length=1)
|
|
138
|
+
category: str = Field(min_length=1)
|
|
139
|
+
message: str = Field(min_length=1)
|
|
140
|
+
body: str = Field(min_length=1)
|
|
141
|
+
references: list[str] = Field(default_factory=list)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MultiSpanFinding(_FindingBase):
|
|
145
|
+
"""One logical issue manifested across multiple source locations.
|
|
146
|
+
|
|
147
|
+
Use for cross-file findings like "hard-coded secret present in N
|
|
148
|
+
files" or "this deprecated API is used in N places". `spans` lists
|
|
149
|
+
every site; a renderer typically produces one block per span.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
severity: str = Field(min_length=1)
|
|
153
|
+
category: str = Field(min_length=1)
|
|
154
|
+
message: str = Field(min_length=1)
|
|
155
|
+
spans: list[Span] = Field(min_length=1)
|
|
156
|
+
recommendation: str = ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = [
|
|
160
|
+
"MultiSpanFinding",
|
|
161
|
+
"NarrativeFinding",
|
|
162
|
+
"Patch",
|
|
163
|
+
"PatchFinding",
|
|
164
|
+
"SimpleFinding",
|
|
165
|
+
"Span",
|
|
166
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Built-in basic guardrail validators (feat-018).
|
|
2
|
+
|
|
3
|
+
Four validators ship in the runtime package as the default tier:
|
|
4
|
+
|
|
5
|
+
- `PromptInjectionBasic` (`prompt_injection_basic`) — regex-based
|
|
6
|
+
pattern matching for the common prompt-injection phrases.
|
|
7
|
+
- `PIIRedactBasic` (`pii_redact_basic`) — regex-based PII
|
|
8
|
+
detection + redaction (email / phone / SSN / credit-card /
|
|
9
|
+
IPv4). Output validator; sets `redacted_content`.
|
|
10
|
+
- `CapabilityCheck` (`capability_check`) — denies tools that
|
|
11
|
+
declare a `destructive` capability unless explicitly
|
|
12
|
+
allowlisted.
|
|
13
|
+
- `Allowlist` (`allowlist`) — bare-name allowlist for tool gates.
|
|
14
|
+
|
|
15
|
+
All four register themselves with the global Resolver under the
|
|
16
|
+
matching category (`guardrails.input` / `guardrails.output` /
|
|
17
|
+
`guardrails.tool_gates`) at import time.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from agentforge.guardrails.allowlist import Allowlist
|
|
23
|
+
from agentforge.guardrails.capability_check import CapabilityCheck
|
|
24
|
+
from agentforge.guardrails.pii_redact_basic import PIIRedactBasic
|
|
25
|
+
from agentforge.guardrails.prompt_injection_basic import PromptInjectionBasic
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Allowlist",
|
|
29
|
+
"CapabilityCheck",
|
|
30
|
+
"PIIRedactBasic",
|
|
31
|
+
"PromptInjectionBasic",
|
|
32
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""`allowlist` — bare-name allowlist for tool dispatch (feat-018).
|
|
2
|
+
|
|
3
|
+
If `allowed` is set, only the tools whose names appear in it can
|
|
4
|
+
run. Pairs naturally with `capability_check` to add a second
|
|
5
|
+
restrictive layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, ClassVar
|
|
11
|
+
|
|
12
|
+
from agentforge_core.contracts.guardrails import ToolCallGate
|
|
13
|
+
from agentforge_core.contracts.tool import Tool
|
|
14
|
+
from agentforge_core.resolver import register
|
|
15
|
+
from agentforge_core.values.guardrails import ValidationResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register("guardrails.tool_gates", "allowlist")
|
|
19
|
+
class Allowlist(ToolCallGate):
|
|
20
|
+
"""Only tools whose name appears in `allowed` can run."""
|
|
21
|
+
|
|
22
|
+
name: ClassVar[str] = "allowlist"
|
|
23
|
+
description: ClassVar[str] = (
|
|
24
|
+
"Permits only tools whose names appear in `allowed`. Everything else is blocked."
|
|
25
|
+
)
|
|
26
|
+
cost_estimate_ms: ClassVar[int] = 0
|
|
27
|
+
|
|
28
|
+
def __init__(self, *, allowed: list[str] | None = None) -> None:
|
|
29
|
+
self._allowed = set(allowed or ())
|
|
30
|
+
|
|
31
|
+
async def authorize(
|
|
32
|
+
self,
|
|
33
|
+
tool_name: str,
|
|
34
|
+
tool: Tool,
|
|
35
|
+
args: dict[str, Any],
|
|
36
|
+
context: dict[str, Any],
|
|
37
|
+
) -> ValidationResult:
|
|
38
|
+
del tool, args, context
|
|
39
|
+
if tool_name in self._allowed:
|
|
40
|
+
return ValidationResult.ok()
|
|
41
|
+
return ValidationResult(
|
|
42
|
+
passed=False,
|
|
43
|
+
score=0.0,
|
|
44
|
+
violations=("not_in_allowlist",),
|
|
45
|
+
metadata={"tool": tool_name, "allowed": sorted(self._allowed)},
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["Allowlist"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""`capability_check` — denies tools tagged `destructive` unless
|
|
2
|
+
explicitly allowlisted (feat-018).
|
|
3
|
+
|
|
4
|
+
Tools declare their capabilities via the `Tool.capabilities`
|
|
5
|
+
ClassVar (feat-004). Any tool that includes `"destructive"` in
|
|
6
|
+
its capability set is blocked by this gate by default; the
|
|
7
|
+
agent's config can opt-in specific tools via the
|
|
8
|
+
`destructive_allow` list.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, ClassVar
|
|
14
|
+
|
|
15
|
+
from agentforge_core.contracts.guardrails import ToolCallGate
|
|
16
|
+
from agentforge_core.contracts.tool import Tool
|
|
17
|
+
from agentforge_core.resolver import register
|
|
18
|
+
from agentforge_core.values.guardrails import ValidationResult
|
|
19
|
+
|
|
20
|
+
_DESTRUCTIVE = "destructive"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@register("guardrails.tool_gates", "capability_check")
|
|
24
|
+
class CapabilityCheck(ToolCallGate):
|
|
25
|
+
"""Block `destructive` tools unless explicitly allowlisted."""
|
|
26
|
+
|
|
27
|
+
name: ClassVar[str] = "capability_check"
|
|
28
|
+
description: ClassVar[str] = (
|
|
29
|
+
"Denies any tool whose capabilities include 'destructive' unless "
|
|
30
|
+
"the tool name appears in `destructive_allow`."
|
|
31
|
+
)
|
|
32
|
+
cost_estimate_ms: ClassVar[int] = 0
|
|
33
|
+
|
|
34
|
+
def __init__(self, *, destructive_allow: list[str] | None = None) -> None:
|
|
35
|
+
self._allow = set(destructive_allow or ())
|
|
36
|
+
|
|
37
|
+
async def authorize(
|
|
38
|
+
self,
|
|
39
|
+
tool_name: str,
|
|
40
|
+
tool: Tool,
|
|
41
|
+
args: dict[str, Any],
|
|
42
|
+
context: dict[str, Any],
|
|
43
|
+
) -> ValidationResult:
|
|
44
|
+
del args, context
|
|
45
|
+
caps = set(type(tool).capabilities)
|
|
46
|
+
if _DESTRUCTIVE not in caps:
|
|
47
|
+
return ValidationResult.ok()
|
|
48
|
+
if tool_name in self._allow:
|
|
49
|
+
return ValidationResult.ok()
|
|
50
|
+
return ValidationResult(
|
|
51
|
+
passed=False,
|
|
52
|
+
score=0.0,
|
|
53
|
+
violations=("destructive_not_allowlisted",),
|
|
54
|
+
metadata={"tool": tool_name, "capabilities": sorted(caps)},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["CapabilityCheck"]
|