132 """Run the six-layer read pipeline; caller holds ``_TOOL_CALL_LOCK``.
134 It handles all error conditions and always writes an audit record.
139 MCP tool name, e.g. ``"oct_lint"``.
141 Unvalidated dict from the MCP client.
145 Redacted tool output string (or a structured error message).
148 state = get_server_state()
149 request_id = str(uuid.uuid4())
150 start_time = time.monotonic()
152 _dbg(
"MCP", func, f
"tool={tool_name} request_id={request_id}", 2)
156 timestamp=_utc_now_iso(),
157 session_id=state.session_id,
158 request_id=request_id,
163 project_root_hash=_hash_project_root(state.project_root),
168 if not state.rate_limiter.consume():
169 record.decision =
"denied"
170 record.policy_rule =
"rate_limit"
172 state.audit_logger.write(record)
173 state.metrics.record_rate_limit_hit(tool_name)
175 "Rate limit exceeded. Please wait before making more tool calls.",
180 decision = state.policy.check(tool_name)
181 record.decision = decision.decision
182 record.policy_rule = decision.policy_rule
184 if not decision.allowed:
186 state.audit_logger.write(record)
187 state.metrics.record_call(tool_name, decision.decision,
193 validated_model = validate_tool_args(
194 tool_name, raw_args, project_root=state.project_root
196 except Exception
as exc:
197 record.decision =
"denied"
198 record.policy_rule =
"validation_failure"
200 state.audit_logger.write(record)
201 state.metrics.record_validation_failure(tool_name)
205 record.args = validated_model.model_dump()
208 sandbox_result = state.executor.execute(
210 validated_args=validated_model.model_dump(),
211 project_root=state.project_root,
214 raw_output = sandbox_result.stdout
215 if sandbox_result.stderr:
217 raw_output +=
"\n--- stderr ---\n" + sandbox_result.stderr
219 raw_output = sandbox_result.stderr
222 output_hash = hashlib.sha256(
223 raw_output.encode(
"utf-8", errors=
"replace")
227 redactor_result = state.redactor.apply_all(raw_output, state.project_root)
230 record.exit_code = sandbox_result.exit_code
232 record.duration_ms = duration_ms
233 record.output_size_bytes = len(
234 redactor_result.text.encode(
"utf-8", errors=
"replace")
236 record.output_truncated = (
237 sandbox_result.output_truncated
or redactor_result.truncated
239 record.output_hash_sha256 = output_hash
240 record.redactions_applied = redactor_result.redactions_applied
242 state.audit_logger.write(record)
243 state.metrics.record_call(
244 tool_name,
"allowed", duration_ms / 1000, redactor_result.redactions_applied
249 f
"tool={tool_name} exit_code={sandbox_result.exit_code} "
250 f
"redactions={redactor_result.redactions_applied}",
253 return redactor_result.text
255 except Exception
as exc:
256 _dbg(
"MCP", func, f
"SYSTEM_ERROR: unexpected error in {tool_name}: {exc}", 1)
257 record.decision =
"denied"
258 record.policy_rule =
"internal_error"
261 state.audit_logger.write(record)
292 """Run the write pipeline; caller holds ``_TOOL_CALL_LOCK``.
294 Pipeline: rate-limiter → policy → validator → safety gate →
295 executor → redactor → audit.
297 When the safety gate requires confirmation (``confirm=False`` in
298 args), returns a structured ``[oct-mcp requires_confirmation] …``
299 string without executing anything. The AI host must re-submit with
300 ``confirm=True`` after the user approves.
305 One of the 11 Phase 5B write tool names.
307 Unvalidated dict from the MCP client.
311 Redacted tool output string, a confirmation-required string, or a
312 structured error message.
314 func =
"_run_tool_write"
315 state = get_server_state()
316 request_id = str(uuid.uuid4())
317 start_time = time.monotonic()
319 _dbg(
"MCP", func, f
"tool={tool_name} request_id={request_id}", 2)
324 timestamp=_utc_now_iso(),
325 session_id=state.session_id,
326 request_id=request_id,
331 project_root_hash=_hash_project_root(state.project_root),
336 if not state.rate_limiter.consume():
337 record.decision =
"denied"
338 record.policy_rule =
"rate_limit"
340 state.audit_logger.write(record)
341 state.metrics.record_rate_limit_hit(tool_name)
343 "Rate limit exceeded. Please wait before making more tool calls.",
348 decision = state.policy.check(tool_name)
349 record.decision = decision.decision
350 record.policy_rule = decision.policy_rule
352 if not decision.allowed:
354 state.audit_logger.write(record)
355 state.metrics.record_call(tool_name, decision.decision,
361 validated_model = validate_tool_args(
362 tool_name, raw_args, project_root=state.project_root
364 except Exception
as exc:
365 record.decision =
"denied"
366 record.policy_rule =
"validation_failure"
368 state.audit_logger.write(record)
369 state.metrics.record_validation_failure(tool_name)
372 record.args = validated_model.model_dump()
375 spec = TOOL_MANIFEST.get(tool_name)
377 record.decision =
"denied"
378 record.policy_rule =
"unknown_tool"
380 state.audit_logger.write(record)
383 safety_decision: SafetyDecision = state.safety.check(
384 tool_name, spec, validated_model.model_dump()
387 if not safety_decision.proceed:
388 record.decision =
"requires_confirmation"
389 record.policy_rule =
"hitl_gate"
391 record.duration_ms = duration_ms
392 state.audit_logger.write(record)
393 state.metrics.record_call(tool_name,
"requires_confirmation",
394 duration_ms / 1000, 0)
395 return _format_confirmation_request(
396 tool_name, spec, validated_model.model_dump()
400 sandbox_result = state.executor.execute_write(
402 validated_args=validated_model.model_dump(),
403 project_root=state.project_root,
404 dry_run_override=safety_decision.dry_run,
407 raw_output = sandbox_result.stdout
408 if sandbox_result.stderr:
410 raw_output +=
"\n--- stderr ---\n" + sandbox_result.stderr
412 raw_output = sandbox_result.stderr
414 output_hash = hashlib.sha256(
415 raw_output.encode(
"utf-8", errors=
"replace")
419 redactor_result = state.redactor.apply_all(raw_output, state.project_root)
422 record.decision =
"allowed"
423 record.exit_code = sandbox_result.exit_code
425 record.duration_ms = duration_ms
426 record.output_size_bytes = len(
427 redactor_result.text.encode(
"utf-8", errors=
"replace")
429 record.output_truncated = (
430 sandbox_result.output_truncated
or redactor_result.truncated
432 record.output_hash_sha256 = output_hash
433 record.redactions_applied = redactor_result.redactions_applied
435 state.audit_logger.write(record)
436 state.metrics.record_call(
437 tool_name,
"allowed", duration_ms / 1000, redactor_result.redactions_applied
442 f
"tool={tool_name} exit_code={sandbox_result.exit_code} "
443 f
"redactions={redactor_result.redactions_applied}",
446 return redactor_result.text
448 except Exception
as exc:
449 _dbg(
"MCP", func, f
"SYSTEM_ERROR: unexpected error in {tool_name}: {exc}", 1)
450 record.decision =
"denied"
451 record.policy_rule =
"internal_error"
454 state.audit_logger.write(record)