84def _run_tool(tool_name: str, raw_args: dict) -> str:
85 """Execute one tool through the full six-layer pipeline.
87 This is the single shared entry point called by every tool function.
88 It handles all error conditions and always writes an audit record.
93 MCP tool name, e.g. ``"oct_lint"``.
95 Unvalidated dict from the MCP client.
99 Redacted tool output string (or a structured error message).
102 state = get_server_state()
103 request_id = str(uuid.uuid4())
104 start_time = time.monotonic()
106 _dbg(
"MCP", func, f
"tool={tool_name} request_id={request_id}", 2)
110 timestamp=_utc_now_iso(),
111 session_id=state.session_id,
112 request_id=request_id,
117 project_root_hash=_hash_project_root(state.project_root),
122 if not state.rate_limiter.consume():
123 record.decision =
"denied"
124 record.policy_rule =
"rate_limit"
126 state.audit_logger.write(record)
127 state.metrics.record_rate_limit_hit(tool_name)
129 "Rate limit exceeded. Please wait before making more tool calls.",
134 decision = state.policy.check(tool_name)
135 record.decision = decision.decision
136 record.policy_rule = decision.policy_rule
138 if not decision.allowed:
140 state.audit_logger.write(record)
141 state.metrics.record_call(tool_name, decision.decision,
147 validated_model = validate_tool_args(
148 tool_name, raw_args, project_root=state.project_root
150 except Exception
as exc:
151 record.decision =
"denied"
152 record.policy_rule =
"validation_failure"
154 state.audit_logger.write(record)
155 state.metrics.record_validation_failure(tool_name)
159 record.args = validated_model.model_dump()
162 sandbox_result = state.executor.execute(
164 validated_args=validated_model.model_dump(),
165 project_root=state.project_root,
168 raw_output = sandbox_result.stdout
169 if sandbox_result.stderr:
171 raw_output +=
"\n--- stderr ---\n" + sandbox_result.stderr
173 raw_output = sandbox_result.stderr
176 output_hash = hashlib.sha256(
177 raw_output.encode(
"utf-8", errors=
"replace")
181 redactor_result = state.redactor.apply_all(raw_output, state.project_root)
184 record.exit_code = sandbox_result.exit_code
186 record.duration_ms = duration_ms
187 record.output_size_bytes = len(
188 redactor_result.text.encode(
"utf-8", errors=
"replace")
190 record.output_truncated = (
191 sandbox_result.output_truncated
or redactor_result.truncated
193 record.output_hash_sha256 = output_hash
194 record.redactions_applied = redactor_result.redactions_applied
196 state.audit_logger.write(record)
197 state.metrics.record_call(
198 tool_name,
"allowed", duration_ms / 1000, redactor_result.redactions_applied
203 f
"tool={tool_name} exit_code={sandbox_result.exit_code} "
204 f
"redactions={redactor_result.redactions_applied}",
207 return redactor_result.text
209 except Exception
as exc:
210 _dbg(
"MCP", func, f
"SYSTEM_ERROR: unexpected error in {tool_name}: {exc}", 1)
211 record.decision =
"denied"
212 record.policy_rule =
"internal_error"
215 state.audit_logger.write(record)
222 """Execute one write tool through the HITL-extended pipeline.
224 Pipeline: rate-limiter → policy → validator → safety gate →
225 executor → redactor → audit.
227 When the safety gate requires confirmation (``confirm=False`` in
228 args), returns a structured ``[oct-mcp requires_confirmation] …``
229 string without executing anything. The AI host must re-submit with
230 ``confirm=True`` after the user approves.
235 One of the 11 Phase 5B write tool names.
237 Unvalidated dict from the MCP client.
241 Redacted tool output string, a confirmation-required string, or a
242 structured error message.
244 func =
"_run_tool_write"
245 state = get_server_state()
246 request_id = str(uuid.uuid4())
247 start_time = time.monotonic()
249 _dbg(
"MCP", func, f
"tool={tool_name} request_id={request_id}", 2)
254 timestamp=_utc_now_iso(),
255 session_id=state.session_id,
256 request_id=request_id,
261 project_root_hash=_hash_project_root(state.project_root),
266 if not state.rate_limiter.consume():
267 record.decision =
"denied"
268 record.policy_rule =
"rate_limit"
270 state.audit_logger.write(record)
271 state.metrics.record_rate_limit_hit(tool_name)
273 "Rate limit exceeded. Please wait before making more tool calls.",
278 decision = state.policy.check(tool_name)
279 record.decision = decision.decision
280 record.policy_rule = decision.policy_rule
282 if not decision.allowed:
284 state.audit_logger.write(record)
285 state.metrics.record_call(tool_name, decision.decision,
291 validated_model = validate_tool_args(
292 tool_name, raw_args, project_root=state.project_root
294 except Exception
as exc:
295 record.decision =
"denied"
296 record.policy_rule =
"validation_failure"
298 state.audit_logger.write(record)
299 state.metrics.record_validation_failure(tool_name)
302 record.args = validated_model.model_dump()
305 spec = TOOL_MANIFEST.get(tool_name)
307 record.decision =
"denied"
308 record.policy_rule =
"unknown_tool"
310 state.audit_logger.write(record)
313 safety_decision: SafetyDecision = state.safety.check(
314 tool_name, spec, validated_model.model_dump()
317 if not safety_decision.proceed:
318 record.decision =
"requires_confirmation"
319 record.policy_rule =
"hitl_gate"
321 record.duration_ms = duration_ms
322 state.audit_logger.write(record)
323 state.metrics.record_call(tool_name,
"requires_confirmation",
324 duration_ms / 1000, 0)
325 return _format_confirmation_request(
326 tool_name, spec, validated_model.model_dump()
330 sandbox_result = state.executor.execute_write(
332 validated_args=validated_model.model_dump(),
333 project_root=state.project_root,
334 dry_run_override=safety_decision.dry_run,
337 raw_output = sandbox_result.stdout
338 if sandbox_result.stderr:
340 raw_output +=
"\n--- stderr ---\n" + sandbox_result.stderr
342 raw_output = sandbox_result.stderr
344 output_hash = hashlib.sha256(
345 raw_output.encode(
"utf-8", errors=
"replace")
349 redactor_result = state.redactor.apply_all(raw_output, state.project_root)
352 record.decision =
"allowed"
353 record.exit_code = sandbox_result.exit_code
355 record.duration_ms = duration_ms
356 record.output_size_bytes = len(
357 redactor_result.text.encode(
"utf-8", errors=
"replace")
359 record.output_truncated = (
360 sandbox_result.output_truncated
or redactor_result.truncated
362 record.output_hash_sha256 = output_hash
363 record.redactions_applied = redactor_result.redactions_applied
365 state.audit_logger.write(record)
366 state.metrics.record_call(
367 tool_name,
"allowed", duration_ms / 1000, redactor_result.redactions_applied
372 f
"tool={tool_name} exit_code={sandbox_result.exit_code} "
373 f
"redactions={redactor_result.redactions_applied}",
376 return redactor_result.text
378 except Exception
as exc:
379 _dbg(
"MCP", func, f
"SYSTEM_ERROR: unexpected error in {tool_name}: {exc}", 1)
380 record.decision =
"denied"
381 record.policy_rule =
"internal_error"
384 state.audit_logger.write(record)