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,579 @@
|
|
|
1
|
+
"""Pydantic models for traceforge configuration.
|
|
2
|
+
|
|
3
|
+
All config surfaces (YAML files, env vars, constructor args) share these
|
|
4
|
+
strongly-typed models. Discriminated unions provide exhaustive validation
|
|
5
|
+
while remaining extensible via the factory registry.
|
|
6
|
+
|
|
7
|
+
Follows SOLID:
|
|
8
|
+
- Single Responsibility: each config class owns one concern
|
|
9
|
+
- Open/Closed: new source/sink types add a model + register, don't modify existing
|
|
10
|
+
- Liskov: all SourceConfig subtypes are substitutable
|
|
11
|
+
- Interface Segregation: SDK-only configs (CallbackSink) are separate from serializable
|
|
12
|
+
- Dependency Inversion: pipeline config references abstractions (type discriminators)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Annotated, Literal
|
|
19
|
+
|
|
20
|
+
from pydantic import Field, field_validator, model_validator
|
|
21
|
+
|
|
22
|
+
from traceforge.models import StrictModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ─── Source Configs ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FileWatchSourceConfig(StrictModel):
|
|
29
|
+
"""Watch a file for appended content (OS-native events via watchdog)."""
|
|
30
|
+
|
|
31
|
+
type: Literal["file_watch"] = "file_watch"
|
|
32
|
+
path: Path
|
|
33
|
+
start_at: Literal["beginning", "end"] = "end"
|
|
34
|
+
encoding: str = "utf-8"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FilePollSourceConfig(StrictModel):
|
|
38
|
+
"""Poll a file for changes at a fixed interval."""
|
|
39
|
+
|
|
40
|
+
type: Literal["file_poll"] = "file_poll"
|
|
41
|
+
path: Path
|
|
42
|
+
interval: float = 1.0
|
|
43
|
+
missing: Literal["wait", "error"] = "wait"
|
|
44
|
+
encoding: str = "utf-8"
|
|
45
|
+
|
|
46
|
+
@field_validator("interval")
|
|
47
|
+
@classmethod
|
|
48
|
+
def _interval_positive(cls, v: float) -> float:
|
|
49
|
+
if v <= 0:
|
|
50
|
+
raise ValueError("interval must be positive")
|
|
51
|
+
return v
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class HttpPollSourceConfig(StrictModel):
|
|
55
|
+
"""Poll an HTTP endpoint at a fixed interval."""
|
|
56
|
+
|
|
57
|
+
type: Literal["http_poll"] = "http_poll"
|
|
58
|
+
url: str
|
|
59
|
+
interval: float = 30.0
|
|
60
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
61
|
+
cursor_header: str | None = None
|
|
62
|
+
timeout: float = 30.0
|
|
63
|
+
max_retries: int = 3
|
|
64
|
+
|
|
65
|
+
@field_validator("interval")
|
|
66
|
+
@classmethod
|
|
67
|
+
def _interval_positive(cls, v: float) -> float:
|
|
68
|
+
if v <= 0:
|
|
69
|
+
raise ValueError("interval must be positive")
|
|
70
|
+
return v
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SSESourceConfig(StrictModel):
|
|
74
|
+
"""Connect to a Server-Sent Events stream."""
|
|
75
|
+
|
|
76
|
+
type: Literal["sse"] = "sse"
|
|
77
|
+
url: str
|
|
78
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
79
|
+
reconnect_interval: float = 3.0
|
|
80
|
+
timeout: float = 60.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ReplaySourceConfig(StrictModel):
|
|
84
|
+
"""Replay a previously-captured file (one-shot, no watching)."""
|
|
85
|
+
|
|
86
|
+
type: Literal["replay"] = "replay"
|
|
87
|
+
path: Path
|
|
88
|
+
encoding: str = "utf-8"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Discriminated union of all serializable source types
|
|
92
|
+
SourceConfig = Annotated[
|
|
93
|
+
FileWatchSourceConfig
|
|
94
|
+
| FilePollSourceConfig
|
|
95
|
+
| HttpPollSourceConfig
|
|
96
|
+
| SSESourceConfig
|
|
97
|
+
| ReplaySourceConfig,
|
|
98
|
+
Field(discriminator="type"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ─── Adapter Configs ─────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class MappedJsonAdapterConfig(StrictModel):
|
|
106
|
+
"""Data-driven adapter using a YAML framework mapping."""
|
|
107
|
+
|
|
108
|
+
type: Literal["mapped_json"] = "mapped_json"
|
|
109
|
+
mapping: str # framework name (resolved from bundled + user mappings)
|
|
110
|
+
mapping_file: Path | None = None # explicit path override
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class OtelSpanAdapterConfig(StrictModel):
|
|
114
|
+
"""Adapter for OpenTelemetry span JSON."""
|
|
115
|
+
|
|
116
|
+
type: Literal["otel_span"] = "otel_span"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Discriminated union of all adapter types
|
|
120
|
+
AdapterConfig = Annotated[
|
|
121
|
+
MappedJsonAdapterConfig | OtelSpanAdapterConfig,
|
|
122
|
+
Field(discriminator="type"),
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ─── Sink Configs ────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SqliteSinkConfig(StrictModel):
|
|
130
|
+
"""SQLite storage sink."""
|
|
131
|
+
|
|
132
|
+
type: Literal["sqlite"] = "sqlite"
|
|
133
|
+
path: Path
|
|
134
|
+
journal_mode: Literal["wal", "delete", "truncate"] = "wal"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class JsonlSinkConfig(StrictModel):
|
|
138
|
+
"""Append-only JSONL file sink."""
|
|
139
|
+
|
|
140
|
+
type: Literal["jsonl"] = "jsonl"
|
|
141
|
+
path: Path
|
|
142
|
+
rotate_size_mb: float | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ConsoleSinkConfig(StrictModel):
|
|
146
|
+
"""Pretty-print governance results to terminal."""
|
|
147
|
+
|
|
148
|
+
type: Literal["console"] = "console"
|
|
149
|
+
filter: list[str] = Field(default_factory=lambda: ["warn", "deny", "escalate"])
|
|
150
|
+
color: bool = True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class WebhookSinkConfig(StrictModel):
|
|
154
|
+
"""POST governance results to a webhook URL."""
|
|
155
|
+
|
|
156
|
+
type: Literal["webhook"] = "webhook"
|
|
157
|
+
url: str
|
|
158
|
+
filter: list[str] = Field(default_factory=lambda: ["deny", "escalate"])
|
|
159
|
+
timeout: float = 10.0
|
|
160
|
+
max_retries: int = 3
|
|
161
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class OtelSinkConfig(StrictModel):
|
|
165
|
+
"""Export governance results as OTel spans."""
|
|
166
|
+
|
|
167
|
+
type: Literal["otel"] = "otel"
|
|
168
|
+
endpoint: str = "http://localhost:4318/v1/traces"
|
|
169
|
+
service_name: str = "traceforge"
|
|
170
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class S3SinkConfig(StrictModel):
|
|
174
|
+
"""S3-compatible object store sink."""
|
|
175
|
+
|
|
176
|
+
type: Literal["s3"] = "s3"
|
|
177
|
+
bucket: str
|
|
178
|
+
prefix: str = ""
|
|
179
|
+
region: str | None = None
|
|
180
|
+
endpoint_url: str | None = None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Discriminated union of all serializable sink types
|
|
184
|
+
SinkConfig = Annotated[
|
|
185
|
+
SqliteSinkConfig
|
|
186
|
+
| JsonlSinkConfig
|
|
187
|
+
| ConsoleSinkConfig
|
|
188
|
+
| WebhookSinkConfig
|
|
189
|
+
| OtelSinkConfig
|
|
190
|
+
| S3SinkConfig,
|
|
191
|
+
Field(discriminator="type"),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ─── Pipeline Config ─────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class PipelineConfig(StrictModel):
|
|
199
|
+
"""A single ingestion pipeline: source → adapter → sinks."""
|
|
200
|
+
|
|
201
|
+
name: str
|
|
202
|
+
source: SourceConfig
|
|
203
|
+
adapter: AdapterConfig
|
|
204
|
+
sinks: list[SinkConfig] = Field(min_length=1)
|
|
205
|
+
|
|
206
|
+
@field_validator("name")
|
|
207
|
+
@classmethod
|
|
208
|
+
def _name_non_empty(cls, v: str) -> str:
|
|
209
|
+
if not v.strip():
|
|
210
|
+
raise ValueError("pipeline name must be non-empty")
|
|
211
|
+
return v.strip()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ─── SDK Config ──────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class SDKConfig(StrictModel):
|
|
218
|
+
"""Configuration for traceforge SDK (in-process push mode)."""
|
|
219
|
+
|
|
220
|
+
batch_size: int = Field(default=64, ge=1)
|
|
221
|
+
flush_interval: float = Field(default=5.0, gt=0)
|
|
222
|
+
max_queue_size: int = Field(default=10000, ge=1)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ─── Governance Config ────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class BudgetConfig(StrictModel):
|
|
229
|
+
"""Budget thresholds for governance scoring."""
|
|
230
|
+
|
|
231
|
+
max_tool_calls: int | None = None
|
|
232
|
+
max_by_effect: dict[str, int] | None = None
|
|
233
|
+
max_by_capability: dict[str, int] | None = None
|
|
234
|
+
max_by_scope: dict[str, int] | None = None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class SubprocessGateConfig(StrictModel):
|
|
238
|
+
"""External preflight gate that shells out to a decider command per call.
|
|
239
|
+
|
|
240
|
+
The JSON request is written to the process's stdin; the JSON verdict is read
|
|
241
|
+
from stdout. Fail-closed by default (any error/timeout/bad output -> DENY).
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
type: Literal["subprocess"] = "subprocess"
|
|
245
|
+
command: str
|
|
246
|
+
timeout: float = Field(default=10.0, gt=0)
|
|
247
|
+
fail_open: bool = False
|
|
248
|
+
max_input_bytes: int = Field(default=65536, gt=0)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class HttpGateConfig(StrictModel):
|
|
252
|
+
"""External preflight gate that POSTs the request to a persistent HTTP PDP.
|
|
253
|
+
|
|
254
|
+
Recommended mode (e.g. an OPA REST server). Fail-closed by default (any
|
|
255
|
+
error/timeout/non-2xx -> DENY). ``headers`` carries auth tokens.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
type: Literal["http"] = "http"
|
|
259
|
+
endpoint: str
|
|
260
|
+
timeout: float = Field(default=2.0, gt=0)
|
|
261
|
+
fail_open: bool = False
|
|
262
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
263
|
+
max_input_bytes: int = Field(default=65536, gt=0)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# Discriminated union of external (out-of-process) preflight gate types
|
|
267
|
+
ExternalGateConfig = Annotated[
|
|
268
|
+
SubprocessGateConfig | HttpGateConfig,
|
|
269
|
+
Field(discriminator="type"),
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class GovernanceConfig(StrictModel):
|
|
274
|
+
"""Governance pipeline configuration.
|
|
275
|
+
|
|
276
|
+
Same shape in YAML and SDK::
|
|
277
|
+
|
|
278
|
+
# traceforge.yaml
|
|
279
|
+
governance:
|
|
280
|
+
db_path: ./traceforge.db
|
|
281
|
+
project_root: .
|
|
282
|
+
pii_scanning: true
|
|
283
|
+
integrity_verification: true
|
|
284
|
+
budget:
|
|
285
|
+
max_tool_calls: 200
|
|
286
|
+
max_by_effect:
|
|
287
|
+
destructive: 10
|
|
288
|
+
|
|
289
|
+
# SDK equivalent
|
|
290
|
+
GovernanceConfig(
|
|
291
|
+
db_path="./traceforge.db",
|
|
292
|
+
project_root=".",
|
|
293
|
+
pii_scanning=True,
|
|
294
|
+
integrity_verification=True,
|
|
295
|
+
budget=BudgetConfig(max_tool_calls=200, max_by_effect={"destructive": 10}),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
Preflight gating is configured one of two mutually-exclusive ways:
|
|
299
|
+
|
|
300
|
+
* ``tool_preflight_gate`` — a dotted import path to an in-process Python
|
|
301
|
+
``PreflightGate`` callable (SDK / code-defined policy).
|
|
302
|
+
* ``preflight_gate`` — an out-of-process, YAML-native external decider
|
|
303
|
+
(``http`` or ``subprocess``); see :data:`ExternalGateConfig`.
|
|
304
|
+
|
|
305
|
+
Setting both is a configuration error (raised by the validator below).
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
db_path: str | None = None # None = in-memory
|
|
309
|
+
project_root: str | None = None
|
|
310
|
+
rules_path: str | None = None # custom rules YAML override
|
|
311
|
+
pii_scanning: bool = True
|
|
312
|
+
integrity_verification: bool = True # content-hash tamper detection (baseline + drift)
|
|
313
|
+
budget: BudgetConfig = Field(default_factory=BudgetConfig)
|
|
314
|
+
tool_preflight_gate: str | None = None # dotted import path (e.g. "myapp.policies.my_policy")
|
|
315
|
+
preflight_gate: ExternalGateConfig | None = (
|
|
316
|
+
None # out-of-process external decider (http/subprocess)
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@model_validator(mode="after")
|
|
320
|
+
def _preflight_gate_exclusivity(self) -> "GovernanceConfig":
|
|
321
|
+
"""``tool_preflight_gate`` and ``preflight_gate`` are mutually exclusive.
|
|
322
|
+
|
|
323
|
+
They configure the same slot (the preflight gate chain) via two different
|
|
324
|
+
mechanisms; allowing both would make precedence ambiguous. Fail loudly.
|
|
325
|
+
"""
|
|
326
|
+
if self.tool_preflight_gate is not None and self.preflight_gate is not None:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
"governance.tool_preflight_gate (dotted import path) and "
|
|
329
|
+
"governance.preflight_gate (external gate config) are mutually "
|
|
330
|
+
"exclusive — set only one."
|
|
331
|
+
)
|
|
332
|
+
return self
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ─── Score API Config ────────────────────────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class ScoreAPIConfig(StrictModel):
|
|
339
|
+
"""Configuration for the preflight scoring HTTP endpoint."""
|
|
340
|
+
|
|
341
|
+
enabled: bool = True
|
|
342
|
+
listen: str = "localhost:7331"
|
|
343
|
+
socket: str | None = None # Unix socket alternative (lower latency)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ─── Auto-Detect Config ──────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class AutoDetectConfig(StrictModel):
|
|
350
|
+
"""Framework auto-detection settings."""
|
|
351
|
+
|
|
352
|
+
enabled: bool = True
|
|
353
|
+
frameworks: list[str] = Field(default_factory=list) # empty = detect all known
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ─── Phase Tracker Config ────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class PhaseTrackerConfig(StrictModel):
|
|
360
|
+
"""Phase tracker (debounced majority-vote segmentation) configuration.
|
|
361
|
+
|
|
362
|
+
No hardcoded numeric constants live in the tracker; they all live here.
|
|
363
|
+
Defaults are evidence-based starting points, not magic numbers, and are
|
|
364
|
+
meant to be replaced by the values measured in the
|
|
365
|
+
``phase-tracker-window-sweep`` calibration experiment.
|
|
366
|
+
|
|
367
|
+
# traceforge.yaml
|
|
368
|
+
phase_tracker:
|
|
369
|
+
enabled: true
|
|
370
|
+
window_size: 3
|
|
371
|
+
debounce: 2
|
|
372
|
+
phase_root_depth: 1
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
enabled: bool = True
|
|
376
|
+
|
|
377
|
+
# Sliding window of activity-derived phase signals whose mode is the current
|
|
378
|
+
# phase. Default seeded from Banos 2014 / Wang 2019 short-stream HAR work;
|
|
379
|
+
# recalibrate via phase-tracker-window-sweep.yaml.
|
|
380
|
+
window_size: int = Field(default=3, ge=1)
|
|
381
|
+
|
|
382
|
+
# Consecutive events the window mode must hold a new value before a boundary
|
|
383
|
+
# commits. Higher = fewer spurious transitions, more detection latency.
|
|
384
|
+
debounce: int = Field(default=2, ge=1)
|
|
385
|
+
|
|
386
|
+
# Dot-path depth used to group activities into the root compared at
|
|
387
|
+
# boundaries (1 => 'verification.lint' and 'verification.test' share root
|
|
388
|
+
# 'verification' and do not open a new block).
|
|
389
|
+
phase_root_depth: int = Field(default=1, ge=1)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ─── Title Config ─────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class SessionNamingHeuristicConfig(StrictModel):
|
|
396
|
+
"""Deterministic, zero-cost session-title heuristic (the default floor).
|
|
397
|
+
|
|
398
|
+
Session naming derives a session title from the first substantive user
|
|
399
|
+
message. The heuristic is *extractive* -- it reuses the user's own words --
|
|
400
|
+
so it is coherent by construction (no model, no network, no key). Its ceiling
|
|
401
|
+
is the phrasing already in the message; for abstractive titles, opt into the
|
|
402
|
+
``api`` strategy.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
method: Literal["clip", "imperative", "keyphrase", "hybrid"] = "hybrid"
|
|
406
|
+
max_words: int = Field(default=8, ge=1)
|
|
407
|
+
max_chars: int = Field(default=60, ge=8)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class TitleApiConfig(StrictModel):
|
|
411
|
+
"""Shared opt-in LiteLLM API-tier settings for title generation.
|
|
412
|
+
|
|
413
|
+
Both session naming and activity/step titling reach any LLM provider through
|
|
414
|
+
this one typed surface. The API key is **never** stored here. LiteLLM reads it
|
|
415
|
+
from the provider's conventional environment variable (``OPENAI_API_KEY``,
|
|
416
|
+
``ANTHROPIC_API_KEY``, ``AZURE_API_KEY`` …). Set ``api_key_env`` only to name
|
|
417
|
+
a *different* env var to read from. ``model`` is any LiteLLM model string, so
|
|
418
|
+
OpenAI, Azure, Anthropic, Gemini, and local runtimes (``ollama/…``, ``vllm/…``
|
|
419
|
+
via ``api_base``) are all reachable through one code path.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
model: str = "gpt-4o-mini"
|
|
423
|
+
api_base: str | None = None
|
|
424
|
+
api_key_env: str | None = None
|
|
425
|
+
timeout: float = Field(default=10.0, gt=0)
|
|
426
|
+
max_tokens: int = Field(default=24, ge=1)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class SessionNamingApiConfig(TitleApiConfig):
|
|
430
|
+
"""Opt-in LLM API tier for session naming, served via LiteLLM.
|
|
431
|
+
|
|
432
|
+
Inherits the shared :class:`TitleApiConfig` surface unchanged: session titles
|
|
433
|
+
are short, so the base ``max_tokens`` default is sufficient.
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class SessionNamingConfig(StrictModel):
|
|
438
|
+
"""How the session title is generated from the first substantive user message.
|
|
439
|
+
|
|
440
|
+
# traceforge.yaml
|
|
441
|
+
title:
|
|
442
|
+
session_naming:
|
|
443
|
+
strategy: heuristic # heuristic | api (DEFAULT: heuristic)
|
|
444
|
+
heuristic:
|
|
445
|
+
method: hybrid # clip | imperative | keyphrase | hybrid
|
|
446
|
+
max_words: 8
|
|
447
|
+
max_chars: 60
|
|
448
|
+
api:
|
|
449
|
+
model: gpt-4o-mini # any LiteLLM model string
|
|
450
|
+
api_base: null # azure / ollama / vllm / openai-compatible
|
|
451
|
+
api_key_env: null # override env var name (else LiteLLM default)
|
|
452
|
+
timeout: 10
|
|
453
|
+
max_tokens: 24
|
|
454
|
+
|
|
455
|
+
``heuristic`` (default) is free and coherent by construction. ``api`` engages
|
|
456
|
+
LiteLLM for an abstractive title and takes effect **only** when the provider's
|
|
457
|
+
API key is present in the environment; otherwise it silently falls back to the
|
|
458
|
+
heuristic, so a missing key never errors or blocks.
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
strategy: Literal["heuristic", "api"] = "heuristic"
|
|
462
|
+
heuristic: SessionNamingHeuristicConfig = Field(default_factory=SessionNamingHeuristicConfig)
|
|
463
|
+
api: SessionNamingApiConfig = Field(default_factory=SessionNamingApiConfig)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class ActivityTitlingApiConfig(TitleApiConfig):
|
|
467
|
+
"""Opt-in LLM API tier for activity/step (span) titling, served via LiteLLM.
|
|
468
|
+
|
|
469
|
+
One call per closed activity returns the activity title **and** all of its
|
|
470
|
+
step titles together (as JSON), so the default ``max_tokens`` is larger than
|
|
471
|
+
session naming's to fit a multi-title response.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
max_tokens: int = Field(default=256, ge=1)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class ActivityTitlingConfig(StrictModel):
|
|
478
|
+
"""How activity/step (span) titles are generated for a closed activity.
|
|
479
|
+
|
|
480
|
+
# traceforge.yaml
|
|
481
|
+
title:
|
|
482
|
+
activity_titling:
|
|
483
|
+
strategy: model # model | api (DEFAULT: model)
|
|
484
|
+
api:
|
|
485
|
+
model: gpt-4o-mini # any LiteLLM model string
|
|
486
|
+
api_base: null # azure / ollama / vllm / openai-compatible
|
|
487
|
+
api_key_env: null # override env var name (else LiteLLM default)
|
|
488
|
+
timeout: 10
|
|
489
|
+
max_tokens: 256
|
|
490
|
+
|
|
491
|
+
``model`` (default) titles each closed activity and its steps with the
|
|
492
|
+
packaged, offline ONNX ``traceforge-title-model`` — free, no key, no network,
|
|
493
|
+
byte-for-byte the shipped behavior. ``api`` engages LiteLLM to *refine* those
|
|
494
|
+
titles and takes effect **only** when the provider's API key is present in the
|
|
495
|
+
environment; otherwise it silently keeps the packaged-model titles, so a
|
|
496
|
+
missing key never errors or blocks.
|
|
497
|
+
|
|
498
|
+
The packaged title is always emitted the instant an activity closes; when the
|
|
499
|
+
API tier is configured and keyed the abstractive upgrade arrives **later** as
|
|
500
|
+
an append-only title update, computed off the hot path so live event emission
|
|
501
|
+
is never delayed by the network.
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
strategy: Literal["model", "api"] = "model"
|
|
505
|
+
api: ActivityTitlingApiConfig = Field(default_factory=ActivityTitlingApiConfig)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class TitleConfig(StrictModel):
|
|
509
|
+
"""Titling configuration.
|
|
510
|
+
|
|
511
|
+
Two title surfaces, each a free/offline floor with an opt-in LiteLLM API tier
|
|
512
|
+
that engages only when a provider key is present in the environment:
|
|
513
|
+
|
|
514
|
+
* ``session_naming`` — the session title derived from the first substantive
|
|
515
|
+
user message. Floor: a zero-cost extractive heuristic over the user's own
|
|
516
|
+
words; see :class:`SessionNamingConfig`.
|
|
517
|
+
* ``activity_titling`` — the activity/step (span) titles. Floor: the packaged,
|
|
518
|
+
offline ONNX ``traceforge-title-model`` (proven strong at that task and
|
|
519
|
+
shipped with every install); see :class:`ActivityTitlingConfig`.
|
|
520
|
+
|
|
521
|
+
In both cases the offline floor is emitted immediately and the API upgrade
|
|
522
|
+
(when configured and keyed) is applied later, off the hot path, as an
|
|
523
|
+
append-only title update — never blocking live event emission.
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
session_naming: SessionNamingConfig = Field(default_factory=SessionNamingConfig)
|
|
527
|
+
activity_titling: ActivityTitlingConfig = Field(default_factory=ActivityTitlingConfig)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
# ─── Root Config ─────────────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class TraceforgeConfig(StrictModel):
|
|
534
|
+
"""Root traceforge configuration.
|
|
535
|
+
|
|
536
|
+
Loaded from (in precedence order):
|
|
537
|
+
1. Constructor/init kwargs (highest)
|
|
538
|
+
2. Environment variables (TRACEFORGE_ prefix)
|
|
539
|
+
3. Project-local: ./traceforge.yaml
|
|
540
|
+
4. User-global: ~/.traceforge/config.yaml
|
|
541
|
+
5. Defaults (lowest)
|
|
542
|
+
|
|
543
|
+
Config file location override: TRACEFORGE_CONFIG env var.
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
|
547
|
+
|
|
548
|
+
# Mapping search paths (in addition to bundled)
|
|
549
|
+
mappings_dirs: list[Path] = Field(default_factory=list)
|
|
550
|
+
|
|
551
|
+
# Named pipelines (CLI / file-watch mode)
|
|
552
|
+
pipelines: list[PipelineConfig] = Field(default_factory=list)
|
|
553
|
+
|
|
554
|
+
# SDK-mode configuration
|
|
555
|
+
sdk: SDKConfig = Field(default_factory=SDKConfig)
|
|
556
|
+
|
|
557
|
+
# Governance pipeline configuration
|
|
558
|
+
governance: GovernanceConfig = Field(default_factory=GovernanceConfig)
|
|
559
|
+
|
|
560
|
+
# Score API (preflight scoring endpoint)
|
|
561
|
+
score: ScoreAPIConfig = Field(default_factory=ScoreAPIConfig)
|
|
562
|
+
|
|
563
|
+
# Auto-detection of installed frameworks
|
|
564
|
+
auto_detect: AutoDetectConfig = Field(default_factory=AutoDetectConfig)
|
|
565
|
+
|
|
566
|
+
# Phase tracker (session-level phase segmentation)
|
|
567
|
+
phase_tracker: PhaseTrackerConfig = Field(default_factory=PhaseTrackerConfig)
|
|
568
|
+
|
|
569
|
+
# Titling (span titles + configurable session naming)
|
|
570
|
+
title: TitleConfig = Field(default_factory=TitleConfig)
|
|
571
|
+
|
|
572
|
+
@field_validator("pipelines")
|
|
573
|
+
@classmethod
|
|
574
|
+
def _unique_pipeline_names(cls, v: list[PipelineConfig]) -> list[PipelineConfig]:
|
|
575
|
+
names = [p.name for p in v]
|
|
576
|
+
dupes = [n for n in names if names.count(n) > 1]
|
|
577
|
+
if dupes:
|
|
578
|
+
raise ValueError(f"duplicate pipeline names: {set(dupes)}")
|
|
579
|
+
return v
|