Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions strix/tools/agents_graph/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,13 +350,13 @@ async def wait_for_message( # noqa: PLR0911
)


@function_tool(timeout=120)
@function_tool(timeout=120, strict_mode=False)
async def create_agent(
ctx: RunContextWrapper,
name: str,
task: str,
inherit_context: bool = True,
skills: list[str] | None = None,
skills: str | list[str] | None = None,
) -> str:
"""Spawn a specialist child agent to run in parallel.

Expand Down Expand Up @@ -419,6 +419,23 @@ async def create_agent(
default=str,
)

# Tolerate LLM providers that pass array params as JSON-encoded strings.
# Validate decoded shape: reject anything that isn't a list of strings.
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
Comment on lines +424 to +429

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Decoded JSON Bypasses List Shape

When a provider sends valid JSON that is not a string array, json.loads() still succeeds. A JSON string is expanded into individual characters by list(skills), an object is treated as its keys, and a mixed array can raise inside validate_requested_skills() while formatting heterogeneous values, so create_agent can misapply skills or fail instead of returning a controlled validation error.

Suggested change
if isinstance(skills, str):
try:
skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/tools/agents_graph/tools.py
Line: 423-427

Comment:
**Decoded JSON Bypasses List Shape**

When a provider sends valid JSON that is not a string array, `json.loads()` still succeeds. A JSON string is expanded into individual characters by `list(skills)`, an object is treated as its keys, and a mixed array can raise inside `validate_requested_skills()` while formatting heterogeneous values, so `create_agent` can misapply skills or fail instead of returning a controlled validation error.

```suggestion
    if isinstance(skills, str):
        original_skills = skills
        try:
            decoded_skills = json.loads(skills)
        except json.JSONDecodeError:
            skills = [s.strip() for s in skills.split(",") if s.strip()]
        else:
            if isinstance(decoded_skills, str):
                skills = [decoded_skills]
            elif isinstance(decoded_skills, list) and all(
                isinstance(skill, str) for skill in decoded_skills
            ):
                skills = decoded_skills
            else:
                skills = [original_skills]
```

How can I resolve this? If you propose a fix, please make it concise.

else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
skill_list = list(skills or [])
skill_error = validate_requested_skills(skill_list)
if skill_error:
Expand Down
23 changes: 21 additions & 2 deletions strix/tools/load_skill/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

from __future__ import annotations

import json

from agents import RunContextWrapper, function_tool

from strix.skills import load_skills, validate_requested_skills


@function_tool(timeout=10)
async def load_skill(ctx: RunContextWrapper, skills: list[str]) -> str:
@function_tool(timeout=10, strict_mode=False)
async def load_skill(ctx: RunContextWrapper, skills: str | list[str]) -> str:
"""Return the markdown body of one or more skills as reference material.

Use this when you need exact syntax / workflow / payload guidance
Expand All @@ -25,6 +27,23 @@ async def load_skill(ctx: RunContextWrapper, skills: list[str]) -> str:
``strix/skills/<category>/<name>.md``.
"""
del ctx
# Tolerate LLM providers that pass array params as JSON-encoded strings.
# Validate decoded shape: reject anything that isn't a list of strings.
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
Comment on lines +32 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Decoded JSON Bypasses List Shape

When skills contains valid non-array JSON, decoding succeeds without enforcing the downstream list[str] contract. A JSON string is split into characters, an object silently becomes its keys, and mixed element types can make validation raise, so load_skill can load the wrong selection or terminate the tool call unexpectedly.

Suggested change
if isinstance(skills, str):
try:
skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
if isinstance(skills, str):
original_skills = skills
try:
decoded_skills = json.loads(skills)
except json.JSONDecodeError:
skills = [s.strip() for s in skills.split(",") if s.strip()]
else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
Prompt To Fix With AI
This is a comment left during a code review.
Path: strix/tools/load_skill/tool.py
Line: 31-35

Comment:
**Decoded JSON Bypasses List Shape**

When `skills` contains valid non-array JSON, decoding succeeds without enforcing the downstream `list[str]` contract. A JSON string is split into characters, an object silently becomes its keys, and mixed element types can make validation raise, so `load_skill` can load the wrong selection or terminate the tool call unexpectedly.

```suggestion
    if isinstance(skills, str):
        original_skills = skills
        try:
            decoded_skills = json.loads(skills)
        except json.JSONDecodeError:
            skills = [s.strip() for s in skills.split(",") if s.strip()]
        else:
            if isinstance(decoded_skills, str):
                skills = [decoded_skills]
            elif isinstance(decoded_skills, list) and all(
                isinstance(skill, str) for skill in decoded_skills
            ):
                skills = decoded_skills
            else:
                skills = [original_skills]
```

How can I resolve this? If you propose a fix, please make it concise.

else:
if isinstance(decoded_skills, str):
skills = [decoded_skills]
elif isinstance(decoded_skills, list) and all(
isinstance(skill, str) for skill in decoded_skills
):
skills = decoded_skills
else:
skills = [original_skills]
requested = list(skills or [])
err = validate_requested_skills(requested)
if err:
Expand Down