feat(skill-catalog-subscription): synced catalog + HTTP sync + MCP tools
Consumes the external Subscription Platform as source of truth for skill content; reuses skills_learning domain models (extended with KnowledgeSkill) and workflow.skill_exec resolver. HTTP/MCP deps land in api/ per CONSTITUTION.md; synced skills use a physically separate SQLite file (tasks/skills.sqlite3) to preserve the skill-authoring capability boundary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,39 +1,38 @@
|
||||
## 1. Package scaffolding & data model
|
||||
## 1. Domain model extension (reuse `skills_learning/`)
|
||||
|
||||
- [ ] 1.1 Create `skills/` package (`__init__.py`) alongside existing `core/`, `tools/`, `vision/`, `runtime/`, `api/`, `storage/`
|
||||
- [ ] 1.2 Add `skills/models.py`: `SkillMetadata` (id, name, description, version, tags, source/subscription id, updated_at), `KnowledgeSkill` (content), `FlowTemplateSkill` (steps, parameters), and a `kind` discriminator uniting them
|
||||
- [ ] 1.3 Add dependencies if needed (HTTP client for sync, no new heavy deps expected) to `pyproject.toml`
|
||||
- [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. Skill Catalog storage & query (capability: skill-catalog)
|
||||
## 2. Catalog storage & query (capability: skill-catalog; layer: `storage/`)
|
||||
|
||||
- [ ] 2.1 Implement `skills/catalog.py` local store (decide SQLite table(s) reusing the existing DB vs. separate file, per design's open question) with create/update/remove operations used only by the sync path
|
||||
- [ ] 2.2 Implement `list_skills(caller_context)` returning only skills visible under the caller's active subscriptions, stable-sorted
|
||||
- [ ] 2.3 Implement `search_skills(query, caller_context)` matching name/tags/description
|
||||
- [ ] 2.4 Implement `get_skill(skill_id, caller_context)` returning full content, or a clear "not found" for unknown/invisible ids
|
||||
- [ ] 2.5 Reject/ignore any direct create-or-edit call to the catalog that didn't come from the sync path (enforce "Subscription Platform is sole source of truth")
|
||||
- [ ] 2.6 Implement flow-template tool-reference validation: check each step's `tool` name against the currently registered device-capability tools; mark skill invalid/unavailable if any reference is dangling
|
||||
- [ ] 2.7 Write unit tests: list/search/get visibility filtering, not-found cases, and flow-template validation (valid and dangling-reference cases)
|
||||
- [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)
|
||||
## 3. Subscription sync client (capability: skill-subscription-sync; layer: `api/`)
|
||||
|
||||
- [ ] 3.1 Define the internal sync interface (e.g. `SubscriptionClient.fetch_entitled_skills(subscription_id, since_version)` supporting full or incremental fetch) in `skills/sync_client.py`
|
||||
- [ ] 3.2 Implement a concrete HTTP-based `SubscriptionClient` against the assumed Subscription Platform API (configurable base URL/auth), isolated behind the interface from 3.1
|
||||
- [ ] 3.3 Implement the poll loop: fetch on a configurable interval, diff against local catalog, and apply create/update/remove to `skills/catalog.py`
|
||||
- [ ] 3.4 Implement optional push-triggered sync: a receiver (e.g. a small webhook endpoint) that, on notification, triggers an immediate out-of-cycle pull rather than being required for correctness
|
||||
- [ ] 3.5 Implement subscription-based visibility enforcement at query time (re-check active subscription set, not just last-sync membership) so revoked entitlements stop being visible without waiting for the next full sync
|
||||
- [ ] 3.6 Implement sync failure handling: on network/auth/malformed-response errors, leave the local catalog unchanged and record a last-error timestamp/reason for observability
|
||||
- [ ] 3.7 Write unit tests: create/update/remove sync scenarios, push-triggered immediate sync, query-time revocation re-check, and sync-failure-preserves-cache behavior (using a fake `SubscriptionClient`)
|
||||
- [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)
|
||||
## 4. Skill MCP tools (capability: skill-mcp-tools; layer: `api/`)
|
||||
|
||||
- [ ] 4.1 Implement `skills/mcp_tools.py` registering `list_skills`, `search_skills`, `get_skill` as MCP tools on the existing MCP server surface from `apex-agent-mvp` (or a standalone MCP server instance if that change is not yet applied)
|
||||
- [ ] 4.2 Implement the flow-template parameter-resolution MCP tool: validate provided values against a skill's `parameters` schema and return the fully substituted step sequence, or a clear validation error listing missing/invalid parameters
|
||||
- [ ] 4.3 Add semantic error translation for skill MCP tools (not-found, not-visible/entitled, invalid/unavailable flow template) consistent in style with the device-capability tool error handling
|
||||
- [ ] 4.4 Confirm no MCP tool exists that both resolves and executes a flow-template skill's steps server-side (execution stays with the LLM issuing existing device-capability tool calls one at a time)
|
||||
- [ ] 4.5 Write tests: `list_skills`/`search_skills`/`get_skill` via MCP against a seeded catalog (mocked sync), parameter-resolution success/failure cases, and an explicit check that no batch-execute tool is registered
|
||||
- [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
|
||||
|
||||
- [ ] 5.1 Seed a local catalog via a fake `SubscriptionClient` with one knowledge skill and one flow-template skill, and verify both are listable/searchable/fetchable via MCP tools
|
||||
- [ ] 5.2 Simulate an entitlement revocation (remove a skill from the fake client's entitled set) and confirm it disappears from `list_skills`/`get_skill` after both a sync cycle and, separately, via the query-time re-check before the next sync completes
|
||||
- [ ] 5.3 Resolve a flow-template skill's parameters via MCP, then manually drive the resulting step sequence through the existing `apex-agent-mvp` device-capability tools against a mocked device, confirming each step still goes through the normal Observe-Think-Act loop
|
||||
- [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