Skip to content

Commit 10bf27d

Browse files
committed
feat(orchestrator): per-handler JSON Schema validation; doctor validate-recipe checks handler args; add mcp.call schema
1 parent 9bfb3ce commit 10bf27d

8 files changed

Lines changed: 80 additions & 0 deletions

File tree

‎orchestrator/engine/handlers/agent.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
class AgentWaitHandler(NodeHandler):
99
name = "agent.wait"
1010

11+
def schema(self) -> Dict[str, Any]:
12+
return {
13+
"type": "object",
14+
"properties": {"goal": {"type": "string"}},
15+
"additionalProperties": False,
16+
}
17+
1118
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
1219
logger = kw.get("logger")
1320
goal = str(args.get("goal", ""))

‎orchestrator/engine/handlers/git.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
class GitHandler(NodeHandler):
99
name = "git.apply"
1010

11+
def schema(self) -> Dict[str, Any]:
12+
return {
13+
"type": "object",
14+
"properties": {
15+
"branch": {"type": "string"},
16+
"commit": {"type": "string"},
17+
},
18+
"additionalProperties": False,
19+
}
20+
1121
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
1222
git = kw.get("git")
1323
assert git is not None, "Git adapter missing"

‎orchestrator/engine/handlers/github.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
class OpenPRHandler(NodeHandler):
99
name = "github.open_pr"
1010

11+
def schema(self) -> Dict[str, Any]:
12+
return {
13+
"type": "object",
14+
"properties": {
15+
"base": {"type": "string"},
16+
"title": {"type": "string"},
17+
"bodyFrom": {"type": "string"},
18+
},
19+
"required": ["base", "title"],
20+
"additionalProperties": False,
21+
}
22+
1123
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
1224
gh = kw.get("github")
1325
tm = kw.get("tm")

‎orchestrator/engine/handlers/mcp.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@
88
class MCPCallHandler(NodeHandler):
99
name = "mcp.call"
1010

11+
def schema(self) -> Dict[str, Any]:
12+
return {
13+
"type": "object",
14+
"properties": {
15+
"server": {"type": "string"},
16+
"tool": {"type": "string"},
17+
"arguments": {"type": "object"},
18+
},
19+
"required": ["server", "tool"],
20+
"additionalProperties": False,
21+
}
22+
1123
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
1224
clients: Dict[str, Any] = kw.get("mcp_clients", {})
1325
server = str(args.get("server"))

‎orchestrator/engine/handlers/odoo.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
class OdooToolHandler(NodeHandler):
99
name = "odoo.call"
1010

11+
def schema(self) -> Dict[str, Any]:
12+
return {
13+
"type": "object",
14+
"properties": {
15+
"tool": {"type": "string"},
16+
"args": {"type": "object"},
17+
},
18+
"required": ["tool"],
19+
"additionalProperties": False,
20+
}
21+
1122
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
1223
odoo = kw.get("odoo")
1324
assert odoo is not None, "Odoo adapter missing"

‎orchestrator/engine/handlers/tm.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
class TMDecisionHandler(NodeHandler):
99
name = "tm.log_decision"
1010

11+
def schema(self) -> Dict[str, Any]:
12+
return {
13+
"type": "object",
14+
"properties": {"note": {"type": "string"}},
15+
"required": ["note"],
16+
"additionalProperties": False,
17+
}
18+
1119
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
1220
tm = kw.get("tm")
1321
assert tm is not None, "TM adapter missing"
@@ -19,6 +27,14 @@ def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecRes
1927
class TMSetStatusHandler(NodeHandler):
2028
name = "tm.set_status"
2129

30+
def schema(self) -> Dict[str, Any]:
31+
return {
32+
"type": "object",
33+
"properties": {"status": {"type": "string"}},
34+
"required": ["status"],
35+
"additionalProperties": False,
36+
}
37+
2238
def execute(self, *, args: Dict[str, Any], ctx: Dict[str, Any], **kw) -> ExecResult:
2339
tm = kw.get("tm")
2440
assert tm is not None, "TM adapter missing"

‎orchestrator/engine/registry.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ def register(self, handler: NodeHandler) -> None:
3434
def resolve(self, name: str) -> Optional[NodeHandler]:
3535
return self._by_name.get(name)
3636

37+
def validate_args(self, handler: NodeHandler, args: Dict[str, Any]) -> None:
38+
try:
39+
import jsonschema # type: ignore
40+
except Exception as e: # pragma: no cover
41+
raise RuntimeError("jsonschema is required for handler validation") from e
42+
schema = handler.schema() or {"type": "object"}
43+
jsonschema.validate(instance=args, schema=schema)
44+
3745
def route_compat(self, step: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
3846
"""Map legacy 'uses' step to a handler name + args.
3947

‎orchestrator/main.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ def validate_recipe(
184184
if not reg.resolve(h):
185185
typer.secho(f"step {i}: handler {h} not found", fg=typer.colors.RED)
186186
ok = False
187+
else:
188+
# Validate args against handler schema
189+
_, args = reg.route_compat(step)
190+
reg.validate_args(reg.resolve(h), args) # type: ignore[arg-type]
187191
except Exception as e:
188192
typer.secho(f"step {i}: {e}", fg=typer.colors.RED)
189193
ok = False

0 commit comments

Comments
 (0)