fix(skills): validate skill names and quote provisioning paths
Skill names become host/guest path segments interpolated into the
`bottle.exec` shell strings in each contrib provider's provision_skills.
They were validated only as strings, so a name with shell metacharacters
or path traversal could reach the command.
Layer two defenses:
- Primary: reject any skill name that isn't kebab-case
([a-z][a-z0-9-]*) at manifest load, reusing the convention already
enforced on bottle/agent filenames (new is_valid_entity_name helper
in manifest_schema). Fails loud and early, protecting every consumer
of the name — not just the exec call sites.
- Failsafe: shlex.quote the interpolated skills_dir / dst paths in the
claude, codex, and pi providers, so a future unvalidated field can't
inject shell metacharacters even if it bypasses the load-time check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NkwFXLFff9PYPy4wgVBJp9
This commit is contained in:
@@ -165,6 +165,22 @@ class TestAgentValidation(unittest.TestCase):
|
||||
with self.assertRaises(ManifestError):
|
||||
ManifestAgent.from_dict("a", {"skills": [5]}, set())
|
||||
|
||||
def test_skill_name_rejects_shell_metacharacters(self) -> None:
|
||||
# Skill names become host/guest path segments interpolated into
|
||||
# provisioning shell commands; anything outside kebab-case is
|
||||
# rejected at load so it can never reach a `bottle.exec` string.
|
||||
for bad in ("foo; rm -rf /", "../escape", "foo bar", "Foo", "-leading"):
|
||||
with self.assertRaises(ManifestError):
|
||||
ManifestAgent.from_dict("a", {"skills": [bad]}, set())
|
||||
|
||||
def test_skill_name_accepts_kebab_case(self) -> None:
|
||||
agent = ManifestAgent.from_dict(
|
||||
"a", {"skills": ["init-entry", "quality-eval", "skill0"]}, set()
|
||||
)
|
||||
self.assertEqual(
|
||||
agent.skills, ("init-entry", "quality-eval", "skill0")
|
||||
)
|
||||
|
||||
def test_prompt_not_string(self) -> None:
|
||||
with self.assertRaises(ManifestError):
|
||||
ManifestAgent.from_dict("a", {"prompt": 5}, set())
|
||||
|
||||
Reference in New Issue
Block a user