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,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
|
+
)
|