chore(openspec): archive skill-catalog-subscription
Change is complete (24/24 tasks) per its declared scope (read-only local catalog + MCP tools + sync client contract). Management UI and the upstream Subscription Platform were explicitly out of scope. Deltas synced into three new main specs: skill-catalog, skill-mcp-tools, skill-subscription-sync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-06
|
||||
@@ -0,0 +1,97 @@
|
||||
## Context
|
||||
|
||||
This builds on the `apex-agent-mvp` change (currently proposed, not yet applied), which establishes the Device/Driver/Perception/Agent-Runtime/MCP layers for Apex Agent. That change already defines an MCP Tool Server (`api/mcp.py`) exposing device capabilities; this change adds a parallel `skills/` package and new MCP tools onto the same server, without touching the device/perception/runtime code.
|
||||
|
||||
The key external constraint: skill **authoring, versioning, and entitlement** live in a separate, already-planned **Subscription Platform** (a distinct information-management system, out of scope here). Apex Agent is a **consumer** of that platform's catalog, not the source of truth for skill content. We only control: how Apex Agent stores what it has synced, how it exposes that to the LLM via MCP, and the contract it expects the Subscription Platform to satisfy.
|
||||
|
||||
Two kinds of Skill must be supported, per product direction:
|
||||
- **Knowledge skill**: free-form structured instructional content (markdown/text + metadata) the LLM reads to decide how to act — e.g. "tips for searching effectively on Xiaohongshu." The LLM still plans/executes via the existing `agent-runtime`/`tools` layer.
|
||||
- **Flow-template skill**: a predefined, parameterized sequence of capability calls (tap/swipe/input steps with placeholders, e.g. `{query}`), which the LLM mostly fills in and triggers rather than re-planning from scratch.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Define a single `Skill` data model general enough to represent both knowledge and flow-template skills with shared metadata (id, name, description, version, tags, subscription/source id).
|
||||
- Expose skills to the LLM through MCP tools that are indistinguishable in style from the existing device tools (semantic, driver/platform-agnostic).
|
||||
- Define a clear, minimal contract for what Apex Agent needs from the Subscription Platform: a way to fetch "skills I'm entitled to" and a way to learn about changes (poll or push), without dictating that platform's internal design.
|
||||
- Enforce subscription-based visibility so `list_skills`/`search_skills`/`get_skill` only ever surface skills the current deployment (tenant/device/agent) is actually subscribed to.
|
||||
- Let a flow-template skill's steps execute through the *existing* `tools/` functions from `apex-agent-mvp` (no new execution primitives) — a flow template is just a data-driven script over the same capability calls the Executor already uses.
|
||||
|
||||
**Non-Goals:**
|
||||
- Designing or implementing the Subscription Platform itself (its UI, billing, authoring workflow, or storage).
|
||||
- Automatic skill generation/authoring by the LLM.
|
||||
- A full permission/RBAC system — subscription visibility is scoped to "is this skill in my entitled set," not fine-grained per-user roles.
|
||||
- Real-time push infrastructure (websockets/queues) as a hard requirement — push is supported as an *optional* transport; polling sync must work standalone.
|
||||
- Executing flow-template skills with a new engine — they are interpreted by the existing `runtime/executor.py`, not a separate workflow engine.
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: `Skill` is a single model with a `kind` discriminator (`knowledge` | `flow_template`), not two unrelated types
|
||||
Both kinds share `SkillMetadata` (id, name, description, version, tags, source/subscription id, updated_at). A `KnowledgeSkill` carries `content` (markdown/text). A `FlowTemplateSkill` carries `steps` (ordered list of `{tool, args_template}`, where `args_template` values may contain `{param}` placeholders) and a `parameters` schema (name, type, required, description) used to validate/prompt for inputs before execution.
|
||||
- **Alternative considered**: Two entirely separate catalogs/tables/tool sets for knowledge vs. flow skills. Rejected — `list_skills`/`search_skills` would need to be duplicated, and most metadata (id/name/description/version/tags) is identical; a discriminated union keeps one catalog and one set of list/search tools while `get_skill` returns kind-specific content.
|
||||
|
||||
### D2: Skill Catalog is a local, read-mostly cache; Subscription Platform is always the source of truth
|
||||
`skills/catalog.py` stores the last-synced copy of entitled skills locally (for offline availability and low-latency MCP responses) but never treats local edits as authoritative — there is no "create/update skill" MCP tool or API in this change. All content changes flow one direction: Subscription Platform → sync client → local catalog.
|
||||
- **Alternative considered**: Let Apex Agent locally override/edit synced skills. Rejected — breaks the "another system manages skills" requirement from the proposal and creates drift/merge-conflict problems with no clear owner.
|
||||
|
||||
### D3: Sync client supports pull (poll) as the required baseline, push (webhook) as an optional accelerator
|
||||
`skills/sync_client.py` defines a `fetch_entitled_skills(subscription_id, since_version)` contract against the Subscription Platform's assumed API, called on a configurable interval (baseline). If the platform can also deliver change notifications (webhook/callback), an optional receiver triggers an immediate out-of-cycle fetch instead of waiting for the next poll — but correctness never depends on push arriving.
|
||||
- **Alternative considered**: Push-only (webhook-driven) sync. Rejected — makes Apex Agent's catalog freshness dependent on network reachability of an inbound webhook, which is operationally harder (firewalls/NAT) than Apex Agent making outbound poll requests; poll-as-baseline is strictly more deployable.
|
||||
|
||||
### D4: Subscription-based visibility is enforced at the catalog query layer, not just at sync time
|
||||
Even though sync only ever pulls "entitled" skills, `skills/catalog.py`'s query functions (used by both MCP tools and any future internal caller) re-check that a skill's subscription id is still in the caller's active subscription set at query time, not just at last-sync time. This guards against a stale local cache still containing a skill whose entitlement was revoked but not yet re-synced.
|
||||
- **Alternative considered**: Trust the local cache fully between syncs (no re-check at query time). Rejected — entitlement revocation should not have to wait for the next poll interval to stop being visible if the revocation is already known locally (e.g. via a push notification that only carries "removed" without full content).
|
||||
|
||||
### D5: Flow-template skills execute via existing `tools/`/`runtime/executor.py`, with a resolution step in between
|
||||
`skills/mcp_tools.py` exposes `get_skill` (returns metadata + content/template) and a parameter-resolution helper; it does **not** expose a `run_skill` tool that silently executes on the LLM's behalf. The LLM fetches the flow template, fills placeholders itself (or asks the user for missing required parameters), and then issues the already-existing device-capability tool calls (`tap`, `input_text`, etc.) from `apex-agent-mvp` in the order/values the template specifies.
|
||||
- **Alternative considered**: Add a single `run_skill_flow(skill_id, params)` MCP tool that executes the whole sequence server-side. Rejected for this change — it would bypass the Agent Runtime's Observe→Think→Act loop and Executor retry/wait logic from `apex-agent-mvp`, silently skipping Scene verification between steps. Keeping execution inside the existing loop (LLM issues each tool call itself, informed by the template) preserves the "AI decides based on what it currently observes" principle the whole platform is built on. A batched execution helper can be revisited later as a design decision in its own change if needed.
|
||||
|
||||
### D6: Reuse `skills_learning/` domain models and `workflow/skill_exec.py` resolver; do not duplicate them
|
||||
The archived `skill-learning-runtime` change already shipped `skills_learning/models.py` defining `SkillKind` (`"knowledge" | "flow_template"`), `SkillMetadata`, `FlowStep`, `Skill`, and `FlowTemplateSkill`, plus `workflow/skill_exec.py` with `validate_skill_args` / `resolve_skill_steps` / `_resolve_value`. This change **does not introduce a parallel `skills/` package**. Instead:
|
||||
- Reuse all existing domain types verbatim. The only model-layer addition is a `KnowledgeSkill(Skill)` class in `skills_learning/models.py` (the `kind` Literal already includes `"knowledge"` but no concrete class exists for it yet).
|
||||
- The MCP parameter-resolution helper (§4.2 of tasks) wraps `workflow.skill_exec.resolve_skill_steps` rather than reimplementing it.
|
||||
- The archived `skill-authoring` spec (now in `openspec/specs/skill-authoring/spec.md`) explicitly anticipated this change: locally-synthesized skills live in `skills_learning/store.py` tagged `source = "local-synthesis"` and **must not write into** the externally-synced catalog store. This change honors that boundary by using a **physically separate** SQLite file (`tasks/skills.sqlite3`) for synced skills.
|
||||
- **Alternative considered**: Move all shared skill domain types into `core/skill_models.py` to make the cross-feature reuse explicit. Deferred — would be a larger refactor of already-tested code, and the boundary is data-model-only (no store/runtime coupling). Revisited if a third consumer appears.
|
||||
|
||||
### D7: CONSTITUTION.md compliance — code lands in `core`-extended, `storage`, and `api` layers only
|
||||
Per `docs/CONSTITUTION.md` lines 33-34 ("HTTP and MCP dependencies enter at `api`. No LLM, HTTP, or MCP dependency may appear in `core`, `driver`, `device`, or `tools`."), and the dependency direction `core → driver/device → tools → perception → storage → runtime → api`, this change's files map as follows:
|
||||
|
||||
| Concern | File (new or extended) | Layer | Allowed deps |
|
||||
|---|---|---|---|
|
||||
| Domain models (extend) | `skills_learning/models.py` | core (feature-scoped) | dataclasses, `core.models` only — zero HTTP/MCP/LLM |
|
||||
| Synced-skill local store + query | `storage/skill_catalog.py` (new) | storage | `sqlite3`, `skills_learning.models` — zero HTTP/MCP/LLM |
|
||||
| Subscription HTTP client + poll loop + webhook | `api/skill_sync.py` (new) | api | `httpx`, `storage.skill_catalog` |
|
||||
| Skill MCP tools (list/search/get/resolve) | `api/skill_catalog_mcp.py` (new) | api | `mcp.server.fastmcp`, `storage.skill_catalog`, `workflow.skill_exec` |
|
||||
| MCP server wire-up | `api/mcp.py` (one-line addition) | api | calls `register_skill_catalog_tools(...)` at end of `create_mcp_server` |
|
||||
|
||||
`core`, `driver`, `device`, `tools` receive **zero new imports** from this change. The `tasks/skills.sqlite3` file is physically separate from both `tasks/tasks.sqlite3` (task metadata) and the in-memory `skills_learning.SkillStore` (local synthesis), preserving the archived `skill-authoring` spec's "no cross-writes" contract at the storage layer.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Risk]** The Subscription Platform's actual API shape is unknown/assumed (`fetch_entitled_skills`, optional webhook) → **Mitigation**: keep `skills/sync_client.py` behind a small internal interface (similar to `Driver` in `apex-agent-mvp`) so the concrete HTTP client can be adjusted once the real Subscription Platform API is finalized, without touching `catalog.py` or `mcp_tools.py`.
|
||||
- **[Risk]** Stale cache could serve outdated skill content between polls → **Mitigation**: D4's query-time entitlement re-check plus a configurable poll interval; document the staleness window explicitly rather than promising real-time freshness.
|
||||
- **[Risk]** Flow-template skills reference tools/parameters that drift from the actual `tools/` function signatures in `apex-agent-mvp` (e.g. a template calls a tool that was renamed) → **Mitigation**: validate a flow template's `tool` names against the currently registered MCP/tool set at sync time (or at least at `get_skill` time) and surface a clear "skill unavailable/invalid" error rather than letting a bad call reach the device.
|
||||
- **[Trade-off]** No local skill authoring/editing keeps this change simple and avoids ownership ambiguity, but means Apex Agent is fully dependent on the Subscription Platform being reachable at least once to have any skills at all — acceptable since the platform is a required dependency by design, not an optional enhancement.
|
||||
- **[Trade-off]** Not providing a server-side `run_skill_flow` execution tool keeps flow templates consistent with the Observe-Think-Act loop, at the cost of the LLM needing a few more tool-call round trips per flow skill than a single batched call would take — acceptable given the platform's core principle of always re-observing between actions.
|
||||
- **[Risk]** Reusing `skills_learning/models.py` as the domain model for synced skills couples this change to the `skills_learning` feature package. If either consumer (local synthesis or external sync) needs a divergent model shape, ripple effects result. → **Mitigation**: the coupling is at the data-model layer only — no store, runtime, or executor coupling exists. If a third consumer appears or divergence is needed, refactor the shared types into `core/skill_models.py` as a separate cleanup change (not blocking this one).
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Additive across existing packages — no new top-level package is introduced:
|
||||
- **`skills_learning/models.py`**: gains one new dataclass (`KnowledgeSkill`). No changes to existing types; existing tests in `tests/test_skill_*.py` are unaffected.
|
||||
- **`storage/skill_catalog.py`** (new): independent module + a new SQLite file `tasks/skills.sqlite3`. Does not touch `storage/task_metadata.py`'s `tasks/tasks.sqlite3` or `skills_learning/store.py`'s in-memory store.
|
||||
- **`api/skill_sync.py`** (new): HTTP sync client + poll runner + webhook handler. Self-contained; the webhook registers on the existing FastAPI app from `apex-agent-mvp` if applied, or stands alone for testing.
|
||||
- **`api/skill_catalog_mcp.py`** (new): MCP tool registration helper.
|
||||
- **`api/mcp.py`**: one-line addition — `register_skill_catalog_tools(server, ...)` called at the end of `create_mcp_server`. Existing device-capability tools are unchanged.
|
||||
- Rollback: remove the four new/extended files and delete `tasks/skills.sqlite3`. No data migration or schema changes to existing tables are required.
|
||||
|
||||
If `apex-agent-mvp` is not yet applied, `api/skill_catalog_mcp.py` can stand up its own MCP server instance for independent testing (mirroring the original migration note); otherwise it composes onto the existing server via the one-line wire-up above.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Exact Subscription Platform API contract (auth mechanism, request/response shapes, whether it supports `since_version` incremental sync or only full-catalog fetch) — to be confirmed with that platform's team/spec before the concrete `HttpSubscriptionClient` in `api/skill_sync.py` is finalized. The `SubscriptionClient` Protocol (§3.1 of tasks) is shaped to absorb either full or incremental fetch; the concrete HTTP mapping is the only thing that changes once the real API is known.
|
||||
- Whether flow-template `args_template` placeholders need a richer expression language (e.g. simple conditionals) or plain `{param}` substitution is sufficient for the MVP — left open, default to plain substitution (matching existing `workflow.skill_exec._resolve_value`) and revisit if a real skill needs more.
|
||||
|
||||
## Resolved Questions
|
||||
|
||||
- **Local catalog storage location** (previously open): resolved by D7 — a physically separate SQLite file at `tasks/skills.sqlite3`, owned by `storage/skill_catalog.py`. Chosen to preserve the archived `skill-authoring` spec's "no cross-writes between local-synthesis and synced-skill stores" contract at the storage layer (separate files make the boundary physical, not just conventional). Reusing `tasks/tasks.sqlite3`'s file with new tables was rejected because it would couple sync-skill schema migrations to task-metadata migrations.
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
Apex Agent's MCP tool server (see change `apex-agent-mvp`) gives the LLM raw device capabilities (tap/swipe/screenshot/...), but it has no notion of reusable, task-specific know-how — e.g. "how to search on Xiaohongshu," or "the tap/swipe/input sequence to place an order on Taobao." Today that knowledge would have to live entirely inside the LLM's own reasoning or be re-derived from scratch on every task. We need a **Skill** concept the AI can discover and pull on demand via MCP, and — since skill content will be authored, versioned, and entitled to specific tenants/devices by a separate, already-planned **Subscription Platform** (统一订阅平台,另一套信息管理系统) — Apex Agent needs a defined contract for consuming that platform's catalog rather than owning skill authoring itself.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Introduce a **Skill Catalog** capability: a local data model and store for Skills, where a Skill is either a **knowledge skill** (structured instructional/markdown content the AI reads to decide how to act, analogous to Claude Skills) or a **flow-template skill** (a parameterized, predefined sequence of capability calls — e.g. tap/swipe/input steps with placeholders — that the AI mostly fills in parameters for and triggers, rather than re-planning from scratch). Both kinds share common metadata (id, name, description, version, tags) so they can be listed/searched uniformly.
|
||||
- Introduce **Skill MCP tools** (`list_skills`, `search_skills`, `get_skill`, `run_skill_flow`-input-resolution helper) exposed through the same MCP surface established in `apex-agent-mvp`'s `mcp-tool-server`, so an LLM can discover which skills are available and fetch their content/flow template without knowing anything about the Subscription Platform underneath.
|
||||
- Introduce a **Skill Subscription Sync** capability: a client-side contract for talking to the external Subscription Platform, covering (a) pulling/receiving the catalog of skills a given deployment is entitled to, (b) keeping the local Skill Catalog in sync (create/update/remove on change), and (c) enforcing subscription-based visibility so only skills the current tenant/device/agent is subscribed to are listed or fetchable via the MCP tools.
|
||||
- Explicitly out of scope for this change: designing or building the Subscription Platform itself (authoring UI, billing, skill publishing workflow) — it is treated as an existing/external system; this change only defines the integration contract (API shape, sync semantics, auth) Apex Agent needs from it. Also out of scope: automatic skill-authoring/generation by the LLM, and a skill marketplace UI.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `skill-catalog`: Local Skill data model (knowledge-doc and flow-template variants), storage, and search/query functions used by both the MCP tools and the sync client.
|
||||
- `skill-mcp-tools`: MCP-facing tool surface for listing, searching, and fetching Skill content/flow templates, plus resolving flow-template parameters, without exposing any Subscription Platform or storage detail to the LLM.
|
||||
- `skill-subscription-sync`: Contract and client implementation for syncing the Skill Catalog from the external Subscription Platform (pull and/or push), and for enforcing subscription-based visibility/permission scoping per tenant/device/agent.
|
||||
|
||||
### Modified Capabilities
|
||||
(none — `mcp-tool-server` from the pending `apex-agent-mvp` change is composed with, not modified: this change adds new tools to the same MCP server process rather than changing that capability's existing requirements. If `apex-agent-mvp` has not yet been applied when this change is implemented, the Skill MCP tools should still be registrable on their own MCP server instance and merged in later.)
|
||||
|
||||
## Impact
|
||||
|
||||
- **New code**: extends existing packages rather than adding a new one. Adds `KnowledgeSkill` to `skills_learning/models.py` (reusing the already-shipped `SkillKind`/`SkillMetadata`/`FlowStep`/`Skill`/`FlowTemplateSkill`); new `storage/skill_catalog.py` (synced-skill local store + query); new `api/skill_sync.py` (Subscription Platform HTTP client, poll loop, optional webhook receiver); new `api/skill_catalog_mcp.py` (registers `list_skills`/`search_skills`/`get_skill`/`resolve_flow_template` as MCP tools, wrapping the existing `workflow/skill_exec.resolve_skill_steps`); one-line wire-up in `api/mcp.py:create_mcp_server`.
|
||||
- **Dependencies**: reuses the already-shipped domain models from `skills_learning/` (archived `skill-learning-runtime` change) and the parameter-resolution logic from `workflow/skill_exec.py`. Depends on the `apex-agent-mvp` MCP server surface for tool registration. No new external dependency beyond `httpx` (already in `pyproject.toml`) for the sync API.
|
||||
- **External systems**: introduces a new external dependency — the Subscription Platform's API (assumed to expose an endpoint to fetch entitled skills and, optionally, a webhook/push channel for change notifications). Exact base URL/auth mechanism is a deployment-time configuration, not a code dependency.
|
||||
- **Storage**: adds a new local Skill Catalog store as a **physically separate** SQLite file `tasks/skills.sqlite3` (owned by `storage/skill_catalog.py`), distinct from `tasks/tasks.sqlite3` (task metadata) and the in-memory `skills_learning.SkillStore`. This preserves the archived `skill-authoring` spec's contract that locally-synthesized and externally-synced skills never share storage. A small sync-state table (last-synced version/timestamp/error per subscription) lives in the same file.
|
||||
- **Follow-on work explicitly deferred**: Subscription Platform's own design/build, skill-authoring workflows, billing/entitlement logic beyond "is this skill visible to me," and any LLM-driven automatic skill generation.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Unified Skill data model
|
||||
The system SHALL represent every Skill with shared metadata (id, name, description, version, tags, source/subscription id, updated_at) and a `kind` discriminator of either `knowledge` or `flow_template`, so both kinds can be listed and searched through one catalog.
|
||||
|
||||
#### Scenario: Knowledge skill has content
|
||||
- **WHEN** a Skill with `kind = knowledge` is stored
|
||||
- **THEN** it includes a `content` field (structured instructional text/markdown) in addition to the shared metadata
|
||||
|
||||
#### Scenario: Flow-template skill has steps and parameters
|
||||
- **WHEN** a Skill with `kind = flow_template` is stored
|
||||
- **THEN** it includes an ordered `steps` list (each referencing a tool name and an args template that may contain `{param}` placeholders) and a `parameters` schema (name, type, required, description) in addition to the shared metadata
|
||||
|
||||
### Requirement: Local skill storage
|
||||
The system SHALL persist the synced Skill Catalog locally so that skills can be listed and fetched without a live round-trip to the Subscription Platform for every query.
|
||||
|
||||
#### Scenario: Skill readable after sync
|
||||
- **WHEN** a skill has been synced from the Subscription Platform into the local catalog
|
||||
- **THEN** subsequent list/search/get operations return that skill without requiring a new network call to the Subscription Platform
|
||||
|
||||
#### Scenario: Local catalog is not independently authored
|
||||
- **WHEN** a caller attempts to create or edit a skill directly in the local catalog (outside of a sync operation)
|
||||
- **THEN** the system SHALL reject or ignore the write, since the Subscription Platform is the sole source of truth for skill content
|
||||
|
||||
### Requirement: Skill search and query
|
||||
The system SHALL provide functions to list all currently visible skills, search skills by name/tag/description text, and fetch a single skill by id.
|
||||
|
||||
#### Scenario: List returns only visible skills
|
||||
- **WHEN** the catalog is queried for the list of skills visible to the current caller
|
||||
- **THEN** it returns only skills whose subscription is currently active for that caller, sorted in a stable order (e.g. by name)
|
||||
|
||||
#### Scenario: Search matches on name, tags, or description
|
||||
- **WHEN** a search query string matches a skill's name, a tag, or its description
|
||||
- **THEN** that skill is included in the search results
|
||||
|
||||
#### Scenario: Get by id returns full content
|
||||
- **WHEN** a caller fetches a skill by its id
|
||||
- **THEN** the system returns the full skill record, including `content` for knowledge skills or `steps`/`parameters` for flow-template skills
|
||||
|
||||
#### Scenario: Get by id for unknown or invisible skill
|
||||
- **WHEN** a caller fetches a skill id that does not exist, or exists but is not currently visible to them (subscription inactive)
|
||||
- **THEN** the system returns a clear "not found" result rather than leaking the skill's existence or content
|
||||
|
||||
### Requirement: Flow-template tool reference validation
|
||||
The system SHALL validate that a flow-template skill's referenced tool names correspond to currently registered device-capability tools, and SHALL mark a flow-template skill as invalid/unavailable rather than allowing it to be fetched successfully if a referenced tool does not exist.
|
||||
|
||||
#### Scenario: Valid flow template
|
||||
- **WHEN** every step in a flow-template skill references a tool name that is currently registered
|
||||
- **THEN** the skill is fetchable and returned normally
|
||||
|
||||
#### Scenario: Flow template references an unknown tool
|
||||
- **WHEN** a flow-template skill's steps reference a tool name that is not currently registered (e.g. renamed or removed)
|
||||
- **THEN** fetching that skill returns a clear "skill unavailable/invalid" result instead of a step list containing a dangling tool reference
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: MCP tools for skill discovery and retrieval
|
||||
The system SHALL expose `list_skills`, `search_skills`, and `get_skill` as MCP tools on the same MCP server surface used for device capabilities, so an LLM client can discover and fetch Skill content using the same tool-calling mechanism it already uses for device actions.
|
||||
|
||||
#### Scenario: LLM lists available skills
|
||||
- **WHEN** an MCP client calls `list_skills`
|
||||
- **THEN** it receives the set of skills currently visible to it (per subscription visibility), each with id, name, description, kind, and tags, without any Subscription Platform-specific fields or identifiers
|
||||
|
||||
#### Scenario: LLM searches for a relevant skill
|
||||
- **WHEN** an MCP client calls `search_skills` with a query string
|
||||
- **THEN** it receives matching skills ranked/filtered by relevance to the query, using the same visibility rules as `list_skills`
|
||||
|
||||
#### Scenario: LLM fetches a specific skill's content
|
||||
- **WHEN** an MCP client calls `get_skill` with a skill id
|
||||
- **THEN** it receives the full skill content appropriate to its kind: `content` text for a `knowledge` skill, or `steps`/`parameters` for a `flow_template` skill
|
||||
|
||||
### Requirement: Flow-template parameter resolution helper
|
||||
The system SHALL provide an MCP-facing helper that, given a flow-template skill id and a set of proposed parameter values, validates the values against the skill's declared `parameters` schema and returns the fully resolved step sequence (placeholders substituted) for the LLM to then execute step-by-step via the existing device-capability tools.
|
||||
|
||||
#### Scenario: Valid parameters resolve the template
|
||||
- **WHEN** the LLM provides values for all required parameters of a flow-template skill
|
||||
- **THEN** the system returns the ordered steps with all `{param}` placeholders substituted by the provided values
|
||||
|
||||
#### Scenario: Missing required parameter
|
||||
- **WHEN** the LLM omits a required parameter when resolving a flow-template skill
|
||||
- **THEN** the system returns a clear validation error identifying the missing parameter(s) instead of returning a partially-substituted step list
|
||||
|
||||
### Requirement: No server-side flow execution tool
|
||||
The system SHALL NOT expose an MCP tool that executes a flow-template skill's full step sequence server-side on the LLM's behalf; the LLM SHALL issue each resulting device-capability tool call itself so every step remains subject to the existing Agent Runtime's Observe-Think-Act loop and Executor retry/wait handling.
|
||||
|
||||
#### Scenario: No batch-execute tool is available
|
||||
- **WHEN** the MCP tool list is inspected
|
||||
- **THEN** it contains skill discovery/retrieval/resolution tools but no tool that both resolves and executes a flow-template skill's steps in a single call
|
||||
|
||||
### Requirement: Skill MCP errors are semantic
|
||||
The system SHALL translate catalog-level errors (skill not found, skill not visible/entitled, invalid/unavailable flow template) into clear, semantic MCP tool error responses, consistent in style with the device-capability tool error handling.
|
||||
|
||||
#### Scenario: Requesting a non-visible skill
|
||||
- **WHEN** `get_skill` is called with a skill id that exists but is not visible to the caller's current subscriptions
|
||||
- **THEN** the tool returns a semantic "not found" error rather than a raw database or internal exception
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Pull-based sync from the Subscription Platform
|
||||
The system SHALL periodically fetch, from the external Subscription Platform, the set of skills the current deployment (tenant/device/agent) is entitled to, and SHALL apply creates/updates/removals to the local Skill Catalog to match that entitled set.
|
||||
|
||||
#### Scenario: New skill appears after sync
|
||||
- **WHEN** the Subscription Platform reports a new entitled skill that does not yet exist locally
|
||||
- **THEN** the next sync cycle creates it in the local Skill Catalog
|
||||
|
||||
#### Scenario: Updated skill content is refreshed
|
||||
- **WHEN** the Subscription Platform reports a newer version of a skill already present locally
|
||||
- **THEN** the next sync cycle updates the local copy to the newer version
|
||||
|
||||
#### Scenario: Revoked entitlement removes local visibility
|
||||
- **WHEN** the Subscription Platform no longer includes a previously-entitled skill in the current entitled set
|
||||
- **THEN** the next sync cycle removes or marks that skill as no longer visible in the local Skill Catalog
|
||||
|
||||
#### Scenario: Sync interval is configurable
|
||||
- **WHEN** the deployment is configured with a sync poll interval
|
||||
- **THEN** the system performs pull sync on approximately that interval without requiring a restart to take effect on the next cycle
|
||||
|
||||
### Requirement: Optional push-triggered sync
|
||||
The system MAY support receiving a change notification (e.g. webhook) from the Subscription Platform, and WHEN it does, SHALL trigger an immediate out-of-cycle pull sync rather than waiting for the next poll interval; correctness of the catalog SHALL NOT depend on push notifications being delivered.
|
||||
|
||||
#### Scenario: Push notification triggers immediate sync
|
||||
- **WHEN** a change notification is received from the Subscription Platform
|
||||
- **THEN** the system performs a pull sync immediately, independent of the regular poll schedule
|
||||
|
||||
#### Scenario: No push configured still stays eventually consistent
|
||||
- **WHEN** push notifications are not configured or not received
|
||||
- **THEN** the local Skill Catalog still reflects the Subscription Platform's entitled set within one poll interval via the baseline pull sync
|
||||
|
||||
### Requirement: Subscription-based visibility enforcement
|
||||
The system SHALL enforce, at both sync time and query time, that only skills belonging to the caller's currently active subscription(s) are stored as visible or returned by catalog queries.
|
||||
|
||||
#### Scenario: Query-time re-check catches stale entitlement
|
||||
- **WHEN** a skill's entitlement has been revoked but the local cache has not yet completed its next sync cycle
|
||||
- **AND** a catalog query for that skill is made using already-known revocation information
|
||||
- **THEN** the system does not return that skill as visible, even though its record may still exist locally pending cleanup
|
||||
|
||||
#### Scenario: Multiple subscriptions compose visibility
|
||||
- **WHEN** a deployment holds more than one active subscription, each entitling a different set of skills
|
||||
- **THEN** catalog queries return the union of skills entitled across all of that deployment's active subscriptions
|
||||
|
||||
### Requirement: Sync client contract is transport-replaceable
|
||||
The system SHALL define the Subscription Platform integration (fetch entitled skills, optional change notification receipt) behind a single internal interface, so the concrete HTTP client/auth mechanism can be adjusted to match the real Subscription Platform API without changes to the Skill Catalog or MCP tool layers.
|
||||
|
||||
#### Scenario: Sync client swap does not affect catalog/tools
|
||||
- **WHEN** the concrete Subscription Platform client implementation is replaced (e.g. different auth scheme or request/response shape)
|
||||
- **THEN** `skill-catalog` and `skill-mcp-tools` behavior and their own specs remain unaffected, as long as the new client still satisfies the sync interface
|
||||
|
||||
### Requirement: Sync failure handling
|
||||
The system SHALL treat a failed sync attempt (network error, auth failure, malformed response) as non-fatal to already-cached skills: the local catalog SHALL continue serving its last-known-good state, and the failure SHALL be recorded/observable rather than silently discarded.
|
||||
|
||||
#### Scenario: Sync failure preserves last-known catalog
|
||||
- **WHEN** a sync attempt fails due to a network or platform error
|
||||
- **THEN** the local Skill Catalog is left unchanged (not cleared or partially corrupted) and remains queryable using its last successfully synced state
|
||||
|
||||
#### Scenario: Sync failure is observable
|
||||
- **WHEN** a sync attempt fails
|
||||
- **THEN** the system records the failure (e.g. last-error timestamp/reason) so it can be surfaced to operators rather than failing silently forever
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1. Domain model extension (reuse `skills_learning/`)
|
||||
|
||||
- [x] 1.1 Add `KnowledgeSkill(Skill)` dataclass to `skills_learning/models.py` carrying a `content: str` field plus `to_dict`/`from_dict` mirroring `FlowTemplateSkill`'s shape. The `SkillKind` Literal already includes `"knowledge"` — no discriminator change needed.
|
||||
- [x] 1.2 Confirm `pyproject.toml` already includes `httpx>=0.27.0` and `mcp>=1.27,<2`; no new dependencies required. (If a missing dep is discovered during impl, add it here.)
|
||||
|
||||
## 2. Catalog storage & query (capability: skill-catalog; layer: `storage/`)
|
||||
|
||||
- [x] 2.1 Create `storage/skill_catalog.py` with `SkillCatalogStore(db_path="tasks/skills.sqlite3")` using SQLite. Schema: `subscriptions(id, active, last_synced_version, last_error, last_synced_at)` and `skills(id, subscription_id, name, description, kind, tags_json, version, source, updated_at, content, steps_json, parameters_json)`. Public read API only (`list_skills`, `search_skills`, `get_skill`); write surface is `_apply_sync_*` methods callable only from `api/skill_sync.py`.
|
||||
- [x] 2.2 `list_skills(active_subscriptions: set[str]) -> list[SkillMetadata]` — returns only skills whose `subscription_id ∈ active_subscriptions`, stable-sorted by `name`.
|
||||
- [x] 2.3 `search_skills(query: str, active_subscriptions: set[str]) -> list[SkillMetadata]` — substring match on `name`/`tags`/`description`, ranked by simple relevance (e.g. name-prefix → name-contains → tag/desc).
|
||||
- [x] 2.4 `get_skill(skill_id: str, active_subscriptions: set[str]) -> KnowledgeSkill | FlowTemplateSkill | None` — returns `None` for unknown id **and** for known-but-not-visible id (no existence leak).
|
||||
- [x] 2.5 Enforce sync-only writes: no public `create`/`update`/`remove` methods. The `_apply_sync_upsert` / `_apply_sync_remove` / `_apply_sync_replace_all` methods are prefixed with `_` to signal the contract; only `api/skill_sync.py` imports them.
|
||||
- [x] 2.6 `validate_flow_template_tools(skill: FlowTemplateSkill, registered_tools: set[str]) -> bool` — returns `False` if any `step.tool_name` is not in `registered_tools`; `get_skill` uses this to return `None` (treat as unavailable) when references dangle.
|
||||
- [x] 2.7 `tests/test_skill_catalog.py` — unit tests covering: visibility filtering on list/search/get, not-found vs not-visible indistinguishability, dangling-tool-reference invalidation, stable sort order, round-trip serialization for both `kind` values.
|
||||
|
||||
## 3. Subscription sync client (capability: skill-subscription-sync; layer: `api/`)
|
||||
|
||||
- [x] 3.1 Define `SubscriptionClient` Protocol in `api/skill_sync.py`: `fetch_entitled_skills(subscription_id: str, since_version: int | None = None) -> SyncDelta` where `SyncDelta` carries `skills: list[Skill]`, `removed_ids: list[str]`, `latest_version: int`. Supports both full (since_version=None) and incremental fetch shapes.
|
||||
- [x] 3.2 Implement `HttpSubscriptionClient(base_url, auth_token, timeout)` satisfying the Protocol, using `httpx`. Auth and request/response shape isolated behind this class so the real Subscription Platform API can be wired in by adjusting only this file.
|
||||
- [x] 3.3 Implement `SkillSyncRunner(store, client, subscriptions: list[str], poll_interval)`: a `tick()` method that fetches deltas for each subscription and applies them via `store._apply_sync_*`. Provide both a manual `tick()` API and a `run_forever()` blocking loop; do not auto-start any thread on import.
|
||||
- [x] 3.4 Optional push receiver: add `POST /webhooks/skill-sync` to `api/rest.py` (or a new `api/skill_sync_webhook.py` if rest.py grows too large) that calls `SkillSyncRunner.tick()` immediately. Documented as optional — poll loop remains correct standalone.
|
||||
- [x] 3.5 Query-time revocation re-check (D4): `active_subscriptions` parameter accepted by `list_skills`/`search_skills`/`get_skill` is **re-read from the `subscriptions` table on every call** (caller passes subscription IDs; store checks `active = 1`). Revoking a subscription (sync sets `active = 0`) takes effect on the next query, not just the next sync.
|
||||
- [x] 3.6 Sync failure handling: `SkillSyncRunner.tick()` wraps `client.fetch_entitled_skills` in try/except; on any exception (network/auth/malformed-response), leave `store` unchanged and write `last_error` + `last_synced_at` to the `subscriptions` row. Cache continues serving last-known-good state.
|
||||
- [x] 3.7 `tests/test_skill_sync_client.py` — using a `FakeSubscriptionClient` implementing the Protocol: new/updated/removed delta scenarios, push-triggered immediate `tick()`, query-time revocation before next sync (3.5), failure-preserves-cache (3.6).
|
||||
|
||||
## 4. Skill MCP tools (capability: skill-mcp-tools; layer: `api/`)
|
||||
|
||||
- [x] 4.1 Create `api/skill_catalog_mcp.py` with `register_skill_catalog_tools(server, store, get_active_subscriptions: Callable[[], set[str]], registered_tools: Callable[[], set[str]])`. Wire it into `api/mcp.py:create_mcp_server` with a single call at the end.
|
||||
- [x] 4.2 `resolve_flow_template(skill_id, params: dict)` MCP tool: thin wrapper over `workflow.skill_exec.resolve_skill_steps` (after fetching the skill via `store.get_skill`). Returns the substituted step list. Missing/invalid params surface as semantic errors via 4.3.
|
||||
- [x] 4.3 Register `list_skills`/`search_skills`/`get_skill`/`resolve_flow_template` MCP tools with semantic error translation consistent with `api/errors.py:call_with_semantic_errors`. Distinct error codes for `SkillNotFound`, `SkillNotVisible` (mapped to not-found at the response layer — no existence leak), `InvalidFlowTemplate` (dangling tool reference), `MissingParameter`.
|
||||
- [x] 4.4 Confirm (via test 4.5) that **no** `run_skill_flow` or any other batch-execute MCP tool is registered. Execution stays with the LLM issuing existing device-capability tool calls one at a time, per D5.
|
||||
- [x] 4.5 `tests/test_skill_catalog_mcp.py` — drive `FastMCP` in-memory tool registry: assert `list_skills`/`search_skills`/`get_skill`/`resolve_flow_template` are registered; assert no tool whose name suggests batch execution is registered; exercise each tool against a seeded in-memory `SkillCatalogStore` with mocked `get_active_subscriptions`; cover not-found, not-visible, dangling-tool-reference, and missing-parameter error paths.
|
||||
|
||||
## 5. End-to-end validation
|
||||
|
||||
- [x] 5.1 `tests/test_skill_catalog_e2e.py` — seed a `SkillCatalogStore` via a `FakeSubscriptionClient` with one knowledge skill and one flow-template skill; assert both are listable/searchable/gettable through the MCP tool surface (using the same `register_skill_catalog_tools` registration).
|
||||
- [x] 5.2 Same e2e harness — simulate revocation: remove the skill from the fake client's entitled set; run one sync `tick()` and confirm the skill disappears from `list_skills`/`get_skill`; separately, revoke subscription (`active = 0`) without running sync and confirm the query-time re-check (3.5) hides the skill before the next sync completes.
|
||||
- [x] 5.3 Same e2e harness — call `resolve_flow_template` via MCP, then drive the resulting step sequence through the existing `apex-agent-mvp` device-capability tools (`tap`/`input_text`/etc.) against a mocked device (`tests/fakes.py` or a new fake), confirming each step still flows through the normal Executor Observe-Think-Act loop (no batched execution shortcut).
|
||||
Reference in New Issue
Block a user