traceforge-toolkit 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.
- traceforge/__init__.py +72 -0
- traceforge/__main__.py +5 -0
- traceforge/_generated.py +254 -0
- traceforge/adapters/__init__.py +12 -0
- traceforge/adapters/base.py +82 -0
- traceforge/adapters/genai_otel.py +164 -0
- traceforge/adapters/mapped_json.py +297 -0
- traceforge/adapters/otel.py +220 -0
- traceforge/boundary/__init__.py +46 -0
- traceforge/boundary/data/boundary-model.joblib +0 -0
- traceforge/boundary/decode.py +87 -0
- traceforge/boundary/features.py +137 -0
- traceforge/boundary/inference.py +267 -0
- traceforge/boundary/inferencer.py +146 -0
- traceforge/classify/__init__.py +95 -0
- traceforge/classify/cmd.py +109 -0
- traceforge/classify/coding.py +213 -0
- traceforge/classify/config.py +554 -0
- traceforge/classify/core.py +266 -0
- traceforge/classify/data/binary_info.yaml +358 -0
- traceforge/classify/data/canonical_tools.yaml +251 -0
- traceforge/classify/data/effect_overrides.yaml +480 -0
- traceforge/classify/data/mcp_profiles.yaml +711 -0
- traceforge/classify/data/recommendation_rules.yaml +111 -0
- traceforge/classify/data/risk.yaml +534 -0
- traceforge/classify/data/shell_defaults.yaml +95 -0
- traceforge/classify/data/shell_rules.yaml +1192 -0
- traceforge/classify/data/tool_classifications.yaml +215 -0
- traceforge/classify/data/verb_inference.yaml +218 -0
- traceforge/classify/mcp.py +181 -0
- traceforge/classify/phases.py +75 -0
- traceforge/classify/powershell.py +93 -0
- traceforge/classify/registry.py +138 -0
- traceforge/classify/risk.py +553 -0
- traceforge/classify/rules.py +215 -0
- traceforge/classify/schema.yaml +282 -0
- traceforge/classify/shell.py +503 -0
- traceforge/classify/tools.py +91 -0
- traceforge/classify/workflow.py +32 -0
- traceforge/cli/__init__.py +42 -0
- traceforge/cli/config_cmd.py +130 -0
- traceforge/cli/detect.py +46 -0
- traceforge/cli/download_cmd.py +74 -0
- traceforge/cli/factory.py +60 -0
- traceforge/cli/gate_cmd.py +30 -0
- traceforge/cli/init_cmd.py +86 -0
- traceforge/cli/replay.py +130 -0
- traceforge/cli/runner.py +181 -0
- traceforge/cli/score.py +217 -0
- traceforge/cli/status.py +65 -0
- traceforge/cli/watch.py +297 -0
- traceforge/config/__init__.py +80 -0
- traceforge/config/defaults.py +149 -0
- traceforge/config/loader.py +239 -0
- traceforge/config/mappings.py +104 -0
- traceforge/config/models.py +579 -0
- traceforge/enricher.py +576 -0
- traceforge/formatting/__init__.py +12 -0
- traceforge/formatting/budget.py +92 -0
- traceforge/formatting/density.py +154 -0
- traceforge/gate/__init__.py +17 -0
- traceforge/gate/client.py +145 -0
- traceforge/gate/external.py +305 -0
- traceforge/gate/registry.py +108 -0
- traceforge/gate/server.py +174 -0
- traceforge/gates/__init__.py +8 -0
- traceforge/gates/pii.py +352 -0
- traceforge/gates/pii_patterns.yaml +228 -0
- traceforge/governance/__init__.py +121 -0
- traceforge/governance/assessor.py +441 -0
- traceforge/governance/budget.py +59 -0
- traceforge/governance/canonical.py +56 -0
- traceforge/governance/codec.py +414 -0
- traceforge/governance/context.py +249 -0
- traceforge/governance/drift.py +196 -0
- traceforge/governance/emitter.py +234 -0
- traceforge/governance/envelope.py +138 -0
- traceforge/governance/ifc.py +364 -0
- traceforge/governance/integrity.py +162 -0
- traceforge/governance/labeler.py +250 -0
- traceforge/governance/mcp_drift.py +328 -0
- traceforge/governance/monitor.py +539 -0
- traceforge/governance/observer.py +276 -0
- traceforge/governance/persistence.py +401 -0
- traceforge/governance/phase1.py +77 -0
- traceforge/governance/pii.py +276 -0
- traceforge/governance/pipeline.py +840 -0
- traceforge/governance/registry.py +110 -0
- traceforge/governance/results.py +183 -0
- traceforge/governance/risk_wrapper.py +113 -0
- traceforge/governance/rules.py +368 -0
- traceforge/governance/scorer.py +262 -0
- traceforge/governance/shield.py +318 -0
- traceforge/governance/state.py +466 -0
- traceforge/governance/types.py +176 -0
- traceforge/mappings/__init__.py +1 -0
- traceforge/mappings/aider.yaml +216 -0
- traceforge/mappings/aider_markdown.yaml +113 -0
- traceforge/mappings/amazonq.yaml +56 -0
- traceforge/mappings/antigravity.yaml +88 -0
- traceforge/mappings/claude.yaml +93 -0
- traceforge/mappings/cline.yaml +286 -0
- traceforge/mappings/codex.yaml +158 -0
- traceforge/mappings/continue_dev.yaml +57 -0
- traceforge/mappings/copilot.yaml +266 -0
- traceforge/mappings/copilot_markdown.yaml +72 -0
- traceforge/mappings/copilot_vscode.yaml +181 -0
- traceforge/mappings/crewai.yaml +592 -0
- traceforge/mappings/goose.yaml +128 -0
- traceforge/mappings/langgraph.yaml +165 -0
- traceforge/mappings/maf.yaml +90 -0
- traceforge/mappings/maf_transcript.yaml +99 -0
- traceforge/mappings/openai_agents.yaml +144 -0
- traceforge/mappings/opencode.yaml +473 -0
- traceforge/mappings/openhands.yaml +320 -0
- traceforge/mappings/pydantic_ai.yaml +183 -0
- traceforge/mappings/smolagents.yaml +89 -0
- traceforge/mappings/sweagent.yaml +82 -0
- traceforge/migrations/__init__.py +1 -0
- traceforge/migrations/env.py +60 -0
- traceforge/migrations/models.py +145 -0
- traceforge/migrations/runner.py +44 -0
- traceforge/migrations/script.py.mako +25 -0
- traceforge/migrations/versions/0001_initial.py +176 -0
- traceforge/migrations/versions/__init__.py +4 -0
- traceforge/models.py +29 -0
- traceforge/parsers/__init__.py +21 -0
- traceforge/parsers/aider.py +416 -0
- traceforge/parsers/base.py +226 -0
- traceforge/parsers/copilot.py +314 -0
- traceforge/phase/__init__.py +50 -0
- traceforge/phase/data/phase-model.joblib +0 -0
- traceforge/phase/data/potion-base-8M/README.md +99 -0
- traceforge/phase/data/potion-base-8M/config.json +13 -0
- traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
- traceforge/phase/data/potion-base-8M/modules.json +14 -0
- traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
- traceforge/phase/event_rows.py +107 -0
- traceforge/phase/features.py +468 -0
- traceforge/phase/inference.py +279 -0
- traceforge/phase/inferencer.py +171 -0
- traceforge/phase/segmentation.py +258 -0
- traceforge/pipeline.py +891 -0
- traceforge/preprocessors/__init__.py +34 -0
- traceforge/preprocessors/amazonq.py +224 -0
- traceforge/preprocessors/antigravity.py +116 -0
- traceforge/preprocessors/claude.py +95 -0
- traceforge/preprocessors/cline.py +36 -0
- traceforge/preprocessors/codex.py +311 -0
- traceforge/preprocessors/continue_dev.py +119 -0
- traceforge/preprocessors/copilot_vscode.py +171 -0
- traceforge/preprocessors/goose.py +156 -0
- traceforge/preprocessors/maf_transcript.py +84 -0
- traceforge/preprocessors/openai_agents.py +86 -0
- traceforge/preprocessors/opencode.py +85 -0
- traceforge/preprocessors/openhands.py +36 -0
- traceforge/preprocessors/pydantic_ai.py +62 -0
- traceforge/preprocessors/registry.py +24 -0
- traceforge/preprocessors/smolagents.py +90 -0
- traceforge/py.typed +0 -0
- traceforge/sdk/__init__.py +59 -0
- traceforge/sdk/gate_policy.py +63 -0
- traceforge/sdk/gate_types.py +140 -0
- traceforge/sdk/pipeline.py +265 -0
- traceforge/sdk/verdict.py +81 -0
- traceforge/sinks/__init__.py +23 -0
- traceforge/sinks/base.py +95 -0
- traceforge/sinks/callback.py +132 -0
- traceforge/sinks/console.py +112 -0
- traceforge/sinks/factory.py +94 -0
- traceforge/sinks/jsonl.py +125 -0
- traceforge/sinks/otel_exporter.py +212 -0
- traceforge/sinks/parquet.py +260 -0
- traceforge/sinks/s3.py +206 -0
- traceforge/sinks/sqlite_output.py +234 -0
- traceforge/sinks/webhook.py +136 -0
- traceforge/sources/__init__.py +18 -0
- traceforge/sources/auto_detect.py +173 -0
- traceforge/sources/base.py +45 -0
- traceforge/sources/file_poll.py +136 -0
- traceforge/sources/file_watch.py +221 -0
- traceforge/sources/http_poll.py +141 -0
- traceforge/sources/replay.py +63 -0
- traceforge/sources/sqlite.py +187 -0
- traceforge/sources/sse.py +198 -0
- traceforge/telemetry/__init__.py +192 -0
- traceforge/title/__init__.py +23 -0
- traceforge/title/_resolve.py +79 -0
- traceforge/title/context.py +295 -0
- traceforge/title/data/boilerplate_files.json +14 -0
- traceforge/title/heuristics.py +429 -0
- traceforge/title/hygiene.py +90 -0
- traceforge/title/inference.py +314 -0
- traceforge/title/inferencer.py +477 -0
- traceforge/title/naming.py +398 -0
- traceforge/trace.py +291 -0
- traceforge/tracking/__init__.py +30 -0
- traceforge/tracking/models.py +115 -0
- traceforge/tracking/phase_tracker.py +288 -0
- traceforge/types.py +315 -0
- traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
- traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
- traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
- traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Traceforge SDK — pipeline setup and gating.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from traceforge.sdk import Pipeline, GatePolicy, Verdict, ToolCallRequest
|
|
5
|
+
|
|
6
|
+
def my_preflight(request: ToolCallRequest, ctx: GateContext) -> Verdict:
|
|
7
|
+
if request.risk_score > 60:
|
|
8
|
+
return Verdict.deny(f"score {request.risk_score} exceeds threshold")
|
|
9
|
+
return Verdict.allow()
|
|
10
|
+
|
|
11
|
+
policy = GatePolicy().preflight(my_preflight)
|
|
12
|
+
|
|
13
|
+
pipeline = Pipeline.create(policy=policy)
|
|
14
|
+
pipeline.gate_crewai() # CrewAI hooks
|
|
15
|
+
pipeline.gate_langchain(tool) # LangChain tool wrap
|
|
16
|
+
pipeline.gate_maf() # MAF middleware
|
|
17
|
+
|
|
18
|
+
The preflight callback returns a Verdict. Traceforge enforces it using each framework's
|
|
19
|
+
native blocking mechanism. The postflight callback receives the tool output for audit.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from traceforge.sdk.gate_policy import GatePolicy # noqa: E402
|
|
25
|
+
from traceforge.sdk.gate_types import ( # noqa: E402
|
|
26
|
+
GateContext,
|
|
27
|
+
PostflightAction,
|
|
28
|
+
PostflightVerdict,
|
|
29
|
+
ToolCallRequest,
|
|
30
|
+
ToolCallResult,
|
|
31
|
+
)
|
|
32
|
+
from traceforge.sdk.verdict import ( # noqa: E402
|
|
33
|
+
Decision,
|
|
34
|
+
PostflightGate,
|
|
35
|
+
PreflightGate,
|
|
36
|
+
Verdict,
|
|
37
|
+
)
|
|
38
|
+
from traceforge.trace import EventTrace, TraceStage # noqa: E402
|
|
39
|
+
|
|
40
|
+
# Imported last so every SDK submodule symbol above is already defined before the
|
|
41
|
+
# facade (which pulls in the governance engine + core EventPipeline) resolves,
|
|
42
|
+
# keeping this package free of import-cycle ordering hazards.
|
|
43
|
+
from traceforge.sdk.pipeline import Pipeline # noqa: E402
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"Pipeline",
|
|
47
|
+
"EventTrace",
|
|
48
|
+
"TraceStage",
|
|
49
|
+
"Verdict",
|
|
50
|
+
"Decision",
|
|
51
|
+
"PreflightGate",
|
|
52
|
+
"PostflightGate",
|
|
53
|
+
"GatePolicy",
|
|
54
|
+
"GateContext",
|
|
55
|
+
"ToolCallRequest",
|
|
56
|
+
"ToolCallResult",
|
|
57
|
+
"PostflightVerdict",
|
|
58
|
+
"PostflightAction",
|
|
59
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""GatePolicy — composable, testable gate registration.
|
|
2
|
+
|
|
3
|
+
Gates are registered via a GatePolicy which owns the ordered chain
|
|
4
|
+
of preflight/postflight gates. The pipeline uses this instead of
|
|
5
|
+
per-attach kwargs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from traceforge.sdk.verdict import PostflightGate, PreflightGate
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class GatePolicy:
|
|
19
|
+
"""Ordered collection of gates. Testable without a pipeline.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
policy = (
|
|
23
|
+
GatePolicy()
|
|
24
|
+
.preflight(block_destructive_shell)
|
|
25
|
+
.preflight(rate_limit_gate)
|
|
26
|
+
.postflight(redact_secrets)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
pipeline = Pipeline.create(policy=policy)
|
|
30
|
+
|
|
31
|
+
Gates run in registration order (top-down).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
_preflight: list["PreflightGate"] = field(default_factory=list)
|
|
35
|
+
_postflight: list["PostflightGate"] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
def preflight(self, gate: "PreflightGate") -> "GatePolicy":
|
|
38
|
+
"""Register a preflight gate. Runs in registration order."""
|
|
39
|
+
self._preflight.append(gate)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
def postflight(self, gate: "PostflightGate") -> "GatePolicy":
|
|
43
|
+
"""Register a postflight gate. Runs in registration order."""
|
|
44
|
+
self._postflight.append(gate)
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def preflight_gates(self) -> tuple["PreflightGate", ...]:
|
|
49
|
+
"""Ordered preflight gates (registration order)."""
|
|
50
|
+
return tuple(self._preflight)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def postflight_gates(self) -> tuple["PostflightGate", ...]:
|
|
54
|
+
"""Ordered postflight gates (registration order)."""
|
|
55
|
+
return tuple(self._postflight)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def has_preflight(self) -> bool:
|
|
59
|
+
return len(self._preflight) > 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def has_postflight(self) -> bool:
|
|
63
|
+
return len(self._postflight) > 0
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Gate-facing types: view objects, context, and postflight verdict.
|
|
2
|
+
|
|
3
|
+
Gate authors import from here. These types provide the policy-relevant surface
|
|
4
|
+
with guaranteed non-optional fields — no None checks needed in gate logic.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from traceforge._generated import (
|
|
15
|
+
Action,
|
|
16
|
+
Capability,
|
|
17
|
+
Effect,
|
|
18
|
+
Mechanism,
|
|
19
|
+
Recommendation,
|
|
20
|
+
RiskBand,
|
|
21
|
+
Role,
|
|
22
|
+
Scope,
|
|
23
|
+
)
|
|
24
|
+
from traceforge.sdk.verdict import Verdict
|
|
25
|
+
from traceforge.trace import EMPTY_MAP
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from traceforge.trace import EventTrace
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ─── Gate View Objects ────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class ToolCallRequest:
|
|
36
|
+
"""What preflight gate authors receive. Policy-focused, no None checks.
|
|
37
|
+
|
|
38
|
+
All classification/assessment fields are guaranteed non-None — the pipeline
|
|
39
|
+
only constructs this for fully-assessed EventTraces.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# What tool is being called
|
|
43
|
+
tool: str
|
|
44
|
+
input: MappingProxyType
|
|
45
|
+
target: str | None
|
|
46
|
+
|
|
47
|
+
# How it's classified
|
|
48
|
+
mechanism: Mechanism
|
|
49
|
+
effect: Effect
|
|
50
|
+
capabilities: tuple[Capability, ...]
|
|
51
|
+
scope: tuple[Scope, ...]
|
|
52
|
+
role: tuple[Role, ...]
|
|
53
|
+
action: tuple[Action, ...]
|
|
54
|
+
|
|
55
|
+
# How dangerous
|
|
56
|
+
risk_score: int
|
|
57
|
+
risk_band: RiskBand
|
|
58
|
+
suggested_action: Recommendation
|
|
59
|
+
reason: str
|
|
60
|
+
|
|
61
|
+
# Identity
|
|
62
|
+
session_id: str
|
|
63
|
+
tool_call_id: str
|
|
64
|
+
|
|
65
|
+
# Escape hatch — full EventTrace for raw_event, attributes, etc.
|
|
66
|
+
event_trace: EventTrace
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class ToolCallResult:
|
|
71
|
+
"""What postflight gate authors receive. Includes tool output."""
|
|
72
|
+
|
|
73
|
+
# What tool ran
|
|
74
|
+
tool: str
|
|
75
|
+
input: MappingProxyType
|
|
76
|
+
target: str | None
|
|
77
|
+
|
|
78
|
+
# What it produced
|
|
79
|
+
output: MappingProxyType
|
|
80
|
+
duration_ms: int | None
|
|
81
|
+
error: str | None
|
|
82
|
+
|
|
83
|
+
# Classification + assessment
|
|
84
|
+
mechanism: Mechanism
|
|
85
|
+
effect: Effect
|
|
86
|
+
capabilities: tuple[Capability, ...]
|
|
87
|
+
risk_score: int
|
|
88
|
+
risk_band: RiskBand
|
|
89
|
+
suggested_action: Recommendation
|
|
90
|
+
reason: str
|
|
91
|
+
|
|
92
|
+
# Identity
|
|
93
|
+
session_id: str
|
|
94
|
+
tool_call_id: str
|
|
95
|
+
|
|
96
|
+
# Escape hatch
|
|
97
|
+
event_trace: EventTrace
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ─── GateContext ──────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class GateContext:
|
|
105
|
+
"""Session-scoped state passed as second arg to every gate.
|
|
106
|
+
|
|
107
|
+
Pipeline builds and maintains this per session. Gate authors can
|
|
108
|
+
ignore it for stateless policies.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
session_id: str
|
|
112
|
+
tool_call_count: int
|
|
113
|
+
denied_count: int
|
|
114
|
+
prior_verdicts: tuple[Verdict, ...] = ()
|
|
115
|
+
prior_tool_call_ids: tuple[str, ...] = ()
|
|
116
|
+
agent_id: str | None = None
|
|
117
|
+
user_id: str | None = None
|
|
118
|
+
policy: MappingProxyType = field(default_factory=lambda: EMPTY_MAP)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ─── Postflight Verdict ───────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PostflightAction(StrEnum):
|
|
125
|
+
"""What to do with tool output after execution."""
|
|
126
|
+
|
|
127
|
+
ACCEPT = "accept"
|
|
128
|
+
REDACT = "redact"
|
|
129
|
+
SUPPRESS = "suppress"
|
|
130
|
+
TERMINATE = "terminate"
|
|
131
|
+
ALERT = "alert"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True, slots=True)
|
|
135
|
+
class PostflightVerdict:
|
|
136
|
+
"""Returned by postflight gates. Controls what happens to tool output."""
|
|
137
|
+
|
|
138
|
+
action: PostflightAction = PostflightAction.ACCEPT
|
|
139
|
+
reason: str = ""
|
|
140
|
+
redaction_keys: tuple[str, ...] = ()
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""The traceforge Pipeline: observe -> enrich -> classify -> structure -> [govern] -> sinks.
|
|
2
|
+
|
|
3
|
+
This is the SDK's top-level entry point. It composes traceforge's two halves into one
|
|
4
|
+
object:
|
|
5
|
+
|
|
6
|
+
* the **observation backbone** (:class:`~traceforge.pipeline.EventPipeline`), which
|
|
7
|
+
enriches, classifies, and ML-structures (phase / boundary / title) every event before
|
|
8
|
+
fanning it out to sinks, and
|
|
9
|
+
* the **governance engine** (:class:`~traceforge.governance.pipeline.GovernancePipeline`),
|
|
10
|
+
which scores risk, tracks budgets / drift, and backs the opt-in gating layer.
|
|
11
|
+
|
|
12
|
+
Governance is wired in as **one stage** of the backbone: when enabled, each event is
|
|
13
|
+
scored and its :class:`~traceforge.governance.results.SessionMeta` stamped onto
|
|
14
|
+
``event.metadata.governance`` just before the sinks see it. It is not a separate pipeline
|
|
15
|
+
and not a precondition -- structuring runs with or without it.
|
|
16
|
+
|
|
17
|
+
Gating is a separate, opt-in enforcement layer. The ``gate_*`` helpers install a
|
|
18
|
+
:class:`~traceforge.sdk.GatePolicy` into your agent framework using that framework's native
|
|
19
|
+
blocking mechanism; the policy's preflight callback returns a
|
|
20
|
+
:class:`~traceforge.sdk.Verdict` (ALLOW / DENY). Observation itself never issues verdicts --
|
|
21
|
+
only the gating layer does, and only when you opt in.
|
|
22
|
+
|
|
23
|
+
Usage -- gating a live agent::
|
|
24
|
+
|
|
25
|
+
from traceforge.sdk import Pipeline, GatePolicy, Verdict, ToolCallRequest, GateContext
|
|
26
|
+
|
|
27
|
+
def preflight(request: ToolCallRequest, ctx: GateContext) -> Verdict:
|
|
28
|
+
if request.risk_score > 60:
|
|
29
|
+
return Verdict.deny(f"score {request.risk_score} exceeds threshold")
|
|
30
|
+
return Verdict.allow()
|
|
31
|
+
|
|
32
|
+
pipeline = Pipeline.create(policy=GatePolicy().preflight(preflight))
|
|
33
|
+
tool = pipeline.gate_langchain(tool) # wrap a LangChain tool
|
|
34
|
+
pipeline.gate_crewai() # install CrewAI hooks
|
|
35
|
+
|
|
36
|
+
Usage -- observing an event stream::
|
|
37
|
+
|
|
38
|
+
from traceforge.sdk import Pipeline
|
|
39
|
+
from traceforge.sinks.jsonl import JsonlSink
|
|
40
|
+
|
|
41
|
+
async with Pipeline.create(sinks=[JsonlSink("events.jsonl")]) as pipeline:
|
|
42
|
+
async for event in adapter.stream(...):
|
|
43
|
+
await pipeline.push(event) # enriched, structured, governed, emitted
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from typing import TYPE_CHECKING
|
|
49
|
+
|
|
50
|
+
from traceforge.enricher import Enricher
|
|
51
|
+
from traceforge.governance.pipeline import GovernancePipeline
|
|
52
|
+
from traceforge.pipeline import EventPipeline
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from traceforge.config.models import GovernanceConfig
|
|
56
|
+
from traceforge.sdk.gate_policy import GatePolicy
|
|
57
|
+
from traceforge.sinks.base import StorageSink
|
|
58
|
+
from traceforge.trace import EventTrace
|
|
59
|
+
from traceforge.types import SessionEvent, TelemetrySpan, UsageRecord
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Pipeline:
|
|
63
|
+
"""Observe -> structure backbone with governance as one stage and opt-in gating.
|
|
64
|
+
|
|
65
|
+
Construct via :meth:`create` or :meth:`from_config`; do not instantiate directly.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, backbone: EventPipeline, governance: GovernancePipeline) -> None:
|
|
69
|
+
self._backbone = backbone
|
|
70
|
+
self._governance = governance
|
|
71
|
+
|
|
72
|
+
# ---- construction -------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def create(
|
|
76
|
+
cls,
|
|
77
|
+
config: "GovernanceConfig | None" = None,
|
|
78
|
+
*,
|
|
79
|
+
policy: "GatePolicy | None" = None,
|
|
80
|
+
sinks: "list[StorageSink] | None" = None,
|
|
81
|
+
enable_structure: bool = True,
|
|
82
|
+
enable_title: bool = False,
|
|
83
|
+
enricher: "Enricher | None" = None,
|
|
84
|
+
governance: bool = True,
|
|
85
|
+
) -> "Pipeline":
|
|
86
|
+
"""Build a pipeline from config.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: GovernanceConfig for the governance engine (in-memory DB and
|
|
90
|
+
sensible defaults when omitted).
|
|
91
|
+
policy: Optional GatePolicy enabling the gating layer (the ``gate_*``
|
|
92
|
+
helpers). Omit for observation-only usage.
|
|
93
|
+
sinks: Observation destinations for pushed events. Omit for gating-only
|
|
94
|
+
usage (you never call :meth:`push`).
|
|
95
|
+
enable_structure: Run phase + boundary ML structuring on pushed events
|
|
96
|
+
(default True). Models load lazily on first push, so gating-only
|
|
97
|
+
usage pays nothing.
|
|
98
|
+
enable_title: Also run session-title inference (default False).
|
|
99
|
+
enricher: Custom :class:`~traceforge.enricher.Enricher`; defaults to a
|
|
100
|
+
zero-config ``Enricher()``.
|
|
101
|
+
governance: Wire the governance engine in as a pipeline stage so pushed
|
|
102
|
+
events get ``metadata.governance`` stamped (default True). Set False
|
|
103
|
+
for pure observation; ``gate_*`` / :meth:`score_tool_call` still use
|
|
104
|
+
the engine.
|
|
105
|
+
"""
|
|
106
|
+
gov_engine = GovernancePipeline.create(config, policy=policy)
|
|
107
|
+
return cls._assemble(
|
|
108
|
+
gov_engine,
|
|
109
|
+
sinks=sinks,
|
|
110
|
+
enable_structure=enable_structure,
|
|
111
|
+
enable_title=enable_title,
|
|
112
|
+
enricher=enricher,
|
|
113
|
+
governance=governance,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_config(
|
|
118
|
+
cls,
|
|
119
|
+
path=None,
|
|
120
|
+
*,
|
|
121
|
+
policy: "GatePolicy | None" = None,
|
|
122
|
+
sinks: "list[StorageSink] | None" = None,
|
|
123
|
+
enable_structure: bool = True,
|
|
124
|
+
enable_title: bool = False,
|
|
125
|
+
enricher: "Enricher | None" = None,
|
|
126
|
+
governance: bool = True,
|
|
127
|
+
) -> "Pipeline":
|
|
128
|
+
"""Build a pipeline from a ``traceforge.yaml`` file.
|
|
129
|
+
|
|
130
|
+
Same knobs as :meth:`create`, but the governance engine is loaded from the
|
|
131
|
+
given config file (see :meth:`GovernancePipeline.from_config`).
|
|
132
|
+
|
|
133
|
+
Sink hydration (SDK/CLI parity, issue #116): when ``sinks`` is **omitted**
|
|
134
|
+
(left as ``None``), the standard sinks declared in the config's pipelines
|
|
135
|
+
(``pipelines[].sinks``) are auto-built from their ``SinkConfig`` entries via
|
|
136
|
+
the shared :func:`traceforge.sinks.factory.build_sinks` — the same factory the
|
|
137
|
+
``traceforge watch`` daemon uses. Sinks declared across multiple named
|
|
138
|
+
pipelines are flattened into one list, since the SDK backbone is a single
|
|
139
|
+
source/adapter-agnostic event stream and every declared sink is a live
|
|
140
|
+
destination.
|
|
141
|
+
|
|
142
|
+
Programmatic sinks always win: passing ``sinks=[...]`` — **including an
|
|
143
|
+
explicit empty list ``[]``** — is honored exactly and disables
|
|
144
|
+
auto-hydration. ``None`` is the sole "omitted" sentinel.
|
|
145
|
+
"""
|
|
146
|
+
gov_engine = GovernancePipeline.from_config(path, policy=policy)
|
|
147
|
+
|
|
148
|
+
if sinks is None:
|
|
149
|
+
from traceforge.config.loader import load_config_from_path
|
|
150
|
+
from traceforge.sinks.factory import build_sinks
|
|
151
|
+
|
|
152
|
+
config = load_config_from_path(path)
|
|
153
|
+
sinks = build_sinks(
|
|
154
|
+
[sink_config for pipeline in config.pipelines for sink_config in pipeline.sinks]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return cls._assemble(
|
|
158
|
+
gov_engine,
|
|
159
|
+
sinks=sinks,
|
|
160
|
+
enable_structure=enable_structure,
|
|
161
|
+
enable_title=enable_title,
|
|
162
|
+
enricher=enricher,
|
|
163
|
+
governance=governance,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def _assemble(
|
|
168
|
+
cls,
|
|
169
|
+
gov_engine: GovernancePipeline,
|
|
170
|
+
*,
|
|
171
|
+
sinks,
|
|
172
|
+
enable_structure,
|
|
173
|
+
enable_title,
|
|
174
|
+
enricher,
|
|
175
|
+
governance,
|
|
176
|
+
) -> "Pipeline":
|
|
177
|
+
backbone = EventPipeline(
|
|
178
|
+
sinks=list(sinks) if sinks is not None else [],
|
|
179
|
+
enricher=enricher if enricher is not None else Enricher(),
|
|
180
|
+
enable_phase=enable_structure,
|
|
181
|
+
enable_boundary=enable_structure,
|
|
182
|
+
enable_title=enable_title,
|
|
183
|
+
governance=gov_engine if governance else None,
|
|
184
|
+
)
|
|
185
|
+
return cls(backbone=backbone, governance=gov_engine)
|
|
186
|
+
|
|
187
|
+
# ---- observation backbone ----------------------------------------------
|
|
188
|
+
|
|
189
|
+
async def push(self, event: "SessionEvent") -> None:
|
|
190
|
+
"""Enrich, classify, structure, govern, and emit a single event."""
|
|
191
|
+
await self._backbone.push(event)
|
|
192
|
+
|
|
193
|
+
async def push_span(self, span: "TelemetrySpan") -> None:
|
|
194
|
+
"""Emit a telemetry span to the sinks."""
|
|
195
|
+
await self._backbone.push_span(span)
|
|
196
|
+
|
|
197
|
+
async def push_usage(self, usage: "UsageRecord") -> None:
|
|
198
|
+
"""Emit a usage record to the sinks."""
|
|
199
|
+
await self._backbone.push_usage(usage)
|
|
200
|
+
|
|
201
|
+
async def flush(self) -> None:
|
|
202
|
+
"""Flush held plumbing and pending refinements, then flush all sinks."""
|
|
203
|
+
await self._backbone.flush()
|
|
204
|
+
|
|
205
|
+
async def close(self) -> None:
|
|
206
|
+
"""Flush, then close all sinks."""
|
|
207
|
+
await self._backbone.close()
|
|
208
|
+
|
|
209
|
+
async def __aenter__(self) -> "Pipeline":
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
async def __aexit__(self, *exc) -> None:
|
|
213
|
+
await self.close()
|
|
214
|
+
|
|
215
|
+
# ---- preflight scoring (governance stage, read-only) -------------------
|
|
216
|
+
|
|
217
|
+
def score_tool_call(self, payload: dict) -> "EventTrace":
|
|
218
|
+
"""Score a prospective tool call (dict) without mutating session state."""
|
|
219
|
+
return self._governance.score_tool_call(payload)
|
|
220
|
+
|
|
221
|
+
# ---- gating layer (opt-in; delegates to the governance engine) ---------
|
|
222
|
+
|
|
223
|
+
def gate_crewai(self) -> None:
|
|
224
|
+
"""Install CrewAI gating hooks."""
|
|
225
|
+
return self._governance.gate_crewai()
|
|
226
|
+
|
|
227
|
+
def gate_langchain(self, tool):
|
|
228
|
+
"""Wrap a LangChain tool so calls are gated. Returns the wrapped tool."""
|
|
229
|
+
return self._governance.gate_langchain(tool)
|
|
230
|
+
|
|
231
|
+
def gate_langgraph(self, tools):
|
|
232
|
+
"""Wrap LangGraph tools. Returns a gated tool node."""
|
|
233
|
+
return self._governance.gate_langgraph(tools)
|
|
234
|
+
|
|
235
|
+
def gate_semantic_kernel(self, kernel) -> None:
|
|
236
|
+
"""Install a Semantic Kernel gating filter."""
|
|
237
|
+
return self._governance.gate_semantic_kernel(kernel)
|
|
238
|
+
|
|
239
|
+
def gate_maf(self):
|
|
240
|
+
"""Return Microsoft Agent Framework gating middleware."""
|
|
241
|
+
return self._governance.gate_maf()
|
|
242
|
+
|
|
243
|
+
def gate_smolagents(self, agent_cls=None):
|
|
244
|
+
"""Gate smolagents. Returns a gated agent class."""
|
|
245
|
+
return self._governance.gate_smolagents(agent_cls)
|
|
246
|
+
|
|
247
|
+
def gate_pydantic_ai(self, agent) -> None:
|
|
248
|
+
"""Install Pydantic AI gating."""
|
|
249
|
+
return self._governance.gate_pydantic_ai(agent)
|
|
250
|
+
|
|
251
|
+
def gate_openai_agents(self, agent):
|
|
252
|
+
"""Gate an OpenAI Agents SDK agent."""
|
|
253
|
+
return self._governance.gate_openai_agents(agent)
|
|
254
|
+
|
|
255
|
+
# ---- escape hatches ----------------------------------------------------
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def governance(self) -> GovernancePipeline:
|
|
259
|
+
"""The underlying governance engine (scoring, budgets, gating internals)."""
|
|
260
|
+
return self._governance
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def backbone(self) -> EventPipeline:
|
|
264
|
+
"""The underlying observation / structuring EventPipeline."""
|
|
265
|
+
return self._backbone
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Verdict types for tool-call gating decisions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from traceforge.sdk.gate_types import (
|
|
11
|
+
GateContext,
|
|
12
|
+
PostflightVerdict,
|
|
13
|
+
ToolCallRequest,
|
|
14
|
+
ToolCallResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ─── Decision & Verdict ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Decision(Enum):
|
|
22
|
+
"""The outcome of a gating decision."""
|
|
23
|
+
|
|
24
|
+
ALLOW = "allow"
|
|
25
|
+
DENY = "deny"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Verdict:
|
|
30
|
+
"""A gating decision returned by a gate callback.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
decision: ALLOW or DENY.
|
|
34
|
+
reason: Human-readable reason propagated to the LLM on denial.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
decision: Decision
|
|
38
|
+
reason: str = ""
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def allowed(self) -> bool:
|
|
42
|
+
return self.decision == Decision.ALLOW
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def denied(self) -> bool:
|
|
46
|
+
return self.decision == Decision.DENY
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def allow() -> Verdict:
|
|
50
|
+
"""Convenience factory for ALLOW."""
|
|
51
|
+
return Verdict(decision=Decision.ALLOW)
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def deny(reason: str = "") -> Verdict:
|
|
55
|
+
"""Convenience factory for DENY with reason."""
|
|
56
|
+
return Verdict(decision=Decision.DENY, reason=reason)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ─── Callback Protocols ───────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@runtime_checkable
|
|
63
|
+
class PreflightGate(Protocol):
|
|
64
|
+
"""Protocol for preflight gate callbacks.
|
|
65
|
+
|
|
66
|
+
Receives a ToolCallRequest (policy-focused view) and GateContext.
|
|
67
|
+
Must return a Verdict (ALLOW or DENY).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __call__(self, request: "ToolCallRequest", ctx: "GateContext") -> Verdict: ...
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@runtime_checkable
|
|
74
|
+
class PostflightGate(Protocol):
|
|
75
|
+
"""Protocol for postflight gate callbacks.
|
|
76
|
+
|
|
77
|
+
Receives a ToolCallResult (includes tool output) and GateContext.
|
|
78
|
+
Must return a PostflightVerdict.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __call__(self, result: "ToolCallResult", ctx: "GateContext") -> "PostflightVerdict": ...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Storage sinks for the traceforge pipeline."""
|
|
2
|
+
|
|
3
|
+
from traceforge.sinks.base import StorageSink
|
|
4
|
+
from traceforge.sinks.callback import CallbackSink
|
|
5
|
+
from traceforge.sinks.console import ConsoleSink
|
|
6
|
+
from traceforge.sinks.jsonl import JsonlSink
|
|
7
|
+
from traceforge.sinks.otel_exporter import OtelExporterSink
|
|
8
|
+
from traceforge.sinks.parquet import ParquetSink
|
|
9
|
+
from traceforge.sinks.s3 import S3Sink
|
|
10
|
+
from traceforge.sinks.sqlite_output import SqliteOutputSink
|
|
11
|
+
from traceforge.sinks.webhook import WebhookSink
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"StorageSink",
|
|
15
|
+
"CallbackSink",
|
|
16
|
+
"ConsoleSink",
|
|
17
|
+
"JsonlSink",
|
|
18
|
+
"OtelExporterSink",
|
|
19
|
+
"ParquetSink",
|
|
20
|
+
"S3Sink",
|
|
21
|
+
"SqliteOutputSink",
|
|
22
|
+
"WebhookSink",
|
|
23
|
+
]
|