Skip to content
10 changes: 6 additions & 4 deletions docs/design/2026-07-20-skills-default-disabled.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ Effective disables are `disabled + (defaultDisabled - enabled)`. An explicit `en

One CLI-local resolver computes the effective disabled names and whether each disabled skill is `hard` or `default`. Existing runtime consumers continue reading the effective set through `Config.getDisabledSkillNames()`; core skill discovery and execution APIs do not change.

The `/skills` picker and daemon toggle apply the same rules:
The `/skills` picker continues to operate on discovered Skills. Daemon toggle
routes instead persist settings by requested name without consulting that
catalog:

- enabling removes a workspace hard disable and adds the canonical name to workspace `skills.enabled` only when needed;
- disabling removes the workspace opt-in and adds the canonical name to workspace `skills.disabled`;
- higher-scope `skills.disabled` entries remain locked;
- enabling removes a workspace hard disable and adds the requested name to workspace `skills.enabled` only when needed;
- disabling removes the workspace opt-in and adds the requested name to workspace `skills.disabled`;
- higher-scope `skills.disabled` entries remain authoritative for effective availability but do not block workspace scope from recording or removing its own declaration;
- unrelated and unavailable skill entries are preserved.

Workspace skill status adds a disable reason and optional lock scope so clients can distinguish a hard lock from an overridable default. The daemon-local and ACP status paths both read the same CLI-local resolver.
Expand Down
51 changes: 31 additions & 20 deletions docs/design/daemon-skill-batch-toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

## Problem

Remote Skill managers can toggle only one Skill per request. Closing several
Skills therefore requires client-side request orchestration and provides no
single response that records all target outcomes.
Remote Skill managers need both single and batch mutations to behave like
workspace settings writes. A runtime Skill snapshot is not an ownership source
for `skills.disabled` or `skills.enabled`: disabled entries and applicable
default-disabled opt-ins may be declared before installation and may
intentionally outlive the currently loaded catalog.

## API

Expand All @@ -24,16 +26,19 @@ The request body is:

`skillNames` is a non-empty string array with at most 100 entries. Names are
trimmed and deduplicated case-insensitively while preserving first-seen order.
The response is best-effort for expected target errors: installed targets are
validated against one status snapshot, all valid names are persisted in one
locked write, and changes are applied with one live-session refresh. Names
that are not installed remain valid so callers can declare their state before
installation. Enabling one removes a matching workspace `skills.disabled`
entry and is otherwise a no-op, except for the existing `defaultDisabled`
override behavior; disabling one writes `skills.disabled`. Hidden,
inactive-extension, and locked targets are returned without blocking valid
targets. Unexpected persistence and runtime-generation failures fail the whole
request.
The daemon does not read or validate against runtime Skill status. It applies
all resulting declaration changes in at most one locked settings write and,
when anything changed, performs one live-session refresh. Enabling one removes
a matching workspace `skills.disabled` entry, preserves and normalizes an
existing workspace `skills.enabled` declaration, or records an opt-in for an
effective `skills.defaultDisabled` entry. With no existing workspace
declaration and no effective `skills.defaultDisabled` entry, enable is a no-op
(`changed: false`). Disabling writes `skills.disabled`. Unknown,
non-user-invocable, inactive-Extension, and higher-scope-disabled names use the
same settings path. Higher scopes still determine effective availability after
settings merge, but do not prevent the workspace scope from recording its own
declaration. Unexpected persistence and runtime-generation failures fail the
whole request.

```json
{
Expand Down Expand Up @@ -62,19 +67,25 @@ request.
}
```

`results` and `errors` each preserve request order within their own array;
the response does not reconstruct the original mixed ordering, so clients
re-match targets by `skillName`.
`results` preserves request order. `errors` remains present for wire
compatibility and is empty for structurally valid names. Batch `activation`
reflects child liveness and any required shared refresh independently from each
result's `changed` flag; an all-no-op batch can therefore be `applied` or
`deferred`, and no refresh occurs.

Malformed requests still fail as a whole with HTTP 400. Workspace trust,
authentication, client identity, and generation ownership use the same gates
as the single-Skill route.

## Compatibility

Advertise `workspace_skill_batch_toggle` separately from
`workspace_skill_toggle`. Clients must pre-flight the new capability before
calling the collection route. The existing single-Skill route and response
remain unchanged. The collection routes are HTTP-only: the ACP
Advertise `workspace_skill_settings_batch_toggle` separately from
`workspace_skill_settings_toggle`. These tags replace the retired
`workspace_skill_batch_toggle` and `workspace_skill_toggle` tags, whose
catalog-validated contract is incompatible with settings-only writes. Clients
must pre-flight the settings capability before calling the unchanged route.
The single-Skill route returns the trimmed request name because there is no
catalog lookup from which to obtain a canonical spelling. The collection routes
are HTTP-only: the ACP
`_qwen/workspace/skills` dispatch surface stays read-only, matching the
single-Skill toggle.
40 changes: 18 additions & 22 deletions docs/design/daemon-skill-toggle.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,25 @@

## Goal

Expose the CLI `/skills` panel's workspace enable/disable behavior through daemon REST and the TypeScript SDK, including immediate refresh of active ACP sessions.
Expose workspace Skill settings writes through daemon REST and the TypeScript SDK, including immediate refresh of active ACP sessions without making the runtime Skill catalog an ownership source.

## Public contract

- `POST /workspace/skills/:name/enable`
- `POST /workspaces/:workspace/skills/:name/enable`
- Request body: `{ "enabled": boolean }`
- SDK: `DaemonClient.setWorkspaceSkillEnabled` and `WorkspaceDaemonClient.setWorkspaceSkillEnabled`
- Capability: `workspace_skill_toggle`
- Capability: `workspace_skill_settings_toggle`

The response contains the canonical skill name, requested state, whether persistence changed, activation state, and session refresh counts. `applied` means every active session refreshed, `deferred` means no ACP child was running, and `partial` means at least one session failed to refresh after persistence committed.
The response contains the trimmed requested name, requested state, whether persistence changed, activation state, and session refresh counts. `activation` reflects child liveness and any required refresh independently from `changed`: `applied` means a child was live and any required refresh succeeded, `deferred` means no child was live at the liveness check or a changed request lost its child/session during the required refresh, and `partial` means at least one other required refresh failed after persistence committed. A no-op can therefore be `applied` or `deferred` while `changed` remains false.

## Semantics

The API changes workspace `skills.disabled` and `skills.enabled` as needed. Skill lookup is case-insensitive, but the canonical discovered name is persisted. Enabling a default-disabled skill writes an explicit opt-in; disabling it removes the opt-in and writes a hard workspace disable. Updating one target removes target duplicates and case variants without deleting orphan entries for unavailable skills. A second identical request is a no-op.
The API changes workspace `skills.disabled` and `skills.enabled` by name without consulting the runtime Skill catalog. Enabling a default-disabled Skill writes an explicit opt-in; disabling it removes the opt-in and writes a hard workspace disable. Updating one target removes target duplicates and case variants without deleting orphan entries for unavailable Skills. A name may be disabled before installation, while hidden from user invocation, or while its Extension is inactive. Enabling removes an existing workspace disable or records an opt-in for an effective `skills.defaultDisabled` entry. An existing workspace `skills.enabled` declaration is preserved and normalized to the requested casing. With no existing workspace declaration and no effective `skills.defaultDisabled` entry, enable is a no-op (`changed: false`). A second identical request is also a no-op.
Comment thread
callmeYe marked this conversation as resolved.

The route rejects states the CLI panel cannot toggle:
A hard `skills.disabled` entry inherited from a higher scope remains authoritative for effective availability, but does not prevent workspace scope from recording or removing its own declaration. Workspace declarations otherwise participate in the usual `skills.disabled > skills.enabled > skills.defaultDisabled` resolution and can override higher-scope `skills.defaultDisabled` or `skills.enabled` entries. The route retains request-shape, authentication, client identity, workspace trust, and runtime-generation gates; none of those require a Skill catalog lookup.

- unknown skill: `404 skill_not_found`;
- `userInvocable === false`: `409 skill_not_toggleable`;
- skill from an inactive extension: `409 skill_not_toggleable`;
- disabled in system defaults, user, or system scope: `409 skill_not_toggleable` with the locking scope;
- untrusted workspace: `403 untrusted_workspace`.

The scope lock check and workspace read-modify-write happen inside the daemon's per-workspace settings lock. A failed write stops before refresh and event publication.
The workspace read-modify-write happens inside the daemon's per-workspace settings lock. A failed write stops before refresh and event publication.

## Skill availability versus `disable-model-invocation`

Expand All @@ -36,13 +30,14 @@ The scope lock check and workspace read-modify-write happen inside the daemon's

## Activation flow

1. Resolve the canonical, toggleable skill from the workspace status snapshot.
2. Under the workspace settings lock, re-read every scope, reject higher-scope locks, and commit the canonical workspace list.
3. Invalidate the daemon's cached skill status.
4. If an ACP child is live, invoke `qwen/control/workspace/skills/refresh`.
5. The child reloads workspace-scope settings and refreshes every active session, including busy sessions.
6. Each session reloads its own workspace settings, rebuilds and pushes `available_commands_update`, and notifies SkillManager consumers.
7. Publish the existing workspace `settings_changed` event for each changed skill-settings key.
1. Validate the request name, authorization, workspace trust, client identity, and runtime generation.
2. Under the workspace settings lock, re-read every scope, compute the resulting workspace declaration changes, and commit them in at most one write.
3. If no declaration changed, return `changed: false` without cache invalidation, refresh, or event publication.
4. Otherwise, invalidate the daemon's cached skill status.
5. If an ACP child is live, invoke `qwen/control/workspace/skills/refresh`.
6. The child reloads workspace-scope settings and refreshes every active session, including busy sessions.
7. Each session reloads its own workspace settings, rebuilds and pushes `available_commands_update`, and notifies SkillManager consumers.
8. Publish the existing workspace `settings_changed` event for each changed skill-settings key.

An in-flight model request cannot be rewritten. Subsequent skill execution checks, command snapshots, and model contexts read the new state.

Expand All @@ -53,14 +48,15 @@ An in-flight model request cannot be rewritten. Subsequent skill execution check
- Slash commands: available-command construction removes disabled skills and sends updated command metadata to daemon clients.
- Model context: SkillManager change listeners refresh the Skill tool description and available-skill context.
- Execution validation: the Skill tool re-reads the disabled-name provider before invocation, so later calls are rejected immediately.
- Extension state: inactive extension skills remain non-toggleable even when they are not disabled by settings.
- Extension state: inactive Extensions still keep their Skills unavailable at runtime, independently of whether workspace settings record those names.
- Daemon cache: the cached live-child skill snapshot is invalidated after persistence so later GET requests cannot replay stale state.
- SDK consumers: both primary-workspace and workspace-qualified clients share the response and error contract.
- SDK consumers: both primary-workspace and workspace-qualified clients share the settings-only response contract.
- Events: existing `settings_changed` consumers observe each committed `skills.disabled` or `skills.enabled` value; there is no new event type.

## Failure behavior

- Persistence failure: the HTTP request fails; no ACP refresh and no event.
- No child: persistence succeeds with `deferred`; the next child loads the setting at startup.
- No child after a declaration changed: persistence succeeds with `deferred`; the next child loads the setting at startup.
- No declaration change: the response reports `changed: false`; no refresh or event occurs. `activation` still reflects whether a child was live, but no activation work is needed.
- Per-session refresh failure: persistence remains committed; successful sessions stay refreshed and the response is `partial`.
- Child transport race: if the child disappears after the liveness check, the response is `deferred`; other refresh failures are reported as `partial`.
4 changes: 2 additions & 2 deletions docs/developers/daemon/11-capabilities-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ V2 Extension batch activation: `extension_batch_activation_v2` adds queued globa

Workspace-qualified session reads: `workspace_persisted_transcript`, `workspace_session_export`, `workspace_archived_session_export`, `workspace_session_live_state`. The active and archived export tags are independent from each other and from `session_export` and `workspace_qualified_rest_core`, so clients must pre-flight the exact storage state they intend to export. Persisted transcript paging permits an untrusted secondary under its bounded read policy; both full export paths remain trusted-only. `workspace_session_live_state` is likewise independent from `workspace_qualified_rest_core` and is trusted-only: it serves the selected runtime's memory-only live-session snapshot and catalog version and does not extend the untrusted-secondary persisted read policy to live bridge state.

Workspace mutation (Wave 4+): `workspace_memory`, `workspace_agents`, `workspace_agent_generate`, `workspace_acp_preheat`, `workspace_tool_toggle`, **`workspace_settings`** (conditional), `workspace_permissions`, `workspace_init`, `workspace_github_setup`, `workspace_trust`, `workspace_mcp_restart`, `workspace_mcp_manage`, `workspace_file_read`, `workspace_file_bytes`, `workspace_file_read_cursor`, `workspace_file_write`, `workspace_file_upload`, **`workspace_reload`** (conditional).
Workspace mutation (Wave 4+): `workspace_memory`, `workspace_agents`, `workspace_agent_generate`, `workspace_acp_preheat`, `workspace_tool_toggle`, `workspace_skill_settings_toggle`, `workspace_skill_settings_batch_toggle`, **`workspace_settings`** (conditional), `workspace_permissions`, `workspace_init`, `workspace_github_setup`, `workspace_trust`, `workspace_mcp_restart`, `workspace_mcp_manage`, `workspace_file_read`, `workspace_file_bytes`, `workspace_file_read_cursor`, `workspace_file_write`, `workspace_file_upload`, **`workspace_reload`** (conditional). The two Skill settings tags replace the retired catalog-validated `workspace_skill_toggle` and `workspace_skill_batch_toggle` tags.

MCP guardrails: **`mcp_guardrails`** (`modes: ['warn', 'enforce']`), `mcp_guardrail_events`, `mcp_server_runtime_mutation`, **`mcp_workspace_pool`** (conditional), **`mcp_pool_restart`** (conditional).

Expand Down Expand Up @@ -185,7 +185,7 @@ sequenceDiagram
## State and lifecycle

- `CAPABILITIES_SCHEMA_VERSION` is the wire envelope shape version, currently `1`. Bump it only for an envelope break.
- `SERVE_PROTOCOL_VERSION = 'v1'` is the protocol-feature version. Adding features inside v1 is additive; old clients do not see new behavior unless they preflight the new tag. Removing a feature is a v2 break.
- `SERVE_PROTOCOL_VERSION = 'v1'` is the protocol-feature version. Adding features inside v1 is additive; old clients do not see new behavior unless they preflight the new tag. Corrected behavior may replace a capability inside v1: the replacement tag supersedes the old tag, the old tag stops being advertised, and clients must preflight the replacement. Removing a feature without a replacement is a v2 break.
- `EVENT_SCHEMA_VERSION = 1` is the SSE frame `v` field (see [`09-event-schema.md`](./09-event-schema.md)). It is an independent version axis; bumping event schema does not imply bumping protocol version, and vice versa.
- `session_resume` is the stable daemon capability for `POST /session/:id/resume`. `unstable_session_resume` remains advertised as a deprecated alias because the underlying ACP method is still named `connection.unstable_resumeSession`; new clients should feature-detect `session_resume`.

Expand Down
Loading
Loading