Skip to content

Commit bb7a581

Browse files
OriNachumclaude
andcommitted
fix: address PR review round 2 - symlink validation, name checks, stderr
1. Reject symlinks pointing outside scripts/ during discovery (security). 2. Validate skill names against tool-name rules (alphanumeric/hyphens/underscores, max 64 chars). 3. Include stderr in output on successful execution (preserves warnings). 4. Add SKILLS_* vars to .env.example for discoverability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent def9b81 commit bb7a581

2 files changed

Lines changed: 43 additions & 4 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ API_ADAPTER_PORT=8080
1111
STREAM_TIMEOUT=120.0
1212
HEARTBEAT_INTERVAL=15.0
1313

14+
# Agent Skills Configuration (optional, disabled by default)
15+
SKILLS_ENABLED=false
16+
SKILLS_DIR=./skills
17+
SKILLS_EXEC_TIMEOUT=30
18+
1419
# Logging Configuration (optional)
1520
LOG_LEVEL=INFO
1621
LOG_FILE_PATH=./log/api_adapter.log

src/open_responses_server/common/skill_manager.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99

1010
from .config import SKILLS_ENABLED, SKILLS_DIR, SKILLS_EXEC_TIMEOUT, logger
1111

12+
import re
13+
1214
# Maximum characters of SKILL.md instructions to include in tool description
1315
_MAX_INSTRUCTIONS_LENGTH = 2000
1416

17+
# Tool names must be alphanumeric + hyphens/underscores, max 64 chars
18+
_VALID_TOOL_NAME_RE = re.compile(r'^[a-zA-Z0-9_-]{1,64}$')
19+
1520

1621
def _parse_skill_md(content: str) -> Optional[dict]:
1722
"""Parse SKILL.md content into frontmatter dict + instructions body.
@@ -120,14 +125,35 @@ async def startup_skills(self) -> None:
120125
logger.warning(f"[SKILLS-STARTUP] Skipping '{entry.name}': invalid SKILL.md frontmatter")
121126
continue
122127

128+
if not _VALID_TOOL_NAME_RE.match(parsed["name"]):
129+
logger.warning(
130+
f"[SKILLS-STARTUP] Skipping '{entry.name}': "
131+
f"skill name '{parsed['name']}' contains invalid characters "
132+
f"(must be alphanumeric, hyphens, underscores, max 64 chars)"
133+
)
134+
continue
135+
123136
scripts_dir = entry / "scripts"
124137
references_dir = entry / "references"
125138

126139
available_scripts: List[str] = []
127140
if scripts_dir.is_dir():
141+
scripts_dir_resolved = scripts_dir.resolve()
128142
for script_file in sorted(scripts_dir.iterdir()):
129-
if script_file.is_file() and os.access(script_file, os.X_OK):
130-
available_scripts.append(script_file.name)
143+
if not script_file.is_file() or not os.access(script_file, os.X_OK):
144+
continue
145+
# Reject symlinks pointing outside scripts/
146+
try:
147+
resolved = script_file.resolve()
148+
except OSError:
149+
continue
150+
if not str(resolved).startswith(str(scripts_dir_resolved) + os.sep):
151+
logger.warning(
152+
f"[SKILLS-STARTUP] Skipping symlink '{script_file.name}' "
153+
f"in skill '{parsed['name']}': points outside scripts/"
154+
)
155+
continue
156+
available_scripts.append(script_file.name)
131157

132158
if not available_scripts:
133159
logger.warning(f"[SKILLS-STARTUP] Skill '{parsed['name']}' has no executable scripts in scripts/")
@@ -289,11 +315,19 @@ async def execute_skill_tool(self, tool_name: str, arguments: dict) -> str:
289315
logger.warning(f"[SKILLS-EXEC] {error_msg}")
290316
raise RuntimeError(error_msg)
291317

318+
# Include stderr warnings in output when script succeeds
319+
output = stdout_text
320+
if stderr_text:
321+
if output:
322+
output += "\n\n[stderr]\n" + stderr_text
323+
else:
324+
output = stderr_text
325+
292326
logger.info(
293327
f"[SKILLS-EXEC] Skill '{skill.name}' script '{script_name}' "
294-
f"completed successfully ({len(stdout_text)} chars output)"
328+
f"completed successfully ({len(output)} chars output)"
295329
)
296-
return stdout_text if stdout_text else "(no output)"
330+
return output if output else "(no output)"
297331

298332
except asyncio.TimeoutError:
299333
logger.error(

0 commit comments

Comments
 (0)