Skip to content
Closed
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
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ per-app `features.external_url_fetch.enabled` field. The deployment-attachment p

### Skills

Skills are reusable instruction modules. Predefined skills are loaded at startup from `config/predefined/skills/`.
DIAL prompt skills (`dial_prompt_skills/`) are fetched at request time from DIAL Core's prompts API.
`SkillsRegistry` merges both sources per request.
Skills are reusable instruction modules. Three sources: predefined skills loaded at startup from
`config/predefined/skills/`; DIAL prompt skills (`dial_prompt_skills/`) fetched per request from Core's prompts API;
and DIAL skill resources (`dial_skills/`) fetched per request from Core's `/v2/skills` API — a folder with `SKILL.md`
plus bundled text files the agent reads on demand via `read_skill(skill_name, file_path)`.
`SkillsRegistry` merges all three per request and owns precedence (predefined > dial-prompt > dial-skill).

### Configuration Model

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ Controls which tool-execution stages are surfaced in the DIAL UI for each app. S
| `EXTERNAL_URL_FETCH_HOST_ALLOWLIST` | — | No | Comma-separated allowlist of host patterns for external URL fetches. Unset (default) means no admin-level host restriction. Patterns: exact host (`example.com`) or `*.example.com` for any subdomain. Re-checked on every redirect hop. Per-app `features.external_url_fetch.host_allowlist` narrows further (intersection) but never expands. |
| `EXTERNAL_URL_FETCH_MAX_REDIRECTS` | `5` | No | Maximum HTTP redirects on external URL fetches. Each hop is SSRF-checked. Hard ceiling 10. |
| `EXTERNAL_URL_FETCH_CONNECT_TIMEOUT_SECONDS` | `5.0` | No | TCP connect timeout (seconds) for external URL fetches. Read/write/pool timeouts use the resolved tool timeout. |
| **Skills** | | | |
| `DIAL_SKILLS_FILE_MAX_BYTES` | `262144` | No | Cap on a single file read from a DIAL skill resource, `SKILL.md` included. Must exceed the largest manifest you expect: an over-cap manifest drops the skill. See [docs/skills.md](docs/skills.md). |
| `DIAL_SKILLS_MAX_FILES` | `200` | No | Maximum bundled files advertised to the agent per DIAL skill resource; beyond it the listing is truncated |
| `DIAL_SKILLS_LISTING_MAX_PAGES` | `10` | No | Maximum file-listing pages followed per DIAL skill resource, bounding a server-supplied cursor |
| **Feature Gating** | | | |
| `ENABLE_PREVIEW_FEATURES` | `false` | No | Enable preview features across the deployment (schema visibility + runtime activation) |
| **Templates** | | | |
Expand Down
533 changes: 533 additions & 0 deletions docs/designs/skill_invocation.md

Large diffs are not rendered by default.

494 changes: 494 additions & 0 deletions docs/designs/skills_as_dial_resource.md

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion docs/generated-app-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,29 @@
"title": "DialPromptSkillConfig",
"type": "object"
},
"DialSkillConfig": {
"properties": {
"type": {
"const": "dial-skill",
"default": "dial-skill",
"description": "Skill sourced from a DIAL skill resource (folder with SKILL.md).",
"title": "Type",
"type": "string"
},
"url": {
"description": "Relative skill resource URL in DIAL (e.g. skills/<bucket>/<path>)",
"dial:resource": true,
"title": "Url",
"type": "string"
}
},
"required": [
"url"
],
"title": "DialSkillConfig",
"type": "object",
"x-preview": true
},
"DialSystemPromptConfig": {
"properties": {
"type": {
Expand Down Expand Up @@ -3743,13 +3766,17 @@
"items": {
"discriminator": {
"mapping": {
"dial-prompt": "#/$defs/DialPromptSkillConfig"
"dial-prompt": "#/$defs/DialPromptSkillConfig",
"dial-skill": "#/$defs/DialSkillConfig"
},
"propertyName": "type"
},
"oneOf": [
{
"$ref": "#/$defs/DialPromptSkillConfig"
},
{
"$ref": "#/$defs/DialSkillConfig"
}
]
},
Expand Down
13 changes: 1 addition & 12 deletions docs/generated-config-support-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,7 @@
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/DialPromptSkillConfig"
}
],
"title": "Config",
"discriminator": {
"propertyName": "type",
"mapping": {
"dial-prompt": "#/components/schemas/DialPromptSkillConfig"
}
}
"$ref": "#/components/schemas/DialPromptSkillConfig"
}
}
},
Expand Down
12 changes: 12 additions & 0 deletions docs/generated-internal-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,18 @@
},
"type": "string",
"description": "The name of the skill to read. This should match the name from the available_skills list."
},
"file_path": {
"display": {
"stage": {
"ignore": true,
"ignore_parameter_name": false,
"show_value_in_stage_title": false,
"order": 0
}
},
"type": "string",
"description": "Optional. Path of a file bundled with the skill, relative to the skill root, written exactly as listed in that skill's <skill_files> block (e.g. references/api-schema.md). Omit it to read the skill's instructions."
}
},
"required": [
Expand Down
119 changes: 115 additions & 4 deletions docs/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ named directories. They are automatically loaded and made available to the agent

## How Skills Work

Skills come from three sources, merged per request:

| Source | Origin | Bundled files |
|---|---|---|
| Predefined | `config/predefined/skills/`, loaded at startup | No |
| [DIAL prompt](#dial-prompt-skills) | `prompts/<bucket>/<path>`, per request | No |
| [DIAL skill resource](#dial-skill-resources) | `skills/<bucket>/<path>`, per request | Yes, text files |

- Skills are loaded from `config/predefined/skills/` at startup. Each skill lives in its own subdirectory
(e.g. `skills/my-skill/SKILL.md`).
- Each skill is presented to the agent as XML metadata in the system prompt.
Expand Down Expand Up @@ -114,9 +122,9 @@ optional features. The table below summarises what is and isn't supported.
| `name` validation (length, charset, consecutive hyphens) | Supported | |
| `description`, `license`, `compatibility`, `metadata` | Supported | |
| `allowed-tools` | Partial | Exposed in XML metadata but **not enforced** at runtime. |
| Optional subdirectories (`scripts/`, `references/`, `assets/`) | Not supported | Only `SKILL.md` is read; other directory contents are ignored. |
| Progressive disclosure (on-demand file references) | Not supported | The agent can read `SKILL.md` content via `read_skill` but cannot access referenced files within the skill directory. |
| Dynamic skill registration | Not supported | Skills are loaded once at startup; adding or modifying skills requires a restart. |
| Optional subdirectories (`scripts/`, `references/`, `assets/`) | Partial | Supported for [DIAL skill resources](#dial-skill-resources) (text files only). Predefined and DIAL-prompt skills read `SKILL.md` alone. |
| Progressive disclosure (on-demand file references) | Partial | Supported for [DIAL skill resources](#dial-skill-resources) via `read_skill(skill_name, file_path)`. Not available for the other two sources. |
| Dynamic skill registration | Partial | DIAL-prompt and DIAL-skill sources are resolved fresh per request. Predefined skills are loaded once at startup; adding or modifying them requires a restart. |

For the full specification, see [agentskills.io/specification](https://agentskills.io/specification).
For design rationale and known limitations, see [the design doc](designs/skills_and_file_transfer.md).
Expand Down Expand Up @@ -177,12 +185,115 @@ skill takes precedence**. The DIAL prompt skill is skipped and a warning is logg
- DIAL prompts are single text documents — they cannot contain `scripts/`, `references/`, or `assets/`
subdirectories.
- DIAL prompts are fetched fresh on each request (no cross-request caching).
- The `skills` config field is a **preview feature** — it requires `ENABLE_PREVIEW_FEATURES=true`.
- A prompt cannot bundle files. Use a [DIAL skill resource](#dial-skill-resources) when the skill
needs reference material the agent can open on demand.

For design details, see [the design doc](designs/dial_prompts_as_skills.md).

---

## DIAL Skill Resources

DIAL Core stores skills as **folder-shaped resources**: a mandatory `SKILL.md` plus an arbitrary file
hierarchy, served through the `/v2/skills` API. Unlike a DIAL prompt, such a skill can bundle the
reference material its manifest points at, and the agent reads those files **on demand**.

### Configuration

```json
{
"skills": [
{
"type": "dial-skill",
"url": "skills/<bucket>/<path>"
}
]
}
```

The `url` is a relative path including the `skills/` resource type prefix
(e.g. `skills/my-bucket/refund-policy`), following the same convention as `dial-prompt` and file
context URLs.

### Progressive Disclosure

At request time QuickApps reads the skill's `SKILL.md` and lists its bundled files. The file list is
appended to the manifest as a `<skill_files>` block, so the agent sees it the moment it calls
`read_skill`:

```markdown
---
name: refund-policy
description: How to handle refund requests, by region.
---

# Refund Policy

Determine the customer's region, then read the matching reference file.

<skill_files>
references/eu-rules.md
references/us-rules.md
</skill_files>
```

The agent then opens one with `read_skill(skill_name="refund-policy", file_path="references/eu-rules.md")`
— one request to DIAL Core per file, and only for files it actually asks for. Repeat reads within a
request are served from memory.

A path is readable **only if it appears in that skill's `<skill_files>` block**. Anything else — a
traversal attempt, a hidden file, a path the model invented — is refused, and the error hands the
inventory back so the agent can correct itself.

### Which Files Are Advertised

A bundled file is listed and readable when it is a regular file whose extension is one of
`.md`, `.markdown`, `.txt`, `.json`, `.yaml`, `.yml`, `.csv`, `.tsv`, `.xml`, `.html`, `.toml`,
`.ini`, `.sql`, `.py`, `.sh`, `.js`, `.ts`.

Excluded: subfolders, hidden entries at any depth (including Core's own `.dial-resource` marker),
`SKILL.md` itself (already returned by `read_skill` without a `file_path`), and everything binary —
images, PDFs and other assets are **not** available in this release.

### Limits

| Variable | Default | Purpose |
|---|---|---|
| `DIAL_SKILLS_FILE_MAX_BYTES` | `262144` | Cap on a single file read, `SKILL.md` included. |
| `DIAL_SKILLS_MAX_FILES` | `200` | Maximum files advertised per skill. |
| `DIAL_SKILLS_LISTING_MAX_PAGES` | `10` | Maximum listing pages followed per skill. |

An over-cap `SKILL.md` drops the skill; an over-cap bundled file fails that one read. Files must be
valid UTF-8.

### Name Collision

Precedence is **predefined > dial-prompt > dial-skill**, and first configured wins within a source.
A skill that loses a collision is skipped and reported in the initialization issues stage.

### Error Handling

- **Inaccessible skill** (403, 404): skipped with a reported reason; other skills stay available.
- **Invalid `SKILL.md`**: skipped, same as a DIAL prompt skill.
- **File listing fails**: the skill is still loaded, without its bundled files, and a warning is
reported.
- **DIAL Core outage**: all DIAL skills are dropped and the request is served with the remaining
sources.

### Limitations

- **Access**: DIAL Core does not yet auto-share config-declared skills to the application's
per-request key. A `dial-skill` therefore resolves only when the caller's own key already has
access — a skill in the user's own bucket, one shared with them, or a published one.
- Skills are fetched fresh on each request (no cross-request caching).
- Binary and asset files are not readable.
- Validation of a `dial-skill` URL is not offered by `/skills/validate`: DIAL Core validates
`SKILL.md` when the skill is written, so a stored skill is already valid.

For design details, see [the design doc](designs/skills_as_dial_resource.md).

---

## Migrating from Agent Instructions

The `config/predefined/instructions/` directory convention and `AgentInstructionsProvider` have been removed. The skills
Expand Down
10 changes: 5 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ authors = [
]
dependencies = [
# Core framework & DI
"aidial-client (>=0.16.0,<0.17.0)", # DIAL API client
"aidial-client (>=0.17.0,<0.18.0)", # DIAL API client
"aidial-sdk[telemetry]>=0.38.0,<0.39.0", # DIAL integration SDK
"pydantic>=2.12.4,<3.0.0", # Data validation
"pydantic-settings>=2.14.2,<3.0.0", # Settings management
Expand Down
2 changes: 2 additions & 0 deletions src/quickapp/app_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from quickapp.dial_deployment_tooling import DialDeploymentToolingModule
from quickapp.dial_files_tooling.dial_files_tooling_module import DialFilesToolingModule
from quickapp.dial_prompt_skills.dial_prompt_skills_module import DialPromptSkillsModule
from quickapp.dial_skills.dial_skills_module import DialSkillsModule
from quickapp.file_transfer import FileTransferModule
from quickapp.internal_tooling.internal_tooling_module import InternalToolModule
from quickapp.mcp_tooling import MCPToolingModule
Expand Down Expand Up @@ -58,6 +59,7 @@ def build_di_modules() -> list[Module]:
LazyOnDemandStrategyModule(),
SkillsModule(),
DialPromptSkillsModule(),
DialSkillsModule(),
TimestampModule(),
AgentHooksModule(),
DialFilesToolingModule(),
Expand Down
18 changes: 16 additions & 2 deletions src/quickapp/config/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from pydantic import BaseModel, Field

from quickapp.common.base_config import DialResourceConfigField
from quickapp.common.base_config import DialResourceConfigField, preview_model


class DialPromptSkillConfig(BaseModel):
Expand All @@ -18,7 +18,21 @@ class DialPromptSkillConfig(BaseModel):
]


@preview_model
class DialSkillConfig(BaseModel):
type: Literal["dial-skill"] = Field(
default="dial-skill",
description="Skill sourced from a DIAL skill resource (folder with SKILL.md).",
)
url: Annotated[
str,
DialResourceConfigField(
description="Relative skill resource URL in DIAL (e.g. skills/<bucket>/<path>)"
),
]


SkillConfig = Annotated[
DialPromptSkillConfig,
DialPromptSkillConfig | DialSkillConfig,
Field(discriminator="type"),
]
16 changes: 8 additions & 8 deletions src/quickapp/configuration_support/_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from quickapp.common.dial_settings import DialSettings
from quickapp.config.application import ApplicationConfig
from quickapp.config.skill import DialPromptSkillConfig, SkillConfig
from quickapp.config.skill import DialPromptSkillConfig
from quickapp.dial_prompt_skills._dial_prompt_skill_resolver import (
fetch_and_validate_dial_prompt_skill,
)
Expand Down Expand Up @@ -47,14 +47,14 @@ async def get_default_configuration() -> dict[str, Any]:
async def get_skills() -> list[SkillMetadata]:
return self.__skills_provider.get_all_skills()

# Deliberately typed to DialPromptSkillConfig rather than the SkillConfig
# union: a DIAL skill resource is created and validated by Core (a stored
# skill already has a valid SKILL.md), so there is nothing for this
# endpoint to add for `dial-skill`. Keeping the union here would publish
# an OpenAPI that advertises a request this handler cannot serve.
@app.post(CONFIG_SUPPORT_URI + "/skills/validate", response_model=SkillMetadata)
async def validate_skill(config: SkillConfig, request: Request) -> SkillMetadata:
if isinstance(config, DialPromptSkillConfig):
return await self._validate_dial_prompt_skill(config, request)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported skill type: {config.type}",
)
async def validate_skill(config: DialPromptSkillConfig, request: Request) -> SkillMetadata:
return await self._validate_dial_prompt_skill(config, request)

async def _validate_dial_prompt_skill(
self,
Expand Down
12 changes: 11 additions & 1 deletion src/quickapp/dial_files_tooling/dial_files_tooling_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
INTERNAL_FILE_READ_LINES_TOOL_NAME,
INTERNAL_FILE_SEARCH_TOOL_NAME,
INTERNAL_FILE_TOOL_NAME_PREFIX,
INTERNAL_SKILLS_READ_SKILL_TOOL_NAME,
)
from quickapp.config.application import ApplicationConfig
from quickapp.config.dial_files import DialFilesConfig
Expand Down Expand Up @@ -75,8 +76,17 @@ def configure(self, binder: Binder) -> None:
# from offload, regardless of config: a large read-back slice must never be
# re-offloaded (infinite recursion). This guard cannot be removed via the
# per-app / env-var `excluded_tools`, which is additive on top of it.
#
# `read_skill` joins them for a different reason: its result can carry the
# manifest's `<skill_files>` inventory, which progressive disclosure depends
# on. Offloading it into a DIAL-file pointer would strip that inventory from
# the tool result the model actually sees.
_MANDATORY_EXCLUDED_TOOLS = frozenset(
{INTERNAL_FILE_READ_LINES_TOOL_NAME, INTERNAL_FILE_SEARCH_TOOL_NAME}
{
INTERNAL_FILE_READ_LINES_TOOL_NAME,
INTERNAL_FILE_SEARCH_TOOL_NAME,
INTERNAL_SKILLS_READ_SKILL_TOOL_NAME,
}
)

@request_scope
Expand Down
Loading
Loading