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.
41- oct.core.diagnostics (_dbg structured logger)
42- oct.mcp.sandbox (SandboxExecutor and SandboxResult, which run each built argv)
43- oct (not imported; run as python -m oct, the CLI every tool argv invokes)
46from __future__
import annotations
49from pathlib
import Path
50from typing
import Callable
52from oct.core.diagnostics
import _dbg
63 """Build argv for ``oct lint``."""
64 cmd = [sys.executable,
"-m",
"oct",
"lint"]
65 if args.get(
"json_output",
True):
67 profile = args.get(
"profile")
69 cmd.extend([
"--profile", profile])
70 if args.get(
"changed",
False):
71 cmd.append(
"--changed")
72 if args.get(
"fix_headers",
False):
73 cmd.append(
"--fix-headers")
74 jobs = args.get(
"jobs", 1)
76 cmd.extend([
"--jobs", str(jobs)])
77 paths = args.get(
"paths", [])
83 """Build argv for ``oct health``."""
84 cmd = [sys.executable,
"-m",
"oct",
"health"]
85 if args.get(
"json_output",
True):
87 if args.get(
"verbose",
False):
88 cmd.append(
"--verbose")
93 """Build argv for ``oct export-skeleton``.
95 OI-502: no longer emits ``--output-dir``; that flag is not accepted
96 by the CLI. The output destination is derived by the CLI from the
99 cmd = [sys.executable,
"-m",
"oct",
"export-skeleton"]
100 if args.get(
"json_output",
True):
106 """Build argv for ``oct deps``.
108 OI-502: no longer emits ``--check``; that flag is not accepted by
109 the CLI. Pin auditing is instead available via ``--audit-pins`` (not
112 cmd = [sys.executable,
"-m",
"oct",
"deps"]
113 if args.get(
"json_output",
True):
119 """Build argv for ``oct test``.
121 OI-502: no longer emits ``--jobs``; the CLI does not accept it and
122 the ``jobs`` field was removed from :class:`OctTestArgs`.
124 cmd = [sys.executable,
"-m",
"oct",
"test"]
125 if args.get(
"json_output",
True):
127 if args.get(
"changed",
False):
128 cmd.append(
"--changed")
129 paths = args.get(
"paths", [])
135 """Build argv for ``oct typecheck``."""
136 cmd = [sys.executable,
"-m",
"oct",
"typecheck"]
137 if args.get(
"json_output",
True):
139 paths = args.get(
"paths", [])
145 """Build argv for ``oct diag <subcommand>``.
147 OI-502: no longer emits ``--json``; no diag subcommand currently
148 accepts that flag (``validate-config`` may grow one via OI-534, at
149 which point this builder will conditionally re-emit it).
151 subcommand = args.get(
"subcommand",
"validate-config")
152 cmd = [sys.executable,
"-m",
"oct",
"diag", subcommand]
157 """Build argv for ``oct git status``."""
158 cmd = [sys.executable,
"-m",
"oct",
"git",
"status"]
159 if args.get(
"json_output",
True):
161 if args.get(
"verbose",
False):
162 cmd.append(
"--verbose")
167 """Build argv for ``oct git check``."""
168 cmd = [sys.executable,
"-m",
"oct",
"git",
"check"]
169 if args.get(
"json_output",
True):
171 if args.get(
"verbose",
False):
172 cmd.append(
"--verbose")
185 """Build argv for ``oct format``.
187 Uses ``--dry-run`` unless ``apply=True`` AND ``confirm=True``.
188 The safety gate enforces this rule upstream; the argv builder also
189 checks ``_dry_run_override`` (set by execute_write()) as a redundant guard.
191 cmd = [sys.executable,
"-m",
"oct",
"format"]
193 dry_run = args.get(
"_dry_run_override",
not (args.get(
"apply")
and args.get(
"confirm")))
195 cmd.append(
"--dry-run")
198 if args.get(
"json_output",
True):
200 if args.get(
"changed",
False):
201 cmd.append(
"--changed")
202 if args.get(
"verbose",
False):
203 cmd.append(
"--verbose")
205 cmd.extend(args.get(
"paths", []))
210 """Build argv for ``oct docs``."""
211 cmd = [sys.executable,
"-m",
"oct",
"docs"]
212 if args.get(
"all_docs",
False):
214 elif args.get(
"sphinx",
False):
215 cmd.append(
"--sphinx")
216 elif args.get(
"doxygen",
False):
217 cmd.append(
"--doxygen")
218 if args.get(
"clean",
False):
219 cmd.append(
"--clean")
220 if args.get(
"dry_run",
True):
221 cmd.append(
"--dry-run")
226 """Build argv for ``oct export-source``."""
227 cmd = [sys.executable,
"-m",
"oct",
"export-source"]
228 if args.get(
"verbose",
False):
229 cmd.append(
"--verbose")
230 if args.get(
"single_dir",
False):
231 cmd.append(
"--single-dir")
232 if args.get(
"warn_secrets",
True):
233 cmd.append(
"--warn-secrets")
234 if args.get(
"block_secrets",
False):
235 cmd.append(
"--block-secrets")
240 """Build argv for ``oct new``."""
241 path = args.get(
"path",
"")
242 cmd = [sys.executable,
"-m",
"oct",
"new", path]
243 if args.get(
"dry_run",
True):
244 cmd.append(
"--dry-run")
245 if args.get(
"force",
False):
246 cmd.append(
"--force")
247 if args.get(
"create_test",
False):
249 if args.get(
"verbose",
False):
250 cmd.append(
"--verbose")
255 """Build argv for ``oct scaffold``."""
256 name = args.get(
"name",
"")
257 cmd = [sys.executable,
"-m",
"oct",
"scaffold", name]
258 if args.get(
"dry_run",
True):
259 cmd.append(
"--dry-run")
264 """Build argv for ``oct clean``."""
265 cmd = [sys.executable,
"-m",
"oct",
"clean"]
266 path = args.get(
"path")
269 if args.get(
"dry_run",
True):
270 cmd.append(
"--dry-run")
275 """Build argv for ``oct install-hooks`` (deprecated; calls git hooks install)."""
276 cmd = [sys.executable,
"-m",
"oct",
"git",
"hooks",
"install"]
277 if args.get(
"force",
False):
278 cmd.append(
"--force")
283 """Build argv for ``oct git commit``.
285 Note: ``--no-verify`` is intentionally omitted - bypassing hooks
286 is never permitted via MCP (design decision D-5B-6).
288 message = args.get(
"message",
"")
289 cmd = [sys.executable,
"-m",
"oct",
"git",
"commit",
"-m", message]
290 if args.get(
"json_output",
True):
292 if args.get(
"force",
False):
293 cmd.append(
"--force")
298 """Build argv for ``oct git hooks install``."""
299 cmd = [sys.executable,
"-m",
"oct",
"git",
"hooks",
"install"]
300 if args.get(
"force",
False):
301 cmd.append(
"--force")
306 """Build argv for ``oct git init``."""
307 cmd = [sys.executable,
"-m",
"oct",
"git",
"init"]
308 if args.get(
"dry_run",
True):
309 cmd.append(
"--dry-run")
310 if args.get(
"github",
False):
311 cmd.append(
"--github")
312 if args.get(
"no_hooks",
False):
313 cmd.append(
"--no-hooks")
318 """Build argv for ``oct git changelog``."""
319 cmd = [sys.executable,
"-m",
"oct",
"git",
"changelog"]
320 if args.get(
"json_output",
True):
322 if args.get(
"dry_run",
True):
323 cmd.append(
"--dry-run")
324 since = args.get(
"since")
326 cmd.extend([
"--since", since])
327 version = args.get(
"version")
329 cmd.extend([
"--release", version])
337_ARGV_BUILDERS: dict[str, Callable[[dict, Path], list[str]]] = {
339 "oct_lint": _build_argv_lint,
340 "oct_health": _build_argv_health,
341 "oct_skeleton": _build_argv_skeleton,
342 "oct_deps": _build_argv_deps,
343 "oct_test": _build_argv_test,
344 "oct_typecheck": _build_argv_typecheck,
345 "oct_diag": _build_argv_diag,
346 "oct_git_status": _build_argv_git_status,
347 "oct_git_check": _build_argv_git_check,
349 "oct_format": _build_argv_format,
350 "oct_docs": _build_argv_docs,
351 "oct_export_source": _build_argv_export_source,
352 "oct_new": _build_argv_new,
353 "oct_scaffold": _build_argv_scaffold,
354 "oct_clean": _build_argv_clean,
355 "oct_install_hooks": _build_argv_install_hooks,
356 "oct_git_commit": _build_argv_git_commit,
357 "oct_git_hooks_install": _build_argv_git_hooks_install,
358 "oct_git_init": _build_argv_git_init,
359 "oct_git_changelog": _build_argv_git_changelog,
369 """Maps MCP tool names and validated args to sandboxed ``oct`` calls.
374 :class:`~oct.mcp.sandbox.SandboxExecutor` instance (injected
375 so tests can substitute a mock).
377 Default subprocess timeout in seconds.
379 Override timeout for ``oct_test`` (pytest may need more time).
381 Hard cap on combined stdout+stderr passed to the sandbox.
386 sandbox: SandboxExecutor |
None =
None,
387 timeout_default: int = 60,
388 timeout_test: int = 300,
389 max_output_bytes: int = 1_048_576,
391 func =
"ToolExecutor.__init__"
396 _dbg(
"MCP", func,
"executor initialised", 2)
401 validated_args: dict,
404 """Execute *tool_name* with *validated_args* inside the sandbox.
409 One of the 9 Phase 5A tool names.
411 A ``model_dump()`` dict from the corresponding Pydantic model.
413 Absolute project root path (used as subprocess cwd and
414 forwarded to the argv builder).
418 :class:`~oct.mcp.sandbox.SandboxResult` - never raises.
420 func =
"ToolExecutor.execute"
421 builder = _ARGV_BUILDERS.get(tool_name)
423 msg = f
"No argv builder for tool {tool_name!r}"
424 _dbg(
"MCP", func, f
"SYSTEM_ERROR: {msg}", 1)
432 cmd = builder(validated_args, project_root)
433 _dbg(
"MCP", func, f
"tool={tool_name} cmd={cmd!r}", 3)
438 if tool_name ==
"oct_test"
452 validated_args: dict,
454 dry_run_override: bool =
False,
456 """Execute a write tool, injecting the safety-gate dry-run decision.
461 One of the 11 Phase 5B write tool names.
463 A ``model_dump()`` dict from the corresponding Pydantic model.
465 Absolute project root path.
467 When ``True`` (as determined by :class:`~oct.mcp.safety.SafetyGate`),
468 injects ``_dry_run_override=True`` into the args dict so that
469 ``_build_argv_format`` always passes ``--dry-run`` regardless of
470 the ``apply`` field value.
472 func =
"ToolExecutor.execute_write"
473 args = dict(validated_args)
475 args[
"_dry_run_override"] =
True
477 builder = _ARGV_BUILDERS.get(tool_name)
479 msg = f
"No argv builder for write tool {tool_name!r}"
480 _dbg(
"MCP", func, f
"SYSTEM_ERROR: {msg}", 1)
488 cmd = builder(args, project_root)
489 _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)
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)