UAT: SkillService._schema_to_skill_dict drops inline tool capability fields — writes, checkpointable, side_effects, and input_schema are silently lost during skill registration #2400

Open
opened 2026-04-03 17:33:01 +00:00 by freemo · 1 comment
Owner

Metadata

  • Branch: fix/skill-service-inline-tool-capability-mapping
  • Commit Message: fix(skills): populate ToolCapability from inline tool schema fields in SkillService._schema_to_skill_dict
  • Milestone: v3.6.0
  • Parent Epic: #400

Description

When a skill is registered via agents skill add --config <file>, the SkillService._schema_to_skill_dict() method converts the SkillConfigSchema to a dict for Skill.from_config(). However, it only copies description, source, and code for inline tools — it silently drops writes, checkpointable, side_effects, and input_schema fields.

What was tested

  • Code analysis of src/cleveragents/application/services/skill_service.py lines 220-230 (_schema_to_skill_dict)
  • Runtime test: registered a skill with writes: true inline tool, verified the domain model has capability=None

Expected behavior (from spec)

The spec defines inline tools with writes, checkpointable, and side_effects fields. These should be preserved when a skill is registered from YAML. The SkillInlineTool.capability field should be populated with a ToolCapability object reflecting these values.

Actual behavior

# YAML config:
# inline_tools:
#   - name: write-tool
#     source: custom
#     code: print(1)
#     writes: true
#     checkpointable: true

config = SkillConfigSchema.from_yaml(yaml_str)
print(config.inline_tools[0].writes)         # True ✓
print(config.inline_tools[0].checkpointable) # True ✓

result = SkillService._schema_to_skill_dict(config)
print(result['anonymous_tools'])
# [{'description': 'write-tool', 'source': 'custom', 'code': 'print(1)'}]
# writes, checkpointable, side_effects, input_schema are ALL MISSING

svc = SkillService()
svc.add_skill(config)
skill = svc.get_skill('local/test')
print(skill.anonymous_tools[0].capability)  # None — capability is lost!

Security impact

Because capability=None, the InlineToolExecutor.execute() write guard check (if tool.capability is not None and tool.capability.writes) is never triggered. This means write-capable inline tools loaded from YAML can bypass read-only context enforcement — a security violation.

Code location

src/cleveragents/application/services/skill_service.py, method _schema_to_skill_dict(), lines ~220-230:

if config.inline_tools:
    data["anonymous_tools"] = []
    for it in config.inline_tools:
        tool_dict: dict[str, Any] = {
            "description": it.description or it.name,
            "source": it.source,
            "code": it.code,
        }
        # BUG: writes, checkpointable, side_effects, input_schema are NOT included
        data["anonymous_tools"].append(tool_dict)

Fix required

The conversion must include writes, checkpointable, side_effects, and input_schema fields, and construct a ToolCapability object when writes or checkpointable is True.

Steps to reproduce

  1. Create a skill YAML with an inline tool that has writes: true
  2. Register it via SkillService.add_skill()
  3. Retrieve the skill and check skill.anonymous_tools[0].capability — it will be None
  4. Execute the inline tool in a read-only context — it will succeed (bypassing the write guard)

Subtasks

  • Audit SkillService._schema_to_skill_dict() to identify all fields dropped during inline tool conversion
  • Update _schema_to_skill_dict() to include writes, checkpointable, side_effects, and input_schema fields in the tool_dict for each inline tool
  • Construct a ToolCapability object from the schema fields and include it in the dict when writes or checkpointable is True
  • Verify Skill.from_config() correctly deserialises the ToolCapability from the dict (fix if needed)
  • Tests (Behave): Add scenario — register skill with writes: true inline tool, assert capability.writes is True on retrieved skill
  • Tests (Behave): Add scenario — execute write-capable inline tool in read-only context after YAML registration, assert write guard is triggered
  • Tests (Behave): Add scenario — side_effects and input_schema fields are preserved end-to-end through registration
  • Update docstring on _schema_to_skill_dict() to document all fields it maps
  • Run nox -e typecheck to confirm no type regressions introduced by the fix

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly:
    fix(skills): populate ToolCapability from inline tool schema fields in SkillService._schema_to_skill_dict
  • The commit is pushed to the remote on the branch matching the Branch in Metadata exactly:
    fix/skill-service-inline-tool-capability-mapping
  • The commit is submitted as a pull request to master, reviewed, and merged.
  • skill.anonymous_tools[0].capability is no longer None for skills registered from YAML with writes: true or checkpointable: true.
  • The write guard in InlineToolExecutor.execute() is correctly triggered for write-capable inline tools loaded from YAML in a read-only context.
  • All nox stages pass (nox -e lint, nox -e typecheck, nox -e unit_tests, nox -e integration_tests).
  • Coverage >= 97%

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `fix/skill-service-inline-tool-capability-mapping` - **Commit Message**: `fix(skills): populate ToolCapability from inline tool schema fields in SkillService._schema_to_skill_dict` - **Milestone**: v3.6.0 - **Parent Epic**: #400 ## Description When a skill is registered via `agents skill add --config <file>`, the `SkillService._schema_to_skill_dict()` method converts the `SkillConfigSchema` to a dict for `Skill.from_config()`. However, it only copies `description`, `source`, and `code` for inline tools — it silently drops `writes`, `checkpointable`, `side_effects`, and `input_schema` fields. ### What was tested - Code analysis of `src/cleveragents/application/services/skill_service.py` lines 220-230 (`_schema_to_skill_dict`) - Runtime test: registered a skill with `writes: true` inline tool, verified the domain model has `capability=None` ### Expected behavior (from spec) The spec defines inline tools with `writes`, `checkpointable`, and `side_effects` fields. These should be preserved when a skill is registered from YAML. The `SkillInlineTool.capability` field should be populated with a `ToolCapability` object reflecting these values. ### Actual behavior ```python # YAML config: # inline_tools: # - name: write-tool # source: custom # code: print(1) # writes: true # checkpointable: true config = SkillConfigSchema.from_yaml(yaml_str) print(config.inline_tools[0].writes) # True ✓ print(config.inline_tools[0].checkpointable) # True ✓ result = SkillService._schema_to_skill_dict(config) print(result['anonymous_tools']) # [{'description': 'write-tool', 'source': 'custom', 'code': 'print(1)'}] # writes, checkpointable, side_effects, input_schema are ALL MISSING svc = SkillService() svc.add_skill(config) skill = svc.get_skill('local/test') print(skill.anonymous_tools[0].capability) # None — capability is lost! ``` ### Security impact Because `capability=None`, the `InlineToolExecutor.execute()` write guard check (`if tool.capability is not None and tool.capability.writes`) is never triggered. This means write-capable inline tools loaded from YAML can **bypass read-only context enforcement** — a security violation. ### Code location `src/cleveragents/application/services/skill_service.py`, method `_schema_to_skill_dict()`, lines ~220-230: ```python if config.inline_tools: data["anonymous_tools"] = [] for it in config.inline_tools: tool_dict: dict[str, Any] = { "description": it.description or it.name, "source": it.source, "code": it.code, } # BUG: writes, checkpointable, side_effects, input_schema are NOT included data["anonymous_tools"].append(tool_dict) ``` ### Fix required The conversion must include `writes`, `checkpointable`, `side_effects`, and `input_schema` fields, and construct a `ToolCapability` object when `writes` or `checkpointable` is True. ### Steps to reproduce 1. Create a skill YAML with an inline tool that has `writes: true` 2. Register it via `SkillService.add_skill()` 3. Retrieve the skill and check `skill.anonymous_tools[0].capability` — it will be `None` 4. Execute the inline tool in a read-only context — it will succeed (bypassing the write guard) ## Subtasks - [ ] Audit `SkillService._schema_to_skill_dict()` to identify all fields dropped during inline tool conversion - [ ] Update `_schema_to_skill_dict()` to include `writes`, `checkpointable`, `side_effects`, and `input_schema` fields in the `tool_dict` for each inline tool - [ ] Construct a `ToolCapability` object from the schema fields and include it in the dict when `writes` or `checkpointable` is `True` - [ ] Verify `Skill.from_config()` correctly deserialises the `ToolCapability` from the dict (fix if needed) - [ ] Tests (Behave): Add scenario — register skill with `writes: true` inline tool, assert `capability.writes is True` on retrieved skill - [ ] Tests (Behave): Add scenario — execute write-capable inline tool in read-only context after YAML registration, assert write guard is triggered - [ ] Tests (Behave): Add scenario — `side_effects` and `input_schema` fields are preserved end-to-end through registration - [ ] Update docstring on `_schema_to_skill_dict()` to document all fields it maps - [ ] Run `nox -e typecheck` to confirm no type regressions introduced by the fix ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly: `fix(skills): populate ToolCapability from inline tool schema fields in SkillService._schema_to_skill_dict` - The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly: `fix/skill-service-inline-tool-capability-mapping` - The commit is submitted as a **pull request** to `master`, reviewed, and **merged**. - `skill.anonymous_tools[0].capability` is no longer `None` for skills registered from YAML with `writes: true` or `checkpointable: true`. - The write guard in `InlineToolExecutor.execute()` is correctly triggered for write-capable inline tools loaded from YAML in a read-only context. - All nox stages pass (`nox -e lint`, `nox -e typecheck`, `nox -e unit_tests`, `nox -e integration_tests`). - Coverage >= 97% --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-new-issue-creator
freemo added this to the v3.6.0 milestone 2026-04-03 17:33:06 +00:00
Author
Owner

Issue triaged by project owner:

  • State: Verified
  • Priority: High — Inline tool capability fields (writes, checkpointable, side_effects, input_schema) being silently lost during skill registration means the runtime cannot make correct decisions about tool safety and capabilities.
  • Milestone: v3.6.0 (as specified in issue metadata)
  • MoSCoW: Should Have — While tool capabilities are important for safety and correctness, the tools still function without these metadata fields. This should be fixed before production use.
  • Parent Epic: #400 (Post-MVP Security)

The issue is well-described with clear code references and a definition of done. Valid and actionable.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: ca-project-owner

Issue triaged by project owner: - **State**: Verified - **Priority**: High — Inline tool capability fields (`writes`, `checkpointable`, `side_effects`, `input_schema`) being silently lost during skill registration means the runtime cannot make correct decisions about tool safety and capabilities. - **Milestone**: v3.6.0 (as specified in issue metadata) - **MoSCoW**: Should Have — While tool capabilities are important for safety and correctness, the tools still function without these metadata fields. This should be fixed before production use. - **Parent Epic**: #400 (Post-MVP Security) The issue is well-described with clear code references and a definition of done. Valid and actionable. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: ca-project-owner
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Blocks
#400 Epic: Post-MVP Security
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core#2400
No description provided.