feat(serve): expose skill installation paths (#6811)

This commit is contained in:
callmeYe authored and GitHub committed 2026-07-13 09:29:09 +00:00
1 parent 3d5dd41bc7
commit be4c46d085
8 files changed
+55 -4

No files matched your search

@@ -0,0 +1,33 @@
# Workspace Skill Installation Paths
Date: 2026-07-13
## Contract
Each skill returned by `GET /workspace/skills` and
`GET /workspaces/:workspace/skills` includes `installedPath`, the existing
absolute `SkillConfig.filePath` that points to its `SKILL.md` file. The value is
copied as stored; the status layer does not resolve symlinks or canonicalize it
again.
## Compatibility
This is an additive v1 field. The current daemon always emits it, while the ACP
bridge and TypeScript SDK public status types keep it optional so clients stay
compatible with older daemons. The protocol version and capability list do not
change.
## Data Flow
`SkillManager.listSkills()` supplies `SkillConfig` records. The shared
`mapSkillConfigToStatus()` function copies `filePath` to `installedPath`. Both
the live ACP snapshot and daemon-local fallback use that mapper, so project,
user, bundled, extension, inactive-extension, and disabled skills have the same
shape. The workspace status service forwards that shared result to both route
forms.
## Redaction Boundary
The status mapper remains an explicit metadata allowlist. It exposes the
installation file path but not the skill body, hooks, `skillRoot`, or any other
skill configuration. This change adds no UI behavior.
+8 -2
View File
@@ -819,14 +819,20 @@ Recommended poll cadence: aligned with whatever already polls `/workspace/mcp`;
"description": "Review code",
"level": "project",
"modelInvocable": true,
"installedPath": "/home/alice/project/.qwen/skills/review/SKILL.md",
"argumentHint": "[path]"
}
]
}
```
`level` is one of `project`, `user`, `extension`, or `bundled`. `errors` is
omitted when discovery succeeds.
`level` is one of `project`, `user`, `extension`, or `bundled`.
`installedPath` is the existing absolute path to the skill's `SKILL.md`; the
daemon returns it as stored without separately resolving symlinks or
canonicalizing it. Current daemons emit it for every skill, while clients must
tolerate its absence from older v1 daemons. Skill bodies, hooks, `skillRoot`,
and other skill configuration remain excluded. `errors` is omitted when
discovery succeeds.
### `GET /workspace/providers`
+1
View File
@@ -416,6 +416,7 @@ export interface ServeWorkspaceSkillStatus extends ServeStatusCell {
description: string;
level: ServeSkillLevel;
modelInvocable: boolean;
installedPath?: string;
argumentHint?: string;
model?: string;
extensionName?: string;
@@ -2560,6 +2560,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
level: 'project',
argumentHint: '[path]',
modelInvocable: true,
installedPath: '/secret/SKILL.md',
}),
expect.objectContaining({
kind: 'skill',
@@ -2577,6 +2578,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
description: 'Disabled by settings',
level: 'project',
modelInvocable: true,
installedPath: '/disabled/SKILL.md',
}),
expect.objectContaining({
kind: 'skill',
@@ -2595,6 +2597,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
level: 'extension',
extensionName: 'GSD Core',
modelInvocable: true,
installedPath: '/ext/gsd-core/skills/gsd-display-stale/SKILL.md',
}),
expect.objectContaining({
kind: 'skill',
@@ -2604,6 +2607,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
level: 'extension',
extensionName: 'GSD Core',
modelInvocable: true,
installedPath: '/ext/gsd-core/skills/gsd-config-only/SKILL.md',
}),
]),
});
@@ -2622,7 +2626,7 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(JSON.stringify(skills)).not.toContain('extension secret body');
expect(JSON.stringify(skills)).not.toContain('display stale body');
expect(JSON.stringify(skills)).not.toContain('config only body');
expect(JSON.stringify(skills)).not.toContain('/secret');
expect(JSON.stringify(skills)).not.toContain('"skillRoot"');
expect(JSON.stringify(skills)).not.toContain('secret-hook');
expect(providers).toMatchObject({
@@ -13,8 +13,10 @@ function makeSkill(overrides: Partial<SkillConfig> = {}): SkillConfig {
name: 'review',
description: 'Review changed code',
level: 'bundled',
filePath: '/skills/review/SKILL.md',
body: 'Review instructions',
...overrides,
} as SkillConfig;
};
}
describe('mapSkillConfigToStatus', () => {
@@ -31,6 +33,7 @@ describe('mapSkillConfigToStatus', () => {
level: 'bundled',
modelInvocable: true,
argumentHint: '[pr-number]',
installedPath: '/skills/review/SKILL.md',
});
});
@@ -28,6 +28,7 @@ export function mapSkillConfigToStatus(
description: skill.description,
level: skill.level,
modelInvocable,
installedPath: skill.filePath,
...(skill.argumentHint ? { argumentHint: skill.argumentHint } : {}),
...(skill.model ? { model: skill.model } : {}),
...(skill.extensionName ? { extensionName: skill.extensionName } : {}),
@@ -99,10 +99,12 @@ describe('createWorkspaceSkillsStatusProvider', () => {
{
name: 'enabled',
status: 'ok',
installedPath: '/skills/enabled/SKILL.md',
},
{
name: 'disabled',
status: 'disabled',
installedPath: '/skills/disabled/SKILL.md',
},
]);
});
@@ -1073,6 +1073,7 @@ export interface DaemonWorkspaceSkillStatus extends DaemonStatusCell {
description: string;
level: DaemonSkillLevel;
modelInvocable: boolean;
installedPath?: string;
argumentHint?: string;
model?: string;
extensionName?: string;