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,414 @@
|
|
|
1
|
+
"""Persistence codec for governance value objects.
|
|
2
|
+
|
|
3
|
+
Bidirectional mapping between :class:`SessionMeta` / ``SessionStateSnapshot`` and the
|
|
4
|
+
JSON-able dicts persisted in the idempotency and audit store. Pure and stateless:
|
|
5
|
+
it owns no store handle and no session residency, only the structural
|
|
6
|
+
(de)serialization the monitor and scorer depend on.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from traceforge.governance.results import (
|
|
15
|
+
Evidence,
|
|
16
|
+
EvidencePointer,
|
|
17
|
+
EscalationContext,
|
|
18
|
+
RecommendedAction,
|
|
19
|
+
RiskRecommendation,
|
|
20
|
+
SessionMeta,
|
|
21
|
+
TransformSuggestion,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from traceforge.governance.state import SessionStateSnapshot
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _decode_budget_dims(raw: list | None) -> tuple[tuple[str, int], ...]:
|
|
29
|
+
"""Safely decode budget dimension pairs from JSON, skipping malformed entries."""
|
|
30
|
+
if not raw:
|
|
31
|
+
return ()
|
|
32
|
+
result = []
|
|
33
|
+
for item in raw:
|
|
34
|
+
if isinstance(item, (list, tuple)) and len(item) == 2:
|
|
35
|
+
key, val = item
|
|
36
|
+
if isinstance(key, str) and isinstance(val, (int, float)):
|
|
37
|
+
result.append((key, int(val)))
|
|
38
|
+
return tuple(result)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MetaCodec:
|
|
42
|
+
"""Serialize and deserialize governance value objects for durable storage."""
|
|
43
|
+
|
|
44
|
+
def serialize_snapshot(self, snapshot: "SessionStateSnapshot") -> dict:
|
|
45
|
+
"""Serialize Phase-1 snapshot for reservation persistence."""
|
|
46
|
+
return {
|
|
47
|
+
"budget": {
|
|
48
|
+
"total_tool_calls": snapshot.budget.total_tool_calls,
|
|
49
|
+
"total_tokens": snapshot.budget.total_tokens,
|
|
50
|
+
"elapsed_seconds": snapshot.budget.elapsed_seconds,
|
|
51
|
+
"pressure": snapshot.budget.pressure,
|
|
52
|
+
"by_effect": list(snapshot.budget.by_effect),
|
|
53
|
+
"by_capability": list(snapshot.budget.by_capability),
|
|
54
|
+
"by_scope": list(snapshot.budget.by_scope),
|
|
55
|
+
"by_role": list(snapshot.budget.by_role),
|
|
56
|
+
"by_phase": list(snapshot.budget.by_phase),
|
|
57
|
+
"by_mechanism": list(snapshot.budget.by_mechanism),
|
|
58
|
+
"by_action": list(snapshot.budget.by_action),
|
|
59
|
+
"by_structure": list(snapshot.budget.by_structure),
|
|
60
|
+
},
|
|
61
|
+
"phase_window": list(snapshot.phase_window),
|
|
62
|
+
"taint_ledger": [
|
|
63
|
+
{
|
|
64
|
+
"event_id": t.event_id,
|
|
65
|
+
"source_event_key": t.source_event_key,
|
|
66
|
+
"clearance": t.clearance,
|
|
67
|
+
"source": t.source,
|
|
68
|
+
"payload_pointer": t.payload_pointer,
|
|
69
|
+
}
|
|
70
|
+
for t in snapshot.taint_ledger
|
|
71
|
+
],
|
|
72
|
+
"last_assistant_event_id": snapshot.last_assistant_event_id,
|
|
73
|
+
"last_user_event_id": snapshot.last_user_event_id,
|
|
74
|
+
"event_count": snapshot.event_count,
|
|
75
|
+
"dropped_events": snapshot.dropped_events,
|
|
76
|
+
"last_sequence": snapshot.last_sequence,
|
|
77
|
+
"gap_ordinal": snapshot.gap_ordinal,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def deserialize_snapshot(self, data: dict) -> "SessionStateSnapshot":
|
|
81
|
+
"""Reconstruct snapshot from persisted reservation."""
|
|
82
|
+
from traceforge.governance.state import BudgetSnapshot, SessionStateSnapshot, TaintEntry
|
|
83
|
+
|
|
84
|
+
budget_data = data.get("budget", {})
|
|
85
|
+
budget = BudgetSnapshot(
|
|
86
|
+
total_tool_calls=budget_data.get("total_tool_calls", 0),
|
|
87
|
+
total_tokens=budget_data.get("total_tokens", 0),
|
|
88
|
+
elapsed_seconds=budget_data.get("elapsed_seconds", 0.0),
|
|
89
|
+
by_effect=_decode_budget_dims(budget_data.get("by_effect", ())),
|
|
90
|
+
by_capability=_decode_budget_dims(budget_data.get("by_capability", ())),
|
|
91
|
+
by_scope=_decode_budget_dims(budget_data.get("by_scope", ())),
|
|
92
|
+
by_role=_decode_budget_dims(budget_data.get("by_role", ())),
|
|
93
|
+
by_phase=_decode_budget_dims(budget_data.get("by_phase", ())),
|
|
94
|
+
by_mechanism=_decode_budget_dims(budget_data.get("by_mechanism", ())),
|
|
95
|
+
by_action=_decode_budget_dims(budget_data.get("by_action", ())),
|
|
96
|
+
by_structure=_decode_budget_dims(budget_data.get("by_structure", ())),
|
|
97
|
+
pressure=budget_data.get("pressure", False),
|
|
98
|
+
)
|
|
99
|
+
taints = tuple(
|
|
100
|
+
TaintEntry(
|
|
101
|
+
event_id=t["event_id"],
|
|
102
|
+
source_event_key=t.get("source_event_key") or t["event_id"],
|
|
103
|
+
clearance=t["clearance"],
|
|
104
|
+
source=t["source"],
|
|
105
|
+
payload_pointer=t["payload_pointer"],
|
|
106
|
+
)
|
|
107
|
+
for t in data.get("taint_ledger", ())
|
|
108
|
+
)
|
|
109
|
+
return SessionStateSnapshot(
|
|
110
|
+
budget=budget,
|
|
111
|
+
phase_window=tuple(data.get("phase_window", ())),
|
|
112
|
+
taint_ledger=taints,
|
|
113
|
+
last_assistant_event_id=data.get("last_assistant_event_id"),
|
|
114
|
+
last_user_event_id=data.get("last_user_event_id"),
|
|
115
|
+
event_count=data.get("event_count", 0),
|
|
116
|
+
dropped_events=data.get("dropped_events", 0),
|
|
117
|
+
last_sequence=data.get("last_sequence"),
|
|
118
|
+
gap_ordinal=data.get("gap_ordinal", 0),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def deserialize_escalation(self, data: dict | None) -> "EscalationContext | None":
|
|
122
|
+
"""Reconstruct EscalationContext from cached evidence data."""
|
|
123
|
+
if not data:
|
|
124
|
+
return None
|
|
125
|
+
from traceforge.classify.core import Classification
|
|
126
|
+
|
|
127
|
+
cls = (
|
|
128
|
+
Classification.from_dict(data["classification"]) if data.get("classification") else None
|
|
129
|
+
)
|
|
130
|
+
action_val = data.get("recommended_action", "allow")
|
|
131
|
+
action = (
|
|
132
|
+
RecommendedAction(action_val)
|
|
133
|
+
if action_val in tuple(RecommendedAction)
|
|
134
|
+
else RecommendedAction.ALLOW
|
|
135
|
+
)
|
|
136
|
+
return EscalationContext(
|
|
137
|
+
canonical_id=data.get("canonical_id", ""),
|
|
138
|
+
classification=cls,
|
|
139
|
+
recommended_action=action,
|
|
140
|
+
reason_code=data.get("reason_code", ""),
|
|
141
|
+
mitre_techniques=tuple(data.get("mitre_techniques", ())),
|
|
142
|
+
drift=None, # Drift is session-contextual, not cached
|
|
143
|
+
budget_snapshot=None,
|
|
144
|
+
pii_taint=data.get("pii_taint", False),
|
|
145
|
+
ifc_violations=data.get("ifc_violations", 0),
|
|
146
|
+
tool_name=data.get("tool_name", ""),
|
|
147
|
+
tool_args_summary=data.get("tool_args_summary", ""),
|
|
148
|
+
session_id=data.get("session_id", ""),
|
|
149
|
+
timestamp=datetime.fromisoformat(data["timestamp"])
|
|
150
|
+
if data.get("timestamp")
|
|
151
|
+
else datetime.now(timezone.utc),
|
|
152
|
+
event_id=data.get("event_id", ""),
|
|
153
|
+
classification_summary=data.get("classification_summary", ""),
|
|
154
|
+
risk_factors=tuple(data.get("risk_factors", ())),
|
|
155
|
+
session_event_count=data.get("session_event_count", 0),
|
|
156
|
+
recent_phase_window=tuple(data.get("recent_phase_window", ())),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def serialize_meta(self, meta: SessionMeta) -> dict:
|
|
160
|
+
"""Full serialization for idempotency cache — preserves all governance decisions."""
|
|
161
|
+
rec_data = None
|
|
162
|
+
if meta.recommendation:
|
|
163
|
+
transform_data = None
|
|
164
|
+
if meta.recommendation.transform:
|
|
165
|
+
transform_data = {
|
|
166
|
+
"target_kind": meta.recommendation.transform.target_kind,
|
|
167
|
+
"path": meta.recommendation.transform.path,
|
|
168
|
+
"original": meta.recommendation.transform.original,
|
|
169
|
+
"replacement": meta.recommendation.transform.replacement,
|
|
170
|
+
"rationale": meta.recommendation.transform.rationale,
|
|
171
|
+
"confidence": meta.recommendation.transform.confidence,
|
|
172
|
+
}
|
|
173
|
+
rec_data = {
|
|
174
|
+
"action": str(meta.recommendation.recommended_action),
|
|
175
|
+
"reason_code": meta.recommendation.reason_code,
|
|
176
|
+
"canonical_id": meta.recommendation.canonical_id,
|
|
177
|
+
"message": meta.recommendation.message,
|
|
178
|
+
"transform": transform_data,
|
|
179
|
+
}
|
|
180
|
+
risk_data = None
|
|
181
|
+
if meta.risk_assessment:
|
|
182
|
+
risk_data = {
|
|
183
|
+
"score": meta.risk_assessment.score,
|
|
184
|
+
"level": meta.risk_assessment.level,
|
|
185
|
+
"confidence": meta.risk_assessment.confidence,
|
|
186
|
+
"factors": list(meta.risk_assessment.factors),
|
|
187
|
+
"mitre": list(meta.risk_assessment.mitre),
|
|
188
|
+
}
|
|
189
|
+
cls_data = None
|
|
190
|
+
if meta.classification:
|
|
191
|
+
cls_data = meta.classification.to_dict()
|
|
192
|
+
budget_data = None
|
|
193
|
+
if meta.budget_snapshot:
|
|
194
|
+
budget_data = {
|
|
195
|
+
"total_tool_calls": meta.budget_snapshot.total_tool_calls,
|
|
196
|
+
"total_tokens": meta.budget_snapshot.total_tokens,
|
|
197
|
+
"elapsed_seconds": meta.budget_snapshot.elapsed_seconds,
|
|
198
|
+
"pressure": meta.budget_snapshot.pressure,
|
|
199
|
+
"by_effect": list(meta.budget_snapshot.by_effect),
|
|
200
|
+
"by_capability": list(meta.budget_snapshot.by_capability),
|
|
201
|
+
"by_scope": list(meta.budget_snapshot.by_scope),
|
|
202
|
+
"by_role": list(meta.budget_snapshot.by_role),
|
|
203
|
+
"by_phase": list(meta.budget_snapshot.by_phase),
|
|
204
|
+
"by_mechanism": list(meta.budget_snapshot.by_mechanism),
|
|
205
|
+
"by_action": list(meta.budget_snapshot.by_action),
|
|
206
|
+
"by_structure": list(meta.budget_snapshot.by_structure),
|
|
207
|
+
}
|
|
208
|
+
evidence_data = None
|
|
209
|
+
if meta.evidence:
|
|
210
|
+
pointers_data = [
|
|
211
|
+
{
|
|
212
|
+
"event_id": p.event_id,
|
|
213
|
+
"rule_id": p.rule_id,
|
|
214
|
+
"detector": p.detector,
|
|
215
|
+
"payload_pointer": p.payload_pointer,
|
|
216
|
+
}
|
|
217
|
+
for p in meta.evidence.pointers
|
|
218
|
+
]
|
|
219
|
+
evidence_data = {
|
|
220
|
+
"canonical_id": meta.evidence.canonical_id,
|
|
221
|
+
"timestamp": meta.evidence.timestamp.isoformat(),
|
|
222
|
+
"session_id": meta.evidence.session_id,
|
|
223
|
+
"mechanism": meta.evidence.mechanism,
|
|
224
|
+
"effect": meta.evidence.effect,
|
|
225
|
+
"scope": list(meta.evidence.scope),
|
|
226
|
+
"role": list(meta.evidence.role),
|
|
227
|
+
"action": list(meta.evidence.action),
|
|
228
|
+
"capability": list(meta.evidence.capability),
|
|
229
|
+
"structure": list(meta.evidence.structure),
|
|
230
|
+
"source_labels": list(meta.evidence.source_labels),
|
|
231
|
+
"recommended_action": str(meta.evidence.recommended_action),
|
|
232
|
+
"risk_score": meta.evidence.risk_score,
|
|
233
|
+
"risk_factors": list(meta.evidence.risk_factors),
|
|
234
|
+
"mitre_techniques": list(meta.evidence.mitre_techniques),
|
|
235
|
+
"rule_id": meta.evidence.rule_id,
|
|
236
|
+
"matched_predicates": list(meta.evidence.matched_predicates),
|
|
237
|
+
"pointers": pointers_data,
|
|
238
|
+
}
|
|
239
|
+
if meta.evidence.escalation:
|
|
240
|
+
esc = meta.evidence.escalation
|
|
241
|
+
evidence_data["escalation"] = {
|
|
242
|
+
"canonical_id": esc.canonical_id,
|
|
243
|
+
"classification": esc.classification.to_dict() if esc.classification else None,
|
|
244
|
+
"recommended_action": str(esc.recommended_action),
|
|
245
|
+
"reason_code": esc.reason_code,
|
|
246
|
+
"mitre_techniques": list(esc.mitre_techniques),
|
|
247
|
+
"pii_taint": esc.pii_taint,
|
|
248
|
+
"ifc_violations": esc.ifc_violations,
|
|
249
|
+
"tool_name": esc.tool_name,
|
|
250
|
+
"tool_args_summary": esc.tool_args_summary,
|
|
251
|
+
"session_id": esc.session_id,
|
|
252
|
+
"timestamp": esc.timestamp.isoformat(),
|
|
253
|
+
"event_id": esc.event_id,
|
|
254
|
+
"classification_summary": esc.classification_summary,
|
|
255
|
+
"risk_factors": list(esc.risk_factors),
|
|
256
|
+
"session_event_count": esc.session_event_count,
|
|
257
|
+
"recent_phase_window": list(esc.recent_phase_window),
|
|
258
|
+
}
|
|
259
|
+
mcp_alerts_data = []
|
|
260
|
+
if meta.mcp_alerts:
|
|
261
|
+
for alert in meta.mcp_alerts:
|
|
262
|
+
mcp_alerts_data.append(
|
|
263
|
+
{
|
|
264
|
+
"tool_name": alert.tool_name,
|
|
265
|
+
"server": alert.server,
|
|
266
|
+
"alert_type": alert.alert_type,
|
|
267
|
+
"previous": alert.previous,
|
|
268
|
+
"current": alert.current,
|
|
269
|
+
"severity": alert.severity,
|
|
270
|
+
"timestamp": alert.timestamp.isoformat(),
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
return {
|
|
274
|
+
"classification": cls_data,
|
|
275
|
+
"recommendation": rec_data,
|
|
276
|
+
"risk": risk_data,
|
|
277
|
+
"budget": budget_data,
|
|
278
|
+
"evidence": evidence_data,
|
|
279
|
+
"mcp_alerts": mcp_alerts_data,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def deserialize_meta(self, data: dict) -> SessionMeta:
|
|
283
|
+
"""Reconstruct SessionMeta from cache — full fidelity for idempotent output."""
|
|
284
|
+
from traceforge.classify.core import Classification
|
|
285
|
+
from traceforge.classify.risk import RiskAssessment
|
|
286
|
+
from traceforge.governance.state import BudgetSnapshot
|
|
287
|
+
|
|
288
|
+
risk = None
|
|
289
|
+
risk_data = data.get("risk")
|
|
290
|
+
if risk_data:
|
|
291
|
+
risk = RiskAssessment(
|
|
292
|
+
score=risk_data.get("score", 0),
|
|
293
|
+
level=risk_data.get("level", "safe"),
|
|
294
|
+
confidence=risk_data.get("confidence", "medium"),
|
|
295
|
+
factors=tuple(risk_data.get("factors", ())),
|
|
296
|
+
mitre=tuple(risk_data.get("mitre", ())),
|
|
297
|
+
version="cached",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
rec = None
|
|
301
|
+
rec_data = data.get("recommendation")
|
|
302
|
+
if rec_data and rec_data.get("action"):
|
|
303
|
+
transform = None
|
|
304
|
+
transform_data = rec_data.get("transform")
|
|
305
|
+
if transform_data:
|
|
306
|
+
transform = TransformSuggestion(
|
|
307
|
+
target_kind=transform_data["target_kind"],
|
|
308
|
+
path=transform_data["path"],
|
|
309
|
+
original=transform_data["original"],
|
|
310
|
+
replacement=transform_data.get("replacement"),
|
|
311
|
+
rationale=transform_data.get("rationale", ""),
|
|
312
|
+
confidence=transform_data.get("confidence", "medium"),
|
|
313
|
+
)
|
|
314
|
+
# Only construct recommendation if risk is available (type contract)
|
|
315
|
+
if risk is not None:
|
|
316
|
+
rec = RiskRecommendation(
|
|
317
|
+
recommended_action=RecommendedAction(rec_data["action"]),
|
|
318
|
+
assessment=risk,
|
|
319
|
+
reason_code=rec_data.get("reason_code", ""),
|
|
320
|
+
canonical_id=rec_data.get("canonical_id") or "",
|
|
321
|
+
message=rec_data.get("message"),
|
|
322
|
+
transform=transform,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
cls = None
|
|
326
|
+
cls_data = data.get("classification")
|
|
327
|
+
if cls_data:
|
|
328
|
+
cls = Classification.from_dict(cls_data)
|
|
329
|
+
|
|
330
|
+
budget = None
|
|
331
|
+
budget_data = data.get("budget")
|
|
332
|
+
if budget_data:
|
|
333
|
+
budget = BudgetSnapshot(
|
|
334
|
+
total_tool_calls=budget_data.get("total_tool_calls", 0),
|
|
335
|
+
total_tokens=budget_data.get("total_tokens", 0),
|
|
336
|
+
elapsed_seconds=budget_data.get("elapsed_seconds", 0.0),
|
|
337
|
+
by_effect=_decode_budget_dims(budget_data.get("by_effect", ())),
|
|
338
|
+
by_capability=_decode_budget_dims(budget_data.get("by_capability", ())),
|
|
339
|
+
by_scope=_decode_budget_dims(budget_data.get("by_scope", ())),
|
|
340
|
+
by_role=_decode_budget_dims(budget_data.get("by_role", ())),
|
|
341
|
+
by_phase=_decode_budget_dims(budget_data.get("by_phase", ())),
|
|
342
|
+
by_mechanism=_decode_budget_dims(budget_data.get("by_mechanism", ())),
|
|
343
|
+
by_action=_decode_budget_dims(budget_data.get("by_action", ())),
|
|
344
|
+
by_structure=_decode_budget_dims(budget_data.get("by_structure", ())),
|
|
345
|
+
pressure=budget_data.get("pressure", False),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
evidence = None
|
|
349
|
+
evidence_data = data.get("evidence")
|
|
350
|
+
if evidence_data:
|
|
351
|
+
from datetime import datetime as dt_cls
|
|
352
|
+
|
|
353
|
+
pointers = tuple(
|
|
354
|
+
EvidencePointer(
|
|
355
|
+
event_id=p["event_id"],
|
|
356
|
+
rule_id=p["rule_id"],
|
|
357
|
+
detector=p["detector"],
|
|
358
|
+
payload_pointer=p.get("payload_pointer"),
|
|
359
|
+
)
|
|
360
|
+
for p in evidence_data.get("pointers", ())
|
|
361
|
+
)
|
|
362
|
+
evidence = Evidence(
|
|
363
|
+
canonical_id=evidence_data.get("canonical_id", ""),
|
|
364
|
+
timestamp=dt_cls.fromisoformat(evidence_data["timestamp"])
|
|
365
|
+
if evidence_data.get("timestamp")
|
|
366
|
+
else datetime.now(timezone.utc),
|
|
367
|
+
session_id=evidence_data.get("session_id", ""),
|
|
368
|
+
mechanism=evidence_data.get("mechanism", ""),
|
|
369
|
+
effect=evidence_data.get("effect"),
|
|
370
|
+
scope=tuple(evidence_data.get("scope", ())),
|
|
371
|
+
role=tuple(evidence_data.get("role", ())),
|
|
372
|
+
action=tuple(evidence_data.get("action", ())),
|
|
373
|
+
capability=tuple(evidence_data.get("capability", ())),
|
|
374
|
+
structure=tuple(evidence_data.get("structure", ())),
|
|
375
|
+
source_labels=tuple(evidence_data.get("source_labels", ())),
|
|
376
|
+
recommended_action=RecommendedAction(evidence_data["recommended_action"])
|
|
377
|
+
if evidence_data.get("recommended_action") in tuple(RecommendedAction)
|
|
378
|
+
else RecommendedAction.ALLOW,
|
|
379
|
+
risk_score=evidence_data.get("risk_score", 0),
|
|
380
|
+
risk_factors=tuple(evidence_data.get("risk_factors", ())),
|
|
381
|
+
mitre_techniques=tuple(evidence_data.get("mitre_techniques", ())),
|
|
382
|
+
pointers=pointers,
|
|
383
|
+
escalation=self.deserialize_escalation(evidence_data.get("escalation")),
|
|
384
|
+
rule_id=evidence_data.get("rule_id", ""),
|
|
385
|
+
matched_predicates=tuple(evidence_data.get("matched_predicates", ())),
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
from traceforge.governance.mcp_drift import MCPIntegrityAlert
|
|
389
|
+
|
|
390
|
+
mcp_alerts_raw = data.get("mcp_alerts", ())
|
|
391
|
+
mcp_alerts: tuple = tuple(
|
|
392
|
+
MCPIntegrityAlert(
|
|
393
|
+
tool_name=a.get("tool_name", ""),
|
|
394
|
+
server=a.get("server", ""),
|
|
395
|
+
alert_type=a.get("alert_type", "schema_change"),
|
|
396
|
+
previous=a.get("previous", ""),
|
|
397
|
+
current=a.get("current", ""),
|
|
398
|
+
severity=a.get("severity", "info"),
|
|
399
|
+
timestamp=datetime.fromisoformat(a["timestamp"])
|
|
400
|
+
if a.get("timestamp")
|
|
401
|
+
else datetime.now(timezone.utc),
|
|
402
|
+
)
|
|
403
|
+
for a in mcp_alerts_raw
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return SessionMeta(
|
|
407
|
+
classification=cls,
|
|
408
|
+
risk_assessment=risk,
|
|
409
|
+
recommendation=rec,
|
|
410
|
+
budget_snapshot=budget,
|
|
411
|
+
drift=None,
|
|
412
|
+
mcp_alerts=mcp_alerts,
|
|
413
|
+
evidence=evidence,
|
|
414
|
+
)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Translate raw event shapes into a governance EnrichmentContext.
|
|
2
|
+
|
|
3
|
+
ContextBuilder is the adapter seam between the outside world's event shapes
|
|
4
|
+
(SessionEvent from the observation pipeline; ToolCallEvent from the assess API)
|
|
5
|
+
and the internal EnrichmentContext the Assessor and monitor consume. It is a
|
|
6
|
+
pure translator: it holds only the classification engine and the project root,
|
|
7
|
+
mutates no session state, and never touches the store.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Literal
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import traceforge.types
|
|
16
|
+
|
|
17
|
+
from traceforge.classify.config import ClassificationEngine
|
|
18
|
+
from traceforge.governance.types import EnrichmentContext, ToolCallEvent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ContextBuilder:
|
|
22
|
+
"""Build an EnrichmentContext from raw SessionEvent / ToolCallEvent inputs."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, engine: "ClassificationEngine", project_root: str | None = None) -> None:
|
|
25
|
+
self._engine = engine
|
|
26
|
+
self._project_root = project_root
|
|
27
|
+
|
|
28
|
+
def from_session_event(self, event: "traceforge.types.SessionEvent") -> "EnrichmentContext":
|
|
29
|
+
"""Bridge: convert an enriched SessionEvent (from adapters/Enricher) into an EnrichmentContext.
|
|
30
|
+
|
|
31
|
+
This is the canonical path for observation pipeline events. The Enricher
|
|
32
|
+
has already classified the event and stored the result in event.metadata.
|
|
33
|
+
We extract that classification and build the governance context.
|
|
34
|
+
"""
|
|
35
|
+
from traceforge.governance.types import EnrichmentContext, ToolCallEvent
|
|
36
|
+
|
|
37
|
+
# Normalize metadata: events may arrive with metadata=None (e.g. pushed
|
|
38
|
+
# directly, bypassing the Enricher). Fall back to an empty EventMetadata so
|
|
39
|
+
# attribute access below never raises.
|
|
40
|
+
metadata = event.metadata
|
|
41
|
+
if metadata is None:
|
|
42
|
+
from traceforge.types import EventMetadata
|
|
43
|
+
|
|
44
|
+
metadata = EventMetadata()
|
|
45
|
+
|
|
46
|
+
# Extract classification (already computed by Enricher)
|
|
47
|
+
classification = metadata.classification
|
|
48
|
+
if classification is None:
|
|
49
|
+
from traceforge.classify.core import Classification, Mechanism
|
|
50
|
+
|
|
51
|
+
classification = Classification(mechanism=Mechanism.UNKNOWN, effect=None)
|
|
52
|
+
|
|
53
|
+
# Build governance ToolCallEvent from SessionEvent fields
|
|
54
|
+
tool_name = event.payload.get("tool_name", "")
|
|
55
|
+
arguments = event.payload.get("arguments", {})
|
|
56
|
+
server_namespace = event.payload.get("server_namespace")
|
|
57
|
+
|
|
58
|
+
import json as _json
|
|
59
|
+
|
|
60
|
+
tool_args_json = (
|
|
61
|
+
_json.dumps(arguments, default=str) if isinstance(arguments, dict) else str(arguments)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
gov_event = ToolCallEvent(
|
|
65
|
+
event_id=event.id,
|
|
66
|
+
session_id=event.session_id,
|
|
67
|
+
timestamp=event.timestamp,
|
|
68
|
+
source_event_key=event.id,
|
|
69
|
+
span_id=metadata.span_id or event.id,
|
|
70
|
+
tool_name=tool_name,
|
|
71
|
+
server_namespace=server_namespace,
|
|
72
|
+
tool_args_json=tool_args_json,
|
|
73
|
+
source_event_id=None,
|
|
74
|
+
mcp_server_name=event.payload.get("mcp_server_name") or server_namespace,
|
|
75
|
+
tool_description=event.payload.get("tool_description"),
|
|
76
|
+
tool_schema_json=event.payload.get("tool_schema_json"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Build command analysis for shell tools
|
|
80
|
+
command = ""
|
|
81
|
+
if isinstance(arguments, dict):
|
|
82
|
+
command = arguments.get("command", "") or arguments.get("cmd", "")
|
|
83
|
+
elif isinstance(arguments, str):
|
|
84
|
+
command = arguments
|
|
85
|
+
command_analysis = self._build_command_analysis(command) if command else None
|
|
86
|
+
|
|
87
|
+
# Derive engine literal
|
|
88
|
+
mech_str = (
|
|
89
|
+
classification.mechanism.value
|
|
90
|
+
if hasattr(classification.mechanism, "value")
|
|
91
|
+
else str(classification.mechanism)
|
|
92
|
+
).lower()
|
|
93
|
+
if "shell" in mech_str or "process" in mech_str:
|
|
94
|
+
engine_literal: Literal["shell", "mcp", "coding"] = "shell"
|
|
95
|
+
elif "mcp" in mech_str:
|
|
96
|
+
engine_literal = "mcp"
|
|
97
|
+
else:
|
|
98
|
+
engine_literal = "coding"
|
|
99
|
+
|
|
100
|
+
return EnrichmentContext(
|
|
101
|
+
event=gov_event,
|
|
102
|
+
base_classification=classification,
|
|
103
|
+
command_analysis=command_analysis,
|
|
104
|
+
session_state=None,
|
|
105
|
+
mcp_profiles=None,
|
|
106
|
+
project_root=self._project_root,
|
|
107
|
+
engine=engine_literal,
|
|
108
|
+
drift_baseline=None,
|
|
109
|
+
mcp_profile_key=server_namespace,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def from_tool_call(self, event: "ToolCallEvent") -> "EnrichmentContext":
|
|
113
|
+
"""Classify a governance ToolCallEvent and build its EnrichmentContext.
|
|
114
|
+
|
|
115
|
+
Used by the assess pathway (raw dict → ToolCallEvent.from_dict → here).
|
|
116
|
+
For observation pipeline events, use from_session_event instead.
|
|
117
|
+
"""
|
|
118
|
+
import json as _json
|
|
119
|
+
|
|
120
|
+
from traceforge.classify.tools import classify_tool, normalize_tool_name
|
|
121
|
+
from traceforge.governance.types import EnrichmentContext
|
|
122
|
+
|
|
123
|
+
tool_name = event.tool_name
|
|
124
|
+
server_namespace = event.server_namespace
|
|
125
|
+
|
|
126
|
+
# MCP namespace synthesis
|
|
127
|
+
classify_name = tool_name
|
|
128
|
+
if server_namespace and not tool_name.startswith("mcp__"):
|
|
129
|
+
prefix = f"{server_namespace}__"
|
|
130
|
+
base = tool_name[len(prefix) :] if tool_name.startswith(prefix) else tool_name
|
|
131
|
+
classify_name = f"mcp__{server_namespace}__{base}"
|
|
132
|
+
|
|
133
|
+
canonical = normalize_tool_name(classify_name, engine=self._engine)
|
|
134
|
+
is_shell = canonical == "shell"
|
|
135
|
+
|
|
136
|
+
if is_shell:
|
|
137
|
+
tool_input = _json.loads(event.tool_args_json) if event.tool_args_json else {}
|
|
138
|
+
command = (
|
|
139
|
+
tool_input.get("command", "") or tool_input.get("cmd", "")
|
|
140
|
+
if isinstance(tool_input, dict)
|
|
141
|
+
else ""
|
|
142
|
+
)
|
|
143
|
+
classification = self._classify_shell_for_assess(tool_name, command)
|
|
144
|
+
command_analysis = self._build_command_analysis(command) if command else None
|
|
145
|
+
else:
|
|
146
|
+
classification = classify_tool(classify_name, engine=self._engine)
|
|
147
|
+
command_analysis = None
|
|
148
|
+
|
|
149
|
+
# Derive engine literal
|
|
150
|
+
mech_str = (
|
|
151
|
+
classification.mechanism.value
|
|
152
|
+
if hasattr(classification.mechanism, "value")
|
|
153
|
+
else str(classification.mechanism)
|
|
154
|
+
).lower()
|
|
155
|
+
if "shell" in mech_str or "process" in mech_str:
|
|
156
|
+
engine_literal = "shell"
|
|
157
|
+
elif "mcp" in mech_str:
|
|
158
|
+
engine_literal = "mcp"
|
|
159
|
+
else:
|
|
160
|
+
engine_literal = "coding"
|
|
161
|
+
|
|
162
|
+
return EnrichmentContext(
|
|
163
|
+
event=event,
|
|
164
|
+
base_classification=classification,
|
|
165
|
+
command_analysis=command_analysis,
|
|
166
|
+
session_state=None,
|
|
167
|
+
mcp_profiles=None,
|
|
168
|
+
project_root=self._project_root,
|
|
169
|
+
engine=engine_literal,
|
|
170
|
+
drift_baseline=None,
|
|
171
|
+
mcp_profile_key=server_namespace,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _classify_shell_for_assess(self, tool_name: str, command: str):
|
|
175
|
+
"""Shell dialect dispatch for assessment."""
|
|
176
|
+
from traceforge.classify.cmd import classify_cmd_command
|
|
177
|
+
from traceforge.classify.coding import CodingMechanism
|
|
178
|
+
from traceforge.classify.core import Classification
|
|
179
|
+
from traceforge.classify.powershell import classify_powershell_command
|
|
180
|
+
from traceforge.classify.shell import classify_shell
|
|
181
|
+
|
|
182
|
+
if not command:
|
|
183
|
+
return Classification(mechanism=CodingMechanism.PROCESS_SHELL, effect=None)
|
|
184
|
+
lower = tool_name.lower()
|
|
185
|
+
if lower in ("powershell", "pwsh"):
|
|
186
|
+
return classify_powershell_command(command, engine=self._engine)
|
|
187
|
+
if lower == "cmd":
|
|
188
|
+
return classify_cmd_command(command, engine=self._engine)
|
|
189
|
+
return classify_shell(command, engine=self._engine)
|
|
190
|
+
|
|
191
|
+
def _build_command_analysis(self, command: str):
|
|
192
|
+
"""Build CommandAnalysis using the shell classifier's unwrap logic."""
|
|
193
|
+
import shlex
|
|
194
|
+
|
|
195
|
+
from traceforge.classify.shell import _unwrap_binary
|
|
196
|
+
from traceforge.governance.types import CommandAnalysis, PipeSegment
|
|
197
|
+
|
|
198
|
+
if not command or not command.strip():
|
|
199
|
+
return None
|
|
200
|
+
try:
|
|
201
|
+
tokens = shlex.split(command)
|
|
202
|
+
except ValueError:
|
|
203
|
+
tokens = command.split()
|
|
204
|
+
if not tokens:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
binary, _subcmd, flags, _caps = _unwrap_binary(tokens, engine=self._engine)
|
|
208
|
+
targets = tuple(t for t in tokens[1:] if not t.startswith("-") and t != binary)
|
|
209
|
+
|
|
210
|
+
pipe_segments = None
|
|
211
|
+
if "|" in command and "||" not in command and "|&" not in command:
|
|
212
|
+
try:
|
|
213
|
+
lexer = shlex.shlex(command, posix=True, punctuation_chars=True)
|
|
214
|
+
lexer.whitespace_split = False
|
|
215
|
+
all_tokens = list(lexer)
|
|
216
|
+
if "|" in all_tokens:
|
|
217
|
+
segments: list[PipeSegment] = []
|
|
218
|
+
current: list[str] = []
|
|
219
|
+
for tok in all_tokens:
|
|
220
|
+
if tok == "|":
|
|
221
|
+
if current:
|
|
222
|
+
b, _s, f, _c = _unwrap_binary(current, engine=self._engine)
|
|
223
|
+
t = tuple(
|
|
224
|
+
x for x in current[1:] if not x.startswith("-") and x != b
|
|
225
|
+
)
|
|
226
|
+
segments.append(
|
|
227
|
+
PipeSegment(binary=b or current[0], flags=tuple(f), targets=t)
|
|
228
|
+
)
|
|
229
|
+
current = []
|
|
230
|
+
else:
|
|
231
|
+
current.append(tok)
|
|
232
|
+
if current:
|
|
233
|
+
b, _s, f, _c = _unwrap_binary(current, engine=self._engine)
|
|
234
|
+
t = tuple(x for x in current[1:] if not x.startswith("-") and x != b)
|
|
235
|
+
segments.append(
|
|
236
|
+
PipeSegment(binary=b or current[0], flags=tuple(f), targets=t)
|
|
237
|
+
)
|
|
238
|
+
if len(segments) > 1:
|
|
239
|
+
pipe_segments = tuple(segments)
|
|
240
|
+
except ValueError:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
return CommandAnalysis(
|
|
244
|
+
command=command,
|
|
245
|
+
binary=binary or tokens[0],
|
|
246
|
+
flags=tuple(flags),
|
|
247
|
+
targets=targets,
|
|
248
|
+
pipe_segments=pipe_segments,
|
|
249
|
+
)
|