feat(skills): open skill-management-console change + local skill store

Opens the skill-management-console openspec change (cloud/local skill split
with local override) with proposal, design (D1-D11), four delta specs, and
tasks. Implements the agent-side persistent local skill store
(storage/local_skills.py): authored local skills + cloud-skill overrides in
a physically separate SQLite file, with fork-on-revocation. 10 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 07:28:42 +08:00
co-authored by Claude Opus 4.6
parent e5a12f9b74
commit dd03abbbb0
11 changed files with 1031 additions and 0 deletions
@@ -0,0 +1,67 @@
## ADDED Requirements
### Requirement: Agent-local skills persist in a dedicated store
The agent SHALL persist its locally-managed skills (authored skills and overrides) in a local store backed by a SQLite file physically separate from the synced catalog store and from task metadata, so that local authoring survives agent restarts and never shares storage with synced skills. The synced catalog store's read-only-except-sync write contract and the "no cross-writes between local and synced stores" boundary SHALL remain intact.
#### Scenario: Locally-authored skill survives restart
- **WHEN** the agent creates a local skill and is later restarted
- **THEN** the local skill is still present and readable after restart, without any re-sync
#### Scenario: Local and synced stores stay physically separate
- **WHEN** the agent authors a local skill and separately syncs cloud skills
- **THEN** the local skill is written only to the local store and the synced skills only to the synced store; neither store accepts the other's writes
### Requirement: Agent can author, edit, and delete its own local skills
The agent SHALL be able to create new local skills, edit existing local skills, and delete local skills, operating exclusively on the local store. These operations SHALL NOT touch the synced store or any cloud skill.
#### Scenario: Create a new local skill
- **WHEN** the agent creates a skill with no existing id
- **THEN** a new local skill is persisted with a fresh local id and `origin = "local"`
#### Scenario: Edit and delete a local skill
- **WHEN** the agent updates, then deletes, a local skill by its local id
- **THEN** the update is persisted to the local store, and the delete removes it from the local store; the synced store is unchanged
### Requirement: Agent can locally override a cloud skill
The agent SHALL be able to create a local override keyed by a cloud skill's id. While an override exists, the read surface returns the override's content for that id (the cloud version is shadowed). Creating or updating an override SHALL NOT modify the cloud skill or the synced store.
#### Scenario: Override shadows the cloud skill at read time
- **WHEN** the agent has created an override for a cloud skill id and the read surface is queried for that id
- **THEN** the override's content is returned, marked `origin = "cloud"` and `locally_overridden = true`
#### Scenario: Removing an override re-exposes the cloud skill
- **WHEN** the agent deletes the override for a cloud skill id
- **THEN** subsequent reads for that id return the cloud skill's current synced content, with no override flag
### Requirement: Overrides shadow sync updates until removed
When the underlying cloud skill for an overridden id is updated via sync, the override SHALL continue to shadow the new cloud version; the agent SHALL NOT see the cloud update until the override is removed. (Staleness is explicit and operator/agent-visible, not auto-detected.)
#### Scenario: Cloud skill updates under an active override
- **WHEN** a synced cloud skill whose id has an active local override is updated to a new version
- **THEN** the read surface continues to return the override's content, not the new cloud version
### Requirement: An override forks into a local skill on entitlement revocation
If a cloud skill is removed from the agent's entitled set (sync reports it as removed) and a local override exists for it, the override SHALL be promoted to a standalone local skill: it gains a local id, its `origin` becomes `"local"`, and its content is preserved. If no override exists, revocation simply removes the cloud skill from the local view.
#### Scenario: Revocation with an active override forks it
- **WHEN** sync removes a cloud skill id that has an active local override
- **THEN** the override becomes a standalone local skill with a new local id and `origin = "local"`, and remains readable
#### Scenario: Revocation without an override just removes visibility
- **WHEN** sync removes a cloud skill id that has no local override
- **THEN** that id is no longer visible on the read surface
### Requirement: Unified read surface merges synced and local skills with an origin discriminator
The agent SHALL expose a single read surface (`list` / `search` / `get`) that merges cloud-synced skills (after applying overrides) and local skills, reporting each with an `origin` of `"cloud"` or `"local"` and a `locally_overridden` flag when an override is active. The merge SHALL NOT leak entitlement or cloud-management internals to the consumer, and SHALL NOT distinguish "unknown id" from "known but not visible" for cloud skills.
#### Scenario: List returns both origins
- **WHEN** the read surface lists skills
- **THEN** the result includes cloud-synced and local skills, each tagged with `origin`, stable-ordered
#### Scenario: Override is reflected in list and get
- **WHEN** a cloud skill id has an active override
- **THEN** both list and get for that id report `origin = "cloud"` and `locally_overridden = true`, returning the override's content
#### Scenario: No existence leak for invisible cloud skills
- **WHEN** a get is issued for a cloud skill id that is unknown or not entitled to this host and has no override
- **THEN** the read surface returns a not-found result that does not reveal whether the id exists elsewhere
@@ -0,0 +1,64 @@
## ADDED Requirements
### Requirement: Administrators manage cloud-origin skills in the Cloud platform database
The Cloud Control Plane SHALL persist administrator-managed cloud-origin Skills (both `knowledge` and `flow_template` kinds, reusing the shared Skill metadata model) in the Cloud platform database, and SHALL expose authenticated, CSRF-protected, scope-guarded operations to list, create, update, and delete them. A cloud skill's metadata and content SHALL be validated before persistence; invalid input SHALL be rejected without partial writes.
#### Scenario: Administrator creates a cloud skill
- **WHEN** an authorized administrator submits a valid cloud skill (kind, name, model/model-agnostic content, tags, and for flow templates steps and a parameters schema)
- **THEN** the Cloud Control Plane persists it in the Cloud database and returns its non-secret metadata
#### Scenario: Invalid cloud skill is rejected
- **WHEN** an administrator submits a cloud skill with a blank name, unsupported kind, malformed flow-template steps, or duplicate name
- **THEN** the Cloud Control Plane rejects the write without creating or changing a skill
#### Scenario: Non-administrator attempts to mutate a cloud skill
- **WHEN** a principal without the cloud-skill admin scope invokes a cloud-skill mutation endpoint
- **THEN** the Cloud Control Plane rejects the request before reading or modifying any cloud skill
### Requirement: Cloud skills are entitled per host
The Cloud Control Plane SHALL scope each cloud skill's visibility to the set of hosts it is explicitly entitled to, via a skill×host entitlement mapping that administrators manage. An agent SHALL receive, via sync, only the cloud skills entitled to its own host; a skill entitled to no hosts (or to other hosts only) SHALL NOT be visible to that agent.
#### Scenario: Administrator grants entitlement to a host
- **WHEN** an authorized administrator entitles a cloud skill to a specific host
- **THEN** that host's next sync may include that skill, and hosts not entitled never receive it
#### Scenario: Administrator revokes entitlement from a host
- **WHEN** an authorized administrator revokes a cloud skill's entitlement to a host
- **THEN** that host's next incremental sync reports the skill as removed for that host
### Requirement: Cloud serves incremental per-host sync deltas
The Cloud Control Plane SHALL expose a host-scoped endpoint (authenticated with the same host-scoped bearer credential used for heartbeat and planner-decision) that returns an incremental delta of that host's entitled cloud skills. The Cloud SHALL maintain a monotonic per-host entitlement version that advances on any change to that host's entitled set or to an entitled skill's content, and SHALL accept a `since_version` parameter: when present and still servable, the response carries only upserted skills, `removed_ids`, and the new `latest_version`; when absent or too old, the response is a full replace of that host's entitled set.
#### Scenario: First sync is a full replace
- **WHEN** an agent syncs its host without a `since_version`
- **THEN** the Cloud Control Plane returns the host's full entitled set marked as a full replace, plus the current entitlement version
#### Scenario: Incremental sync returns only changes
- **WHEN** an agent syncs its host with a recent `since_version` and entitlements or skill content have changed since
- **THEN** the Cloud Control Plane returns only the upserted skills and `removed_ids` since that version, plus the new `latest_version`
#### Scenario: Sync request from a foreign or unauthenticated host is rejected
- **WHEN** a sync request omits valid host-scoped credentials or presents credentials bound to a different host
- **THEN** the Cloud Control Plane rejects it without disclosing any skill content
### Requirement: Agents report their local-skill inventory to the Cloud
The Cloud Control Plane SHALL accept a best-effort, host-scoped, read-only inventory of an agent's local skills (authored skills and active overrides, metadata only — no secrets), so the Console can display per-host local skills. A failed or absent inventory report SHALL NOT impair the agent's local operation or its entitlement to cloud skills.
#### Scenario: Agent reports its local inventory
- **WHEN** an agent submits its local-skill inventory over the host-scoped channel
- **THEN** the Cloud Control Plane records the reported metadata keyed by host, without persisting any secret or full-content obligation
#### Scenario: Inventory report failure is non-fatal
- **WHEN** an agent's inventory report cannot be delivered or is rejected
- **THEN** the agent's local skills, authoring, and sync behaviour are unaffected
### Requirement: Cloud Console provides a skill management surface
The Cloud Console SHALL provide an administrator-only view to create, edit, and delete cloud skills, to assign and revoke per-host entitlement, and to view each host's reported local-skill inventory (read-only). Mutating actions in the Console SHALL go through the same scope-guarded, CSRF-protected Cloud API endpoints as direct API use.
#### Scenario: Administrator manages cloud skills and entitlements in the Console
- **WHEN** an administrator opens the Skills management view
- **THEN** they can create/edit/delete cloud skills, assign or revoke per-host entitlement, and see each host's reported local skills, all through the admin-authenticated Console
#### Scenario: Read-only observer of local inventory
- **WHEN** an administrator views a host's local-skill inventory in the Console
- **THEN** the inventory is displayed read-only with no ability to mutate an agent's local skills from the Console
@@ -0,0 +1,46 @@
## MODIFIED 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. The tools SHALL present a unified catalog merged from cloud-synced skills and local skills (delegating to the `local-skill-management` read-merge surface), and SHALL return each skill with an `origin` of `"cloud"` or `"local"` and a `locally_overridden` flag when a local override shadows a cloud skill. The responses SHALL NOT expose any entitlement, subscription, or cloud-management-internal fields to the LLM.
#### Scenario: LLM lists available skills across both origins
- **WHEN** an MCP client calls `list_skills`
- **THEN** it receives the merged set of cloud-synced and local skills visible to it, each with id, name, description, kind, tags, `origin`, and `locally_overridden`
#### Scenario: LLM searches across both origins
- **WHEN** an MCP client calls `search_skills` with a query string
- **THEN** it receives matching skills from both origins ranked by relevance, each tagged with `origin`
#### Scenario: LLM fetches a skill, observing any active override
- **WHEN** an MCP client calls `get_skill` with a skill id that has an active local override
- **THEN** it receives the override's content, tagged `origin = "cloud"` and `locally_overridden = true`
### Requirement: Skill MCP errors are semantic
The system SHALL translate skill-level errors (skill not found, skill not visible/entitled, invalid/unavailable flow template, and authoring errors such as attempting to delete a cloud skill with no override) into clear, semantic MCP tool error responses, consistent in style with the device-capability tool error handling. A request for a cloud skill that is unknown or not visible to the caller SHALL produce an indistinguishable not-found error (no existence leak).
#### Scenario: Requesting a non-visible cloud skill
- **WHEN** `get_skill` is called with a cloud skill id that exists but is not visible to the caller's host and has no override
- **THEN** the tool returns a semantic "not found" error rather than a raw database or internal exception
#### Scenario: Deleting a cloud skill that has no override
- **WHEN** `delete_skill` is called with a cloud skill id that has no local override
- **THEN** the tool returns a semantic error indicating there is no local override to remove, without revoking any cloud entitlement
## ADDED Requirements
### Requirement: Authoring MCP tools dispatch by origin
The system SHALL expose `create_skill`, `update_skill`, and `delete_skill` as MCP tools on the agent's MCP server, always registered (no enable/disable gate). Their behaviour SHALL dispatch by the target skill's origin: `create_skill` creates a new local skill; `update_skill` edits a local skill or creates/updates a local override for a cloud skill; `delete_skill` deletes a local skill or removes a local override for a cloud skill. None of these tools SHALL ever write to the synced store or mutate a cloud skill directly; override semantics are governed by the `local-skill-management` capability.
#### Scenario: Create a new local skill
- **WHEN** the LLM calls `create_skill` with content for a new skill
- **THEN** a new local skill is created with `origin = "local"` and is thereafter discoverable via the read tools
#### Scenario: Update dispatches by origin
- **WHEN** the LLM calls `update_skill` with a local skill id
- **THEN** the local skill is edited; and **WHEN** called with a cloud skill id
- **THEN** a local override for that cloud id is created or updated (the cloud/synced store is untouched)
#### Scenario: Delete dispatches by origin
- **WHEN** the LLM calls `delete_skill` with a local skill id
- **THEN** the local skill is deleted; and **WHEN** called with a cloud skill id that has an override
- **THEN** the override is removed and the cloud skill resurfaces on the next read
@@ -0,0 +1,53 @@
## REMOVED Requirements
### Requirement: Pull-based sync from the Subscription Platform
Replaced by per-host incremental pull from this project's Cloud API (the external Subscription Platform is no longer the source).
### Requirement: Subscription-based visibility enforcement
Replaced by per-host entitlement visibility enforcement.
## ADDED Requirements
### Requirement: Pull-based incremental sync from the Cloud API
The agent SHALL periodically pull an incremental delta of its own host's entitled cloud skills from this project's Cloud API (not an external Subscription Platform), using the host-scoped bearer credential already used for heartbeat and planner-decision. The Cloud API maintains a monotonic per-host entitlement version; the agent supplies the last version it successfully applied and the Cloud returns only upserted skills, `removed_ids`, and the new `latest_version`, or a full replace when the agent has no prior version or its version is too old to serve incrementally. The transport-replaceable client interface is preserved — only the concrete upstream and endpoint change.
#### Scenario: Agent syncs incrementally after entitlement or content change
- **WHEN** the agent pulls with a recent `since_version` and its host's entitled set or an entitled skill's content has changed
- **THEN** only the upserted skills and `removed_ids` since that version are applied to the local synced store, and the agent records the new `latest_version`
#### Scenario: Agent performs a full replace on first sync or stale version
- **WHEN** the agent has no prior version, or its `since_version` is too old for the Cloud to serve incrementally
- **THEN** the agent applies a full replace of its host's entitled set and records the returned `latest_version`
### Requirement: Per-host entitlement visibility enforcement
The agent's synced catalog SHALL contain only cloud skills entitled to that agent's own host, and the read surface SHALL re-check visibility at query time so that an entitlement revoked before the next sync completes takes effect as soon as the revocation is reflected in the local synced state. Skills entitled to other hosts only SHALL never be visible.
#### Scenario: Revoked skill disappears after the next incremental sync
- **WHEN** the Cloud reports a previously-entitled skill in `removed_ids` for this host
- **THEN** the next sync removes it from the local synced store and it is no longer visible on the read surface
#### Scenario: Only this host's entitled skills are visible
- **WHEN** the read surface lists cloud skills
- **THEN** only skills entitled to this host are returned, never skills entitled solely to other hosts
### Requirement: The sync runner runs inside the agent process
The agent SHALL construct and run the skill sync runner within the host-agent process so that the synced catalog stays current without manual intervention, on a configurable poll interval. A failed sync attempt SHALL leave the last-known-good synced catalog intact and queryable, and SHALL be observable rather than silently discarded.
#### Scenario: Sync runs automatically in the running agent
- **WHEN** the host agent is running with cloud-transport configured
- **THEN** the sync runner periodically pulls the host's entitled skills and keeps the local synced catalog current without any manual step
#### Scenario: Sync failure preserves the catalog
- **WHEN** a sync attempt fails
- **THEN** the existing synced catalog remains intact and queryable, and the failure is recorded for observability
### Requirement: Agent reports its local-skill inventory to the Cloud
The agent SHALL periodically report a read-only inventory of its local skills (authored skills and active overrides, metadata only) to the Cloud API over the host-scoped channel, so the Console can display per-host local skills. Reporting is best-effort: a failure to report SHALL NOT affect local authoring, overrides, sync, or the agent's cloud entitlements.
#### Scenario: Agent reports local inventory periodically
- **WHEN** the agent has local skills or active overrides
- **THEN** it reports their metadata to the Cloud on a periodic best-effort basis
#### Scenario: Report failure is non-fatal
- **WHEN** the inventory report cannot be delivered
- **THEN** no local skill operation, override, or sync behaviour is impaired