8Tool Executor (Layer 4, inner logic) for the ``oct-mcp`` six-layer
9pipeline. Maps validated tool arguments to ``oct`` CLI argv lists and
10delegates execution to :class:`oct.mcp.sandbox.SandboxExecutor`.
14- Provide one ``_build_argv_<tool>()`` function per Phase 5A tool that
15 converts a validated Pydantic model to an ``["oct", subcommand, ...]``
17- Provide :class:`ToolExecutor` which wires together the Pydantic model
19- Never import oct internals directly — always call the ``oct`` CLI as
20 a subprocess (blueprint §5.5 isolation principle, design decision D3).
26 L2 — lifecycle: tool execution
32- This module does not import ``click`` or the MCP SDK.
33- ``_build_argv_*()`` functions accept a ``dict`` (from
34 ``model.model_dump()``) and return a ``list[str]``. They never raise
35 for valid validated input.
36- :meth:`ToolExecutor.execute` never raises — it returns
37 :class:`~oct.mcp.sandbox.SandboxResult` directly from the sandbox.
40from __future__
import annotations
43from pathlib
import Path
44from typing
import Callable
47 from oc_diagnostics
import _dbg
as _real_dbg
49 def _dbg(*args, **kwargs) -> None:
50 _real_dbg(*args, **kwargs)
52 def _dbg(*args, **kwargs) -> None:
64 """Build argv for ``oct lint``."""
65 cmd = [sys.executable,
"-m",
"oct",
"lint"]
66 if args.get(
"json_output",
True):
68 profile = args.get(
"profile")
70 cmd.extend([
"--profile", profile])
71 if args.get(
"changed",
False):
72 cmd.append(
"--changed")
73 if args.get(
"fix_headers",
False):
74 cmd.append(
"--fix-headers")
75 jobs = args.get(
"jobs", 1)
77 cmd.extend([
"--jobs", str(jobs)])
78 paths = args.get(
"paths", [])
84 """Build argv for ``oct health``."""
85 cmd = [sys.executable,
"-m",
"oct",
"health"]
86 if args.get(
"json_output",
True):
88 if args.get(
"verbose",
False):
89 cmd.append(
"--verbose")
94 """Build argv for ``oct export-skeleton``.
96 OI-502: no longer emits ``--output-dir``; that flag is not accepted
97 by the CLI. The output destination is derived by the CLI from the
100 cmd = [sys.executable,
"-m",
"oct",
"export-skeleton"]
101 if args.get(
"json_output",
True):
107 """Build argv for ``oct deps``.
109 OI-502: no longer emits ``--check``; that flag is not accepted by
110 the CLI. Pin auditing is instead available via ``--audit-pins`` (not
113 cmd = [sys.executable,
"-m",
"oct",
"deps"]
114 if args.get(
"json_output",
True):
120 """Build argv for ``oct test``.
122 OI-502: no longer emits ``--jobs``; the CLI does not accept it and
123 the ``jobs`` field was removed from :class:`OctTestArgs`.
125 cmd = [sys.executable,
"-m",
"oct",
"test"]
126 if args.get(
"json_output",
True):
128 if args.get(
"changed",
False):
129 cmd.append(
"--changed")
130 paths = args.get(
"paths", [])
136 """Build argv for ``oct typecheck``."""
137 cmd = [sys.executable,
"-m",
"oct",
"typecheck"]
138 if args.get(
"json_output",
True):
140 paths = args.get(
"paths", [])
146 """Build argv for ``oct diag <subcommand>``.
148 OI-502: no longer emits ``--json``; no diag subcommand currently
149 accepts that flag (``validate-config`` may grow one via OI-534, at
150 which point this builder will conditionally re-emit it).
152 subcommand = args.get(
"subcommand",
"validate-config")
153 cmd = [sys.executable,
"-m",
"oct",
"diag", subcommand]
158 """Build argv for ``oct git status``."""
159 cmd = [sys.executable,
"-m",
"oct",
"git",
"status"]
160 if args.get(
"json_output",
True):
162 if args.get(
"verbose",
False):
163 cmd.append(
"--verbose")
168 """Build argv for ``oct git check``."""
169 cmd = [sys.executable,
"-m",
"oct",
"git",
"check"]
170 if args.get(
"json_output",
True):
172 if args.get(
"verbose",
False):
173 cmd.append(
"--verbose")
186 """Build argv for ``oct format``.
188 Uses ``--dry-run`` unless ``apply=True`` AND ``confirm=True``.
189 The safety gate enforces this rule upstream; the argv builder also
190 checks ``_dry_run_override`` (set by execute_write()) as a redundant guard.
192 cmd = [sys.executable,
"-m",
"oct",
"format"]
194 dry_run = args.get(
"_dry_run_override",
not (args.get(
"apply")
and args.get(
"confirm")))
196 cmd.append(
"--dry-run")
199 if args.get(
"json_output",
True):
201 if args.get(
"changed",
False):
202 cmd.append(
"--changed")
203 if args.get(
"verbose",
False):
204 cmd.append(
"--verbose")
206 cmd.extend(args.get(
"paths", []))
211 """Build argv for ``oct docs``."""
212 cmd = [sys.executable,
"-m",
"oct",
"docs"]
213 if args.get(
"all_docs",
False):
215 elif args.get(
"sphinx",
False):
216 cmd.append(
"--sphinx")
217 elif args.get(
"doxygen",
False):
218 cmd.append(
"--doxygen")
219 if args.get(
"clean",
False):
220 cmd.append(
"--clean")
221 if args.get(
"dry_run",
True):
222 cmd.append(
"--dry-run")
227 """Build argv for ``oct export-source``."""
228 cmd = [sys.executable,
"-m",
"oct",
"export-source"]
229 if args.get(
"verbose",
False):
230 cmd.append(
"--verbose")
231 if args.get(
"single_dir",
False):
232 cmd.append(
"--single-dir")
233 if args.get(
"warn_secrets",
True):
234 cmd.append(
"--warn-secrets")
235 if args.get(
"block_secrets",
False):
236 cmd.append(
"--block-secrets")
241 """Build argv for ``oct new``."""
242 path = args.get(
"path",
"")
243 cmd = [sys.executable,
"-m",
"oct",
"new", path]
244 if args.get(
"dry_run",
True):
245 cmd.append(
"--dry-run")
246 if args.get(
"force",
False):
247 cmd.append(
"--force")
248 if args.get(
"create_test",
False):
250 if args.get(
"verbose",
False):
251 cmd.append(
"--verbose")
256 """Build argv for ``oct scaffold``."""
257 name = args.get(
"name",
"")
258 cmd = [sys.executable,
"-m",
"oct",
"scaffold", name]
259 if args.get(
"dry_run",
True):
260 cmd.append(
"--dry-run")
265 """Build argv for ``oct clean``."""
266 cmd = [sys.executable,
"-m",
"oct",
"clean"]
267 path = args.get(
"path")
270 if args.get(
"dry_run",
True):
271 cmd.append(
"--dry-run")
276 """Build argv for ``oct install-hooks`` (deprecated; calls git hooks install)."""
277 cmd = [sys.executable,
"-m",
"oct",
"git",
"hooks",
"install"]
278 if args.get(
"force",
False):
279 cmd.append(
"--force")
284 """Build argv for ``oct git commit``.
286 Note: ``--no-verify`` is intentionally omitted — bypassing hooks
287 is never permitted via MCP (design decision D-5B-6).
289 message = args.get(
"message",
"")
290 cmd = [sys.executable,
"-m",
"oct",
"git",
"commit",
"-m", message]
291 if args.get(
"json_output",
True):
293 if args.get(
"force",
False):
294 cmd.append(
"--force")
299 """Build argv for ``oct git hooks install``."""
300 cmd = [sys.executable,
"-m",
"oct",
"git",
"hooks",
"install"]
301 if args.get(
"force",
False):
302 cmd.append(
"--force")
307 """Build argv for ``oct git init``."""
308 cmd = [sys.executable,
"-m",
"oct",
"git",
"init"]
309 if args.get(
"dry_run",
True):
310 cmd.append(
"--dry-run")
311 if args.get(
"github",
False):
312 cmd.append(
"--github")
313 if args.get(
"no_hooks",
False):
314 cmd.append(
"--no-hooks")
319 """Build argv for ``oct git changelog``."""
320 cmd = [sys.executable,
"-m",
"oct",
"git",
"changelog"]
321 if args.get(
"json_output",
True):
323 if args.get(
"dry_run",
True):
324 cmd.append(
"--dry-run")
325 since = args.get(
"since")
327 cmd.extend([
"--since", since])
328 version = args.get(
"version")
330 cmd.extend([
"--release", version])
338_ARGV_BUILDERS: dict[str, Callable[[dict, Path], list[str]]] = {
340 "oct_lint": _build_argv_lint,
341 "oct_health": _build_argv_health,
342 "oct_skeleton": _build_argv_skeleton,
343 "oct_deps": _build_argv_deps,
344 "oct_test": _build_argv_test,
345 "oct_typecheck": _build_argv_typecheck,
346 "oct_diag": _build_argv_diag,
347 "oct_git_status": _build_argv_git_status,
348 "oct_git_check": _build_argv_git_check,
350 "oct_format": _build_argv_format,
351 "oct_docs": _build_argv_docs,
352 "oct_export_source": _build_argv_export_source,
353 "oct_new": _build_argv_new,
354 "oct_scaffold": _build_argv_scaffold,
355 "oct_clean": _build_argv_clean,
356 "oct_install_hooks": _build_argv_install_hooks,
357 "oct_git_commit": _build_argv_git_commit,
358 "oct_git_hooks_install": _build_argv_git_hooks_install,
359 "oct_git_init": _build_argv_git_init,
360 "oct_git_changelog": _build_argv_git_changelog,
370 """Maps MCP tool names and validated args to sandboxed ``oct`` calls.
375 :class:`~oct.mcp.sandbox.SandboxExecutor` instance (injected
376 so tests can substitute a mock).
378 Default subprocess timeout in seconds.
380 Override timeout for ``oct_test`` (pytest may need more time).
382 Hard cap on combined stdout+stderr passed to the sandbox.
387 sandbox: SandboxExecutor |
None =
None,
388 timeout_default: int = 60,
389 timeout_test: int = 300,
390 max_output_bytes: int = 1_048_576,
392 func =
"ToolExecutor.__init__"
397 _dbg(
"MCP", func,
"executor initialised", 2)
402 validated_args: dict,
405 """Execute *tool_name* with *validated_args* inside the sandbox.
410 One of the 9 Phase 5A tool names.
412 A ``model_dump()`` dict from the corresponding Pydantic model.
414 Absolute project root path (used as subprocess cwd and
415 forwarded to the argv builder).
419 :class:`~oct.mcp.sandbox.SandboxResult` — never raises.
421 func =
"ToolExecutor.execute"
422 builder = _ARGV_BUILDERS.get(tool_name)
424 msg = f
"No argv builder for tool {tool_name!r}"
425 _dbg(
"MCP", func, f
"SYSTEM_ERROR: {msg}", 1)
433 cmd = builder(validated_args, project_root)
434 _dbg(
"MCP", func, f
"tool={tool_name} cmd={cmd!r}", 3)
439 if tool_name ==
"oct_test"
453 validated_args: dict,
455 dry_run_override: bool =
False,
457 """Execute a write tool, injecting the safety-gate dry-run decision.
462 One of the 11 Phase 5B write tool names.
464 A ``model_dump()`` dict from the corresponding Pydantic model.
466 Absolute project root path.
468 When ``True`` (as determined by :class:`~oct.mcp.safety.SafetyGate`),
469 injects ``_dry_run_override=True`` into the args dict so that
470 ``_build_argv_format`` always passes ``--dry-run`` regardless of
471 the ``apply`` field value.
473 func =
"ToolExecutor.execute_write"
474 args = dict(validated_args)
476 args[
"_dry_run_override"] =
True
478 builder = _ARGV_BUILDERS.get(tool_name)
480 msg = f
"No argv builder for write tool {tool_name!r}"
481 _dbg(
"MCP", func, f
"SYSTEM_ERROR: {msg}", 1)
489 cmd = builder(args, project_root)
490 _dbg(
"MCP", func, f
"tool={tool_name} cmd={cmd!r}", 3)
list[str] _build_argv_git_check(dict args, Path project_root)
list[str] _build_argv_health(dict args, Path project_root)
list[str] _build_argv_clean(dict args, Path project_root)
list[str] _build_argv_install_hooks(dict args, Path project_root)
list[str] _build_argv_new(dict args, Path project_root)
list[str] _build_argv_git_init(dict args, Path project_root)
list[str] _build_argv_docs(dict args, Path project_root)
list[str] _build_argv_diag(dict args, Path project_root)
None _dbg(*args, **kwargs)
list[str] _build_argv_typecheck(dict args, Path project_root)
list[str] _build_argv_test(dict args, Path project_root)
list[str] _build_argv_git_status(dict args, Path project_root)
list[str] _build_argv_git_changelog(dict args, Path project_root)
list[str] _build_argv_git_hooks_install(dict args, Path project_root)
list[str] _build_argv_lint(dict args, Path project_root)
list[str] _build_argv_deps(dict args, Path project_root)
list[str] _build_argv_skeleton(dict args, Path project_root)
list[str] _build_argv_export_source(dict args, Path project_root)
list[str] _build_argv_format(dict args, Path project_root)
list[str] _build_argv_scaffold(dict args, Path project_root)
list[str] _build_argv_git_commit(dict args, Path project_root)