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.
Files changed (205) hide show
  1. traceforge/__init__.py +72 -0
  2. traceforge/__main__.py +5 -0
  3. traceforge/_generated.py +254 -0
  4. traceforge/adapters/__init__.py +12 -0
  5. traceforge/adapters/base.py +82 -0
  6. traceforge/adapters/genai_otel.py +164 -0
  7. traceforge/adapters/mapped_json.py +297 -0
  8. traceforge/adapters/otel.py +220 -0
  9. traceforge/boundary/__init__.py +46 -0
  10. traceforge/boundary/data/boundary-model.joblib +0 -0
  11. traceforge/boundary/decode.py +87 -0
  12. traceforge/boundary/features.py +137 -0
  13. traceforge/boundary/inference.py +267 -0
  14. traceforge/boundary/inferencer.py +146 -0
  15. traceforge/classify/__init__.py +95 -0
  16. traceforge/classify/cmd.py +109 -0
  17. traceforge/classify/coding.py +213 -0
  18. traceforge/classify/config.py +554 -0
  19. traceforge/classify/core.py +266 -0
  20. traceforge/classify/data/binary_info.yaml +358 -0
  21. traceforge/classify/data/canonical_tools.yaml +251 -0
  22. traceforge/classify/data/effect_overrides.yaml +480 -0
  23. traceforge/classify/data/mcp_profiles.yaml +711 -0
  24. traceforge/classify/data/recommendation_rules.yaml +111 -0
  25. traceforge/classify/data/risk.yaml +534 -0
  26. traceforge/classify/data/shell_defaults.yaml +95 -0
  27. traceforge/classify/data/shell_rules.yaml +1192 -0
  28. traceforge/classify/data/tool_classifications.yaml +215 -0
  29. traceforge/classify/data/verb_inference.yaml +218 -0
  30. traceforge/classify/mcp.py +181 -0
  31. traceforge/classify/phases.py +75 -0
  32. traceforge/classify/powershell.py +93 -0
  33. traceforge/classify/registry.py +138 -0
  34. traceforge/classify/risk.py +553 -0
  35. traceforge/classify/rules.py +215 -0
  36. traceforge/classify/schema.yaml +282 -0
  37. traceforge/classify/shell.py +503 -0
  38. traceforge/classify/tools.py +91 -0
  39. traceforge/classify/workflow.py +32 -0
  40. traceforge/cli/__init__.py +42 -0
  41. traceforge/cli/config_cmd.py +130 -0
  42. traceforge/cli/detect.py +46 -0
  43. traceforge/cli/download_cmd.py +74 -0
  44. traceforge/cli/factory.py +60 -0
  45. traceforge/cli/gate_cmd.py +30 -0
  46. traceforge/cli/init_cmd.py +86 -0
  47. traceforge/cli/replay.py +130 -0
  48. traceforge/cli/runner.py +181 -0
  49. traceforge/cli/score.py +217 -0
  50. traceforge/cli/status.py +65 -0
  51. traceforge/cli/watch.py +297 -0
  52. traceforge/config/__init__.py +80 -0
  53. traceforge/config/defaults.py +149 -0
  54. traceforge/config/loader.py +239 -0
  55. traceforge/config/mappings.py +104 -0
  56. traceforge/config/models.py +579 -0
  57. traceforge/enricher.py +576 -0
  58. traceforge/formatting/__init__.py +12 -0
  59. traceforge/formatting/budget.py +92 -0
  60. traceforge/formatting/density.py +154 -0
  61. traceforge/gate/__init__.py +17 -0
  62. traceforge/gate/client.py +145 -0
  63. traceforge/gate/external.py +305 -0
  64. traceforge/gate/registry.py +108 -0
  65. traceforge/gate/server.py +174 -0
  66. traceforge/gates/__init__.py +8 -0
  67. traceforge/gates/pii.py +352 -0
  68. traceforge/gates/pii_patterns.yaml +228 -0
  69. traceforge/governance/__init__.py +121 -0
  70. traceforge/governance/assessor.py +441 -0
  71. traceforge/governance/budget.py +59 -0
  72. traceforge/governance/canonical.py +56 -0
  73. traceforge/governance/codec.py +414 -0
  74. traceforge/governance/context.py +249 -0
  75. traceforge/governance/drift.py +196 -0
  76. traceforge/governance/emitter.py +234 -0
  77. traceforge/governance/envelope.py +138 -0
  78. traceforge/governance/ifc.py +364 -0
  79. traceforge/governance/integrity.py +162 -0
  80. traceforge/governance/labeler.py +250 -0
  81. traceforge/governance/mcp_drift.py +328 -0
  82. traceforge/governance/monitor.py +539 -0
  83. traceforge/governance/observer.py +276 -0
  84. traceforge/governance/persistence.py +401 -0
  85. traceforge/governance/phase1.py +77 -0
  86. traceforge/governance/pii.py +276 -0
  87. traceforge/governance/pipeline.py +840 -0
  88. traceforge/governance/registry.py +110 -0
  89. traceforge/governance/results.py +183 -0
  90. traceforge/governance/risk_wrapper.py +113 -0
  91. traceforge/governance/rules.py +368 -0
  92. traceforge/governance/scorer.py +262 -0
  93. traceforge/governance/shield.py +318 -0
  94. traceforge/governance/state.py +466 -0
  95. traceforge/governance/types.py +176 -0
  96. traceforge/mappings/__init__.py +1 -0
  97. traceforge/mappings/aider.yaml +216 -0
  98. traceforge/mappings/aider_markdown.yaml +113 -0
  99. traceforge/mappings/amazonq.yaml +56 -0
  100. traceforge/mappings/antigravity.yaml +88 -0
  101. traceforge/mappings/claude.yaml +93 -0
  102. traceforge/mappings/cline.yaml +286 -0
  103. traceforge/mappings/codex.yaml +158 -0
  104. traceforge/mappings/continue_dev.yaml +57 -0
  105. traceforge/mappings/copilot.yaml +266 -0
  106. traceforge/mappings/copilot_markdown.yaml +72 -0
  107. traceforge/mappings/copilot_vscode.yaml +181 -0
  108. traceforge/mappings/crewai.yaml +592 -0
  109. traceforge/mappings/goose.yaml +128 -0
  110. traceforge/mappings/langgraph.yaml +165 -0
  111. traceforge/mappings/maf.yaml +90 -0
  112. traceforge/mappings/maf_transcript.yaml +99 -0
  113. traceforge/mappings/openai_agents.yaml +144 -0
  114. traceforge/mappings/opencode.yaml +473 -0
  115. traceforge/mappings/openhands.yaml +320 -0
  116. traceforge/mappings/pydantic_ai.yaml +183 -0
  117. traceforge/mappings/smolagents.yaml +89 -0
  118. traceforge/mappings/sweagent.yaml +82 -0
  119. traceforge/migrations/__init__.py +1 -0
  120. traceforge/migrations/env.py +60 -0
  121. traceforge/migrations/models.py +145 -0
  122. traceforge/migrations/runner.py +44 -0
  123. traceforge/migrations/script.py.mako +25 -0
  124. traceforge/migrations/versions/0001_initial.py +176 -0
  125. traceforge/migrations/versions/__init__.py +4 -0
  126. traceforge/models.py +29 -0
  127. traceforge/parsers/__init__.py +21 -0
  128. traceforge/parsers/aider.py +416 -0
  129. traceforge/parsers/base.py +226 -0
  130. traceforge/parsers/copilot.py +314 -0
  131. traceforge/phase/__init__.py +50 -0
  132. traceforge/phase/data/phase-model.joblib +0 -0
  133. traceforge/phase/data/potion-base-8M/README.md +99 -0
  134. traceforge/phase/data/potion-base-8M/config.json +13 -0
  135. traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
  136. traceforge/phase/data/potion-base-8M/modules.json +14 -0
  137. traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
  138. traceforge/phase/event_rows.py +107 -0
  139. traceforge/phase/features.py +468 -0
  140. traceforge/phase/inference.py +279 -0
  141. traceforge/phase/inferencer.py +171 -0
  142. traceforge/phase/segmentation.py +258 -0
  143. traceforge/pipeline.py +891 -0
  144. traceforge/preprocessors/__init__.py +34 -0
  145. traceforge/preprocessors/amazonq.py +224 -0
  146. traceforge/preprocessors/antigravity.py +116 -0
  147. traceforge/preprocessors/claude.py +95 -0
  148. traceforge/preprocessors/cline.py +36 -0
  149. traceforge/preprocessors/codex.py +311 -0
  150. traceforge/preprocessors/continue_dev.py +119 -0
  151. traceforge/preprocessors/copilot_vscode.py +171 -0
  152. traceforge/preprocessors/goose.py +156 -0
  153. traceforge/preprocessors/maf_transcript.py +84 -0
  154. traceforge/preprocessors/openai_agents.py +86 -0
  155. traceforge/preprocessors/opencode.py +85 -0
  156. traceforge/preprocessors/openhands.py +36 -0
  157. traceforge/preprocessors/pydantic_ai.py +62 -0
  158. traceforge/preprocessors/registry.py +24 -0
  159. traceforge/preprocessors/smolagents.py +90 -0
  160. traceforge/py.typed +0 -0
  161. traceforge/sdk/__init__.py +59 -0
  162. traceforge/sdk/gate_policy.py +63 -0
  163. traceforge/sdk/gate_types.py +140 -0
  164. traceforge/sdk/pipeline.py +265 -0
  165. traceforge/sdk/verdict.py +81 -0
  166. traceforge/sinks/__init__.py +23 -0
  167. traceforge/sinks/base.py +95 -0
  168. traceforge/sinks/callback.py +132 -0
  169. traceforge/sinks/console.py +112 -0
  170. traceforge/sinks/factory.py +94 -0
  171. traceforge/sinks/jsonl.py +125 -0
  172. traceforge/sinks/otel_exporter.py +212 -0
  173. traceforge/sinks/parquet.py +260 -0
  174. traceforge/sinks/s3.py +206 -0
  175. traceforge/sinks/sqlite_output.py +234 -0
  176. traceforge/sinks/webhook.py +136 -0
  177. traceforge/sources/__init__.py +18 -0
  178. traceforge/sources/auto_detect.py +173 -0
  179. traceforge/sources/base.py +45 -0
  180. traceforge/sources/file_poll.py +136 -0
  181. traceforge/sources/file_watch.py +221 -0
  182. traceforge/sources/http_poll.py +141 -0
  183. traceforge/sources/replay.py +63 -0
  184. traceforge/sources/sqlite.py +187 -0
  185. traceforge/sources/sse.py +198 -0
  186. traceforge/telemetry/__init__.py +192 -0
  187. traceforge/title/__init__.py +23 -0
  188. traceforge/title/_resolve.py +79 -0
  189. traceforge/title/context.py +295 -0
  190. traceforge/title/data/boilerplate_files.json +14 -0
  191. traceforge/title/heuristics.py +429 -0
  192. traceforge/title/hygiene.py +90 -0
  193. traceforge/title/inference.py +314 -0
  194. traceforge/title/inferencer.py +477 -0
  195. traceforge/title/naming.py +398 -0
  196. traceforge/trace.py +291 -0
  197. traceforge/tracking/__init__.py +30 -0
  198. traceforge/tracking/models.py +115 -0
  199. traceforge/tracking/phase_tracker.py +288 -0
  200. traceforge/types.py +315 -0
  201. traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
  202. traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
  203. traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
  204. traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  205. traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,318 @@
1
+ """Runtime enforcement — the Shield.
2
+
3
+ Where the :class:`~traceforge.governance.assessor.Assessor` *assesses* and the monitor
4
+ *observes*, the :class:`Shield` *enforces*. It is the pre-execution checkpoint on the
5
+ gate channel: a tool call reaches the shield's preflight chain BEFORE it executes, and
6
+ only calls the shield allows go on to run. The downstream trace therefore only ever
7
+ observes what the gate allowed — which is why the tool-call counter advances here, at
8
+ the single post-execution observation point (:meth:`_observe_completed_call`), and the
9
+ preflight chain only ever *reads* it.
10
+
11
+ Collaborators are injected (DIP): a :class:`SessionRegistry` for per-session gate state,
12
+ a ``scorer`` that turns a raw payload into an assessed ``EventTrace``, and a
13
+ ``policy_provider`` that yields the current :class:`GatePolicy` (kept live so callers may
14
+ swap the policy after construction).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Callable
20
+
21
+ if TYPE_CHECKING:
22
+ from traceforge.governance.registry import SessionRegistry
23
+ from traceforge.sdk.gate_policy import GatePolicy
24
+ from traceforge.sdk.gate_types import (
25
+ GateContext,
26
+ PostflightVerdict,
27
+ ToolCallRequest,
28
+ ToolCallResult,
29
+ )
30
+ from traceforge.sdk.verdict import Verdict
31
+ from traceforge.trace import EventTrace
32
+
33
+
34
+ class Shield:
35
+ """Runtime enforcement checkpoint — preflight gating + postflight action.
36
+
37
+ Single responsibility: given an assessed call, decide ALLOW/DENY before execution
38
+ and the most-restrictive post-execution action, failing closed on any error. It
39
+ holds no assessment or persistence logic; it reads gate state through the registry
40
+ and advances the tool-call counter only when an allowed call completes.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ registry: "SessionRegistry",
46
+ policy_provider: "Callable[[], GatePolicy | None]",
47
+ scorer: "Callable[[dict], EventTrace]",
48
+ ) -> None:
49
+ import threading
50
+
51
+ self._registry = registry
52
+ self._policy_provider = policy_provider
53
+ self._scorer = scorer
54
+ self._lock = threading.Lock()
55
+
56
+ # ── Orchestration (called by framework adapters) ───────────────────────────
57
+
58
+ def score_and_gate_preflight(self, payload: dict) -> tuple:
59
+ """Score a tool call and run the preflight gate chain. Thread-safe.
60
+
61
+ Returns (trace, verdict) tuple. Verdict is ALLOW or DENY.
62
+ Session ID is derived from the trace (which normalizes from payload).
63
+ """
64
+ with self._lock:
65
+ trace = self._scorer(payload)
66
+ # Use trace.session_id for consistency — it normalizes empty/missing values
67
+ session_id = trace.session_id
68
+ verdict = self.run_preflight(trace, session_id=session_id)
69
+ return trace, verdict
70
+
71
+ def enforce_postflight(
72
+ self,
73
+ trace: "EventTrace",
74
+ *,
75
+ session_id: str,
76
+ output: dict | None = None,
77
+ duration_ms: int | None = None,
78
+ error: str | None = None,
79
+ ) -> "PostflightVerdict":
80
+ """Observe the completed call, then run the postflight chain.
81
+
82
+ This is the post-execution point: the call the shield allowed has now
83
+ run, so the trace observes it here — the single place the tool-call
84
+ counter advances in the gate channel. Then callers check verdict.action:
85
+ - ACCEPT: return result normally
86
+ - REDACT: strip keys from output before returning
87
+ - SUPPRESS: return empty/neutral output to the model
88
+ - TERMINATE: raise/signal session termination
89
+ - ALERT: return result normally but log alert
90
+ """
91
+ self._observe_completed_call(trace, session_id=session_id)
92
+ return self.run_postflight(
93
+ trace,
94
+ session_id=session_id,
95
+ output=output,
96
+ duration_ms=duration_ms,
97
+ error=error,
98
+ )
99
+
100
+ # ── Preflight / postflight chains ──────────────────────────────────────────
101
+
102
+ def run_preflight(self, trace: "EventTrace", *, session_id: str) -> "Verdict":
103
+ """Execute the preflight gate chain. Returns first DENY or ALLOW.
104
+
105
+ All gates come from the GatePolicy. No per-method overrides.
106
+ Fail-closed: if any gate or internal logic raises, returns DENY.
107
+ """
108
+ from traceforge.sdk.verdict import Verdict
109
+
110
+ try:
111
+ request = self._to_tool_call_request(trace)
112
+ ctx = self._build_gate_context(session_id)
113
+ except Exception as exc:
114
+ deny = Verdict.deny(f"gate setup error (fail-closed): {type(exc).__name__}: {exc}")
115
+ self._record_denial(session_id, deny)
116
+ return deny
117
+
118
+ # Run policy chain
119
+ policy = self._policy_provider()
120
+ if policy and policy.has_preflight:
121
+ for gate in policy.preflight_gates:
122
+ try:
123
+ verdict = gate(request, ctx)
124
+ except Exception as exc:
125
+ # Fail-closed: gate exception = DENY
126
+ deny = Verdict.deny(f"gate error (fail-closed): {type(exc).__name__}: {exc}")
127
+ self._record_denial(session_id, deny)
128
+ return deny
129
+ if verdict.denied:
130
+ self._record_denial(session_id, verdict)
131
+ return verdict
132
+
133
+ self._record_allow(session_id, tool_call_id=trace.tool_call_id)
134
+ return Verdict.allow()
135
+
136
+ def run_postflight(
137
+ self,
138
+ trace: "EventTrace",
139
+ *,
140
+ session_id: str,
141
+ output: dict | None = None,
142
+ duration_ms: int | None = None,
143
+ error: str | None = None,
144
+ ) -> "PostflightVerdict":
145
+ """Execute the postflight gate chain. Returns most restrictive action.
146
+
147
+ Priority: TERMINATE > SUPPRESS > REDACT > ALERT > ACCEPT.
148
+ Fail-closed: if any gate or setup raises, returns SUPPRESS.
149
+ """
150
+ from traceforge.sdk.gate_types import PostflightAction, PostflightVerdict
151
+
152
+ try:
153
+ result = self._to_tool_call_result(
154
+ trace, output=output, duration_ms=duration_ms, error=error
155
+ )
156
+ ctx = self._build_gate_context(session_id)
157
+ except Exception as exc:
158
+ return PostflightVerdict(
159
+ action=PostflightAction.SUPPRESS,
160
+ reason=f"postflight setup error (fail-closed): {type(exc).__name__}: {exc}",
161
+ )
162
+
163
+ # Action severity ordering
164
+ _SEVERITY = {
165
+ PostflightAction.ACCEPT: 0,
166
+ PostflightAction.ALERT: 1,
167
+ PostflightAction.REDACT: 2,
168
+ PostflightAction.SUPPRESS: 3,
169
+ PostflightAction.TERMINATE: 4,
170
+ }
171
+
172
+ most_severe = PostflightVerdict()
173
+
174
+ # Run policy chain
175
+ policy = self._policy_provider()
176
+ if policy and policy.has_postflight:
177
+ for gate in policy.postflight_gates:
178
+ try:
179
+ pv = gate(result, ctx)
180
+ except Exception as exc:
181
+ # Fail-closed: gate exception = SUPPRESS (not TERMINATE to avoid crashing)
182
+ pv = PostflightVerdict(
183
+ action=PostflightAction.SUPPRESS,
184
+ reason=f"postflight gate error (fail-closed): {type(exc).__name__}: {exc}",
185
+ )
186
+ if _SEVERITY.get(pv.action, 0) > _SEVERITY.get(most_severe.action, 0):
187
+ most_severe = pv
188
+
189
+ return most_severe
190
+
191
+ @staticmethod
192
+ def apply_postflight_to_output(pv: "PostflightVerdict", result: str) -> str:
193
+ """Apply a postflight verdict to a string result.
194
+
195
+ Returns the (possibly redacted/suppressed) output string.
196
+ Raises RuntimeError on TERMINATE.
197
+ """
198
+ from traceforge.sdk.gate_types import PostflightAction
199
+
200
+ if pv.action == PostflightAction.ACCEPT or pv.action == PostflightAction.ALERT:
201
+ return result
202
+ if pv.action == PostflightAction.SUPPRESS:
203
+ return "[output suppressed by policy]"
204
+ if pv.action == PostflightAction.REDACT:
205
+ redacted = result
206
+ for key in pv.redaction_keys:
207
+ redacted = redacted.replace(key, "[REDACTED]")
208
+ return redacted
209
+ if pv.action == PostflightAction.TERMINATE:
210
+ raise RuntimeError(f"Session terminated by policy: {pv.reason}")
211
+ return result
212
+
213
+ # ── Gate state + counter (single writer for the gate channel) ──────────────
214
+
215
+ def _observe_completed_call(self, trace: "EventTrace", *, session_id: str) -> None:
216
+ """Advance the single tool-call counter for a shield-allowed call that
217
+ has now executed. The trace only ever observes what the gate allowed, so
218
+ this is the one writer of the counter in the gate channel."""
219
+ state = self._ensure_gate_state(session_id)
220
+ phase_window = state.snapshot().phase_window
221
+ state.increment_budget(
222
+ mechanism=trace.mechanism,
223
+ effect=trace.effect,
224
+ scope=frozenset(trace.scope),
225
+ role=frozenset(trace.role),
226
+ action=frozenset(trace.action),
227
+ capability=frozenset(trace.capability),
228
+ structure=frozenset(trace.structure),
229
+ phase=phase_window[-1] if phase_window else None,
230
+ )
231
+
232
+ def _record_denial(self, session_id: str, verdict: "Verdict") -> None:
233
+ """Record a shield denial. Does not touch the tool-call counter."""
234
+ self._ensure_gate_state(session_id).record_denial(verdict)
235
+
236
+ def _record_allow(self, session_id: str, *, tool_call_id: str | None = None) -> None:
237
+ """Record a shield allow. The tool-call counter is NOT advanced here —
238
+ it advances only when the allowed call is observed at completion
239
+ (see _observe_completed_call). The shield only reads the counter."""
240
+ self._ensure_gate_state(session_id).record_allow(tool_call_id=tool_call_id)
241
+
242
+ def _ensure_gate_state(self, session_id: str):
243
+ """Return session state for gate context tracking (thread-safe, ephemeral)."""
244
+ return self._registry.ensure(session_id)
245
+
246
+ def _build_gate_context(self, session_id: str) -> "GateContext":
247
+ """Build a GateContext from current session state.
248
+
249
+ Reads only — the counter and decision log are owned by SessionState and
250
+ exposed through methods. The shield never mutates state from here.
251
+ """
252
+ from traceforge.sdk.gate_types import GateContext
253
+
254
+ state = self._registry.get_gate(session_id)
255
+ if state is None:
256
+ return GateContext(session_id=session_id, tool_call_count=0, denied_count=0)
257
+
258
+ return GateContext(
259
+ session_id=session_id,
260
+ tool_call_count=state.tool_call_count,
261
+ denied_count=state.denied_count,
262
+ prior_verdicts=state.prior_verdicts(),
263
+ prior_tool_call_ids=state.prior_tool_call_ids(),
264
+ )
265
+
266
+ def _to_tool_call_request(self, trace: "EventTrace") -> "ToolCallRequest":
267
+ """Convert a fully-assessed EventTrace into a gate-facing ToolCallRequest."""
268
+ from traceforge.sdk.gate_types import ToolCallRequest
269
+
270
+ return ToolCallRequest(
271
+ tool=trace.canonical_tool or trace.tool_name or "unknown",
272
+ input=trace.tool_input,
273
+ target=trace.target_resource,
274
+ mechanism=trace.mechanism or "unknown",
275
+ effect=trace.effect or "read_only",
276
+ capabilities=trace.capability,
277
+ scope=trace.scope,
278
+ role=trace.role,
279
+ action=trace.action,
280
+ risk_score=trace.risk_score or 0,
281
+ risk_band=trace.risk_band or "unknown",
282
+ suggested_action=trace.suggested_action or "allow",
283
+ reason=trace.reason or "",
284
+ session_id=trace.session_id,
285
+ tool_call_id=trace.tool_call_id,
286
+ event_trace=trace,
287
+ )
288
+
289
+ def _to_tool_call_result(
290
+ self,
291
+ trace: "EventTrace",
292
+ *,
293
+ output: dict | None = None,
294
+ duration_ms: int | None = None,
295
+ error: str | None = None,
296
+ ) -> "ToolCallResult":
297
+ """Convert a fully-assessed EventTrace + output into a gate-facing ToolCallResult."""
298
+ from traceforge.sdk.gate_types import ToolCallResult
299
+ from traceforge.trace import _deep_freeze, EMPTY_MAP
300
+
301
+ return ToolCallResult(
302
+ tool=trace.canonical_tool or trace.tool_name or "unknown",
303
+ input=trace.tool_input,
304
+ target=trace.target_resource,
305
+ output=_deep_freeze(output) if output else EMPTY_MAP,
306
+ duration_ms=duration_ms,
307
+ error=error,
308
+ mechanism=trace.mechanism or "unknown",
309
+ effect=trace.effect or "read_only",
310
+ capabilities=trace.capability,
311
+ risk_score=trace.risk_score or 0,
312
+ risk_band=trace.risk_band or "unknown",
313
+ suggested_action=trace.suggested_action or "allow",
314
+ reason=trace.reason or "",
315
+ session_id=trace.session_id,
316
+ tool_call_id=trace.tool_call_id,
317
+ event_trace=trace,
318
+ )