feat: AAIS/Jarvis governed workspace tools MCP - #17
Conversation
Expose sandboxed read/write/search/git and allowlisted tests over stdio MCP for Cursor, plus a local capability adapter so Jarvis can call the same tools before an MCP client exists. Co-authored-by: Cursor <cursoragent@cursor.com>
| elif not re.fullmatch(r"[A-Za-z0-9_./\\-]+", text): | ||
| return { | ||
| "ok": False, | ||
| "reason_code": "COMMAND_ARG_DENIED", | ||
| "error": f"Unsafe extra arg refused: {text}", | ||
| } | ||
| argv.append(text) |
There was a problem hiding this comment.
Pytest selectors bypass sandbox
When extra_args contains an absolute or traversal pytest selector, the character check accepts it and appends it without calling WorkspacePathSandbox.resolve_path, allowing pytest to execute Python outside AAIS_WORKSPACE_ROOT with the server's privileges.
How this was verified: The request-controlled selector was traced through the permissive argument check directly into the pytest subprocess without the sandbox's absolute-path or traversal guards.
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/aais-tools-mcp/aais_tools_mcp/tools.py
Line: 386-392
Comment:
**Pytest selectors bypass sandbox**
When `extra_args` contains an absolute or traversal pytest selector, the character check accepts it and appends it without calling `WorkspacePathSandbox.resolve_path`, allowing pytest to execute Python outside `AAIS_WORKSPACE_ROOT` with the server's privileges.
**How this was verified:** The request-controlled selector was traced through the permissive argument check directly into the pytest subprocess without the sandbox's absolute-path or traversal guards.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| regex = re.compile(cleaned, flags) | ||
| except re.error as exc: | ||
| return {"ok": False, "reason_code": "PATTERN_INVALID", "error": str(exc)} | ||
|
|
||
| start = self.sandbox.resolve_path(path or ".") | ||
| root = self.sandbox.resolve_root() | ||
| if start.is_file(): | ||
| files = [start] | ||
| elif start.is_dir(): | ||
| files = list(self._iter_text_files(start)) | ||
| else: | ||
| return {"ok": False, "reason_code": "PATH_NOT_FOUND", "error": f"Not found: {path}"} | ||
|
|
||
| matches: list[dict[str, Any]] = [] | ||
| limit = max(1, min(int(max_matches or MAX_SEARCH_MATCHES), MAX_SEARCH_MATCHES)) | ||
| for file_path in files: | ||
| try: | ||
| text = file_path.read_text(encoding="utf-8", errors="replace") | ||
| except OSError: | ||
| continue | ||
| for line_no, line in enumerate(text.splitlines(), start=1): | ||
| if regex.search(line): |
There was a problem hiding this comment.
When a caller applies a catastrophic-backtracking expression to a long source line, regex.search runs without a timeout, blocking the synchronous stdio loop and making every subsequent MCP request unresponsive.
How this was verified: Caller-controlled patterns reach unbounded regex evaluation on lines up to the file-size limit inside the server's serial request path.
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/aais-tools-mcp/aais_tools_mcp/tools.py
Line: 283-304
Comment:
**Regex search blocks server**
When a caller applies a catastrophic-backtracking expression to a long source line, `regex.search` runs without a timeout, blocking the synchronous stdio loop and making every subsequent MCP request unresponsive.
**How this was verified:** Caller-controlled patterns reach unbounded regex evaluation on lines up to the file-size limit inside the server's serial request path.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| """Governed workspace tool implementations for AAIS / Jarvis. | ||
|
|
||
| Mythic: Operator Workshop Toolkit | ||
| Engineering: AaisOperatorToolCatalog (+ per-tool classes) | ||
|
|
||
| Inputs: tool name + JSON-compatible args | ||
| Outputs: result dict with ok/error and reason_code on failure | ||
| Constraints: sandboxed paths; writes gated; commands allowlisted only | ||
| Failure modes: sandbox deny, policy deny, OS errors → structured error dict |
There was a problem hiding this comment.
Subsystem boundaries remain undocumented
This new module lacks the required Mythic/Engineering/Responsibilities/Non-responsibilities/Invariants header and combines seven tool implementations with the catalog dispatcher. Split the independent subsystems and add the mandatory header blocks so ownership and boundaries remain explicit; the same missing-header pattern occurs across the other new Python subsystem files.
Context Used: One subsystem per file; mandatory file header bloc... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/aais-tools-mcp/aais_tools_mcp/tools.py
Line: 1-9
Comment:
**Subsystem boundaries remain undocumented**
This new module lacks the required Mythic/Engineering/Responsibilities/Non-responsibilities/Invariants header and combines seven tool implementations with the catalog dispatcher. Split the independent subsystems and add the mandatory header blocks so ownership and boundaries remain explicit; the same missing-header pattern occurs across the other new Python subsystem files.
**Context Used:** One subsystem per file; mandatory file header bloc... ([source](https://github.com/warheart1984-ctrl/project-infinity/blob/main/.cursor/rules/jon-file-structure.mdc))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| "sys", | ||
| "dev", | ||
| "map_files", |
There was a problem hiding this comment.
The new public classes and functions use ordinary docstrings rather than the required adjacent # Mythic: and # Engineering: comment pair, leaving their operator-facing and implementation-facing roles inconsistent with the repository convention. This pattern also affects the public declarations in the capability, evidence, sandbox, and server modules.
Context Used: Jon mythic vs engineering ontology — identifiers a... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/aais-tools-mcp/aais_tools_mcp/tools.py
Line: 47-49
Comment:
**Dual ontology comments absent**
The new public classes and functions use ordinary docstrings rather than the required adjacent `# Mythic:` and `# Engineering:` comment pair, leaving their operator-facing and implementation-facing roles inconsistent with the repository convention. This pattern also affects the public declarations in the capability, evidence, sandbox, and server modules.
**Context Used:** Jon mythic vs engineering ontology — identifiers a... ([source](https://github.com/warheart1984-ctrl/project-infinity/blob/main/.cursor/rules/jon-ontology.mdc))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| try: | ||
| return handler(dict(arguments or {})) | ||
| except WorkspaceSandboxError as exc: | ||
| return {"ok": False, "reason_code": exc.reason_code, "error": exc.message} | ||
|
|
||
| def _call_read_file(self, args: dict[str, Any]) -> dict[str, Any]: | ||
| return self.read_file.run(str(args.get("path") or ""), max_chars=int(args.get("max_chars") or MAX_READ_CHARS)) |
There was a problem hiding this comment.
Malformed arguments escape contract
When a non-conforming MCP or local capability caller provides a wrong-typed JSON value such as max_chars="abc", unchecked conversions raise ValueError or TypeError while catalog.call catches only WorkspaceSandboxError. Local invocation therefore raises into Jarvis, and MCP invocation returns a generic text error instead of the documented structured ok=false result with a reason code; the same issue affects max_matches and patch string operands.
Context Used: Jon Safety Net — engineering-first prompts with IO... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: services/aais-tools-mcp/aais_tools_mcp/tools.py
Line: 531-537
Comment:
**Malformed arguments escape contract**
When a non-conforming MCP or local capability caller provides a wrong-typed JSON value such as `max_chars="abc"`, unchecked conversions raise `ValueError` or `TypeError` while `catalog.call` catches only `WorkspaceSandboxError`. Local invocation therefore raises into Jarvis, and MCP invocation returns a generic text error instead of the documented structured `ok=false` result with a reason code; the same issue affects `max_matches` and patch string operands.
**Context Used:** Jon Safety Net — engineering-first prompts with IO... ([source](https://github.com/warheart1984-ctrl/project-infinity/blob/main/.cursor/rules/jon-prompting.mdc))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Prefer stdio MCP when AAIS_JARVIS_TOOLS_MCP=1 with fail-open to the in-process adapter, and pin workspace roots via workspace_root so container walks never hit /proc map_files EPERM. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
services/aais-tools-mcp/) with sandboxed read/write/search, allowlisted tests, and git status/diff.AAIS_JARVIS_TOOLS_MCP=1, viasrc/aais_tools_mcp_client.py, with fail-open to the in-process adapter (src/aais_tools_mcp_adapter.py) so chat never dies on spawn/protocol failure.src/workspace_root.py) so MCP children and Jarvis walks never treat/as the workspace (avoids/proc/*/map_filesEPERM).Env flags
AAIS_JARVIS_TOOLS_MCP=1AAIS_TOOLS_MCP_CMDpython3 -m aais_tools_mcp)AAIS_TOOLS_MCP_TIMEOUT_SECAAIS_WORKSPACE_ROOTAAIS_TOOLS_MCP_ALLOW_WRITESallow_write=true)Selection
JarvisOperator.handle_tool_requestroutesread_file/write_file/apply_patch/list_dir/search_code/run_tests/git_status/git_diff→invoke_aais_operator_tool(MCP then adapter). Natural-language workspace browse still usesWorkspaceTools.Test plan
python3 -m pytest tests/test_aais_tools_mcp_client.py tests/test_aais_tools_mcp_adapter.py tests/test_workspace_root.py -qcd services/aais-tools-mcp && PYTHONPATH=. python3 -m pytest tests -qAAIS_JARVIS_TOOLS_MCP=1→transport=mcp_stdioforlist_dirRelated
src/workspace_root.py).Greptile Summary
The PR adds a sandboxed AAIS/Jarvis workspace-tools MCP service and routes structured Jarvis tool requests through stdio MCP with fallback to the in-process adapter.
Confidence Score: 0/5
The PR is not safe to merge while test selectors can escape the workspace, hostile regex searches can block the MCP service, and malformed arguments can break the structured tool contract.
Test selector arguments still reach pytest without sandbox resolution, regex evaluation remains unbounded in the serial request loop, and wrong-typed numeric fields still raise outside the catalog's documented structured-error handling.
Files Needing Attention: services/aais-tools-mcp/aais_tools_mcp/tools.py, services/aais-tools-mcp/aais_tools_mcp/server.py, services/aais-tools-mcp/aais_tools_mcp/capability_adapter.py
Important Files Changed
Sequence Diagram
sequenceDiagram participant J as JarvisOperator participant I as invoke_aais_operator_tool participant C as Stdio MCP Client participant S as AAIS Tools MCP participant A as Local Adapter participant W as Workspace J->>I: structured tool request alt MCP enabled I->>C: invoke tool C->>S: tools/call over stdio S->>W: sandboxed operation S-->>C: structured result C-->>I: result else MCP unavailable or disabled I->>A: fail-open invocation A->>W: sandboxed operation A-->>I: structured result end I-->>J: tool responseReviews (2): Last reviewed commit: "docs: note local adapter is stdio fallba..." | Re-trigger Greptile