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>
7.1 KiB
7.1 KiB
1. Domain model extension (reuse skills_learning/)
- 1.1 Add
KnowledgeSkill(Skill)dataclass toskills_learning/models.pycarrying acontent: strfield plusto_dict/from_dictmirroringFlowTemplateSkill's shape. TheSkillKindLiteral already includes"knowledge"— no discriminator change needed. - 1.2 Confirm
pyproject.tomlalready includeshttpx>=0.27.0andmcp>=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/)
- 2.1 Create
storage/skill_catalog.pywithSkillCatalogStore(db_path="tasks/skills.sqlite3")using SQLite. Schema:subscriptions(id, active, last_synced_version, last_error, last_synced_at)andskills(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 fromapi/skill_sync.py. - 2.2
list_skills(active_subscriptions: set[str]) -> list[SkillMetadata]— returns only skills whosesubscription_id ∈ active_subscriptions, stable-sorted byname. - 2.3
search_skills(query: str, active_subscriptions: set[str]) -> list[SkillMetadata]— substring match onname/tags/description, ranked by simple relevance (e.g. name-prefix → name-contains → tag/desc). - 2.4
get_skill(skill_id: str, active_subscriptions: set[str]) -> KnowledgeSkill | FlowTemplateSkill | None— returnsNonefor unknown id and for known-but-not-visible id (no existence leak). - 2.5 Enforce sync-only writes: no public
create/update/removemethods. The_apply_sync_upsert/_apply_sync_remove/_apply_sync_replace_allmethods are prefixed with_to signal the contract; onlyapi/skill_sync.pyimports them. - 2.6
validate_flow_template_tools(skill: FlowTemplateSkill, registered_tools: set[str]) -> bool— returnsFalseif anystep.tool_nameis not inregistered_tools;get_skilluses this to returnNone(treat as unavailable) when references dangle. - 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 bothkindvalues.
3. Subscription sync client (capability: skill-subscription-sync; layer: api/)
- 3.1 Define
SubscriptionClientProtocol inapi/skill_sync.py:fetch_entitled_skills(subscription_id: str, since_version: int | None = None) -> SyncDeltawhereSyncDeltacarriesskills: list[Skill],removed_ids: list[str],latest_version: int. Supports both full (since_version=None) and incremental fetch shapes. - 3.2 Implement
HttpSubscriptionClient(base_url, auth_token, timeout)satisfying the Protocol, usinghttpx. Auth and request/response shape isolated behind this class so the real Subscription Platform API can be wired in by adjusting only this file. - 3.3 Implement
SkillSyncRunner(store, client, subscriptions: list[str], poll_interval): atick()method that fetches deltas for each subscription and applies them viastore._apply_sync_*. Provide both a manualtick()API and arun_forever()blocking loop; do not auto-start any thread on import. - 3.4 Optional push receiver: add
POST /webhooks/skill-synctoapi/rest.py(or a newapi/skill_sync_webhook.pyif rest.py grows too large) that callsSkillSyncRunner.tick()immediately. Documented as optional — poll loop remains correct standalone. - 3.5 Query-time revocation re-check (D4):
active_subscriptionsparameter accepted bylist_skills/search_skills/get_skillis re-read from thesubscriptionstable on every call (caller passes subscription IDs; store checksactive = 1). Revoking a subscription (sync setsactive = 0) takes effect on the next query, not just the next sync. - 3.6 Sync failure handling:
SkillSyncRunner.tick()wrapsclient.fetch_entitled_skillsin try/except; on any exception (network/auth/malformed-response), leavestoreunchanged and writelast_error+last_synced_atto thesubscriptionsrow. Cache continues serving last-known-good state. - 3.7
tests/test_skill_sync_client.py— using aFakeSubscriptionClientimplementing the Protocol: new/updated/removed delta scenarios, push-triggered immediatetick(), query-time revocation before next sync (3.5), failure-preserves-cache (3.6).
4. Skill MCP tools (capability: skill-mcp-tools; layer: api/)
- 4.1 Create
api/skill_catalog_mcp.pywithregister_skill_catalog_tools(server, store, get_active_subscriptions: Callable[[], set[str]], registered_tools: Callable[[], set[str]]). Wire it intoapi/mcp.py:create_mcp_serverwith a single call at the end. - 4.2
resolve_flow_template(skill_id, params: dict)MCP tool: thin wrapper overworkflow.skill_exec.resolve_skill_steps(after fetching the skill viastore.get_skill). Returns the substituted step list. Missing/invalid params surface as semantic errors via 4.3. - 4.3 Register
list_skills/search_skills/get_skill/resolve_flow_templateMCP tools with semantic error translation consistent withapi/errors.py:call_with_semantic_errors. Distinct error codes forSkillNotFound,SkillNotVisible(mapped to not-found at the response layer — no existence leak),InvalidFlowTemplate(dangling tool reference),MissingParameter. - 4.4 Confirm (via test 4.5) that no
run_skill_flowor 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. - 4.5
tests/test_skill_catalog_mcp.py— driveFastMCPin-memory tool registry: assertlist_skills/search_skills/get_skill/resolve_flow_templateare registered; assert no tool whose name suggests batch execution is registered; exercise each tool against a seeded in-memorySkillCatalogStorewith mockedget_active_subscriptions; cover not-found, not-visible, dangling-tool-reference, and missing-parameter error paths.
5. End-to-end validation
- 5.1
tests/test_skill_catalog_e2e.py— seed aSkillCatalogStorevia aFakeSubscriptionClientwith one knowledge skill and one flow-template skill; assert both are listable/searchable/gettable through the MCP tool surface (using the sameregister_skill_catalog_toolsregistration). - 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 fromlist_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. - 5.3 Same e2e harness — call
resolve_flow_templatevia MCP, then drive the resulting step sequence through the existingapex-agent-mvpdevice-capability tools (tap/input_text/etc.) against a mocked device (tests/fakes.pyor a new fake), confirming each step still flows through the normal Executor Observe-Think-Act loop (no batched execution shortcut).