Planning artifacts (milestones v1.0-v1.2.1, v1.3 queue, PROJECT.md, STATE.md, config) now live alongside the code they describe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
265 lines
12 KiB
Markdown
265 lines
12 KiB
Markdown
---
|
|
phase: 19-adapter-aware-install-uninstall
|
|
plan: "02"
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["19-01"]
|
|
files_modified:
|
|
- server/src/routes/skill-registry.ts
|
|
- server/src/routes/skill-registry-groups.ts
|
|
- server/src/app.ts
|
|
autonomous: true
|
|
requirements: [INST-01, INST-02, INST-03, INST-04, HERM-02]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Route handlers resolve agentSkillsDir from agentId via resolveAdapterSkillConfig, not from request body"
|
|
- "Install route accepts agentId in body and resolves the target directory server-side"
|
|
- "Uninstall route accepts agentId as query param and passes resolved dir to service"
|
|
- "Rollback route accepts agentId in body and resolves the target directory server-side"
|
|
- "Group assign/remove routes resolve dir from the agentId URL param, not from request body"
|
|
- "DELETE route for native skills returns 403"
|
|
- "app.ts passes db to both route factories"
|
|
artifacts:
|
|
- path: "server/src/routes/skill-registry.ts"
|
|
provides: "Adapter-aware install/uninstall/rollback routes with agentId resolution"
|
|
contains: "resolveSkillsDirForAgent"
|
|
- path: "server/src/routes/skill-registry-groups.ts"
|
|
provides: "Adapter-aware group assign/remove routes"
|
|
contains: "resolveSkillsDirForAgent"
|
|
- path: "server/src/app.ts"
|
|
provides: "db passed to skillRegistryRoutes and skillGroupRoutes"
|
|
contains: "skillRegistryRoutes(db)"
|
|
key_links:
|
|
- from: "server/src/routes/skill-registry.ts"
|
|
to: "server/src/services/agents.ts"
|
|
via: "agentService(db).getById for adapter type lookup"
|
|
pattern: "agentService.*getById"
|
|
- from: "server/src/routes/skill-registry.ts"
|
|
to: "@paperclipai/adapter-utils"
|
|
via: "resolveAdapterSkillConfig for path resolution"
|
|
pattern: "resolveAdapterSkillConfig"
|
|
- from: "server/src/app.ts"
|
|
to: "server/src/routes/skill-registry.ts"
|
|
via: "skillRegistryRoutes(db) factory call"
|
|
pattern: "skillRegistryRoutes\\(db\\)"
|
|
---
|
|
|
|
<objective>
|
|
Wire route handlers to resolve skill directories from agentId server-side using the Phase 18 adapter path resolver, replacing client-supplied agentSkillsDir in all request bodies.
|
|
|
|
Purpose: The UI should never compute filesystem paths. The server owns path resolution via the agent's adapter type. This plan connects the service changes from Plan 01 to the HTTP layer.
|
|
|
|
Output: Updated routes accepting agentId, app.ts passing db to route factories, native skill protection at route level.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/19-adapter-aware-install-uninstall/19-RESEARCH.md
|
|
@.planning/phases/18-adapter-path-resolver/18-01-SUMMARY.md
|
|
@.planning/phases/19-adapter-aware-install-uninstall/19-01-SUMMARY.md
|
|
|
|
Read these source files before modifying:
|
|
- server/src/routes/skill-registry.ts
|
|
- server/src/routes/skill-registry-groups.ts
|
|
- server/src/app.ts
|
|
- server/src/services/agents.ts (read-only — understand getById signature)
|
|
- packages/adapter-utils/src/index.ts (read-only — understand resolveAdapterSkillConfig export)
|
|
|
|
<interfaces>
|
|
<!-- From Phase 18 (adapter-utils) -->
|
|
resolveAdapterSkillConfig(adapterType: string): AdapterSkillConfig
|
|
returns: { skillDir: string | null, nativeSkillDir: string | null, format: string, supportsInstall: boolean, unsupportedReason?: string }
|
|
|
|
<!-- From Plan 01 (service layer updates) -->
|
|
install(skillId: string, agentSkillsDir: string): Promise<...>
|
|
uninstall(skillId: string, agentSkillsDir: string): Promise<void>
|
|
rollback(skillId: string, versionId: string, agentSkillsDir: string): Promise<...>
|
|
assignGroup(groupId: string, agentId: string, agentSkillsDir: string): Promise<...>
|
|
removeGroup(groupId: string, agentId: string, agentSkillsDir: string): Promise<...>
|
|
syncHermesNativeSkills(agentId: string): Promise<void>
|
|
listAgentSkills(agentId: string): Promise<Array<{ skillId: string; source: "managed" | "native"; installedAt: number }>>
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add resolveSkillsDirForAgent helper and update route factories</name>
|
|
<files>
|
|
server/src/routes/skill-registry.ts,
|
|
server/src/routes/skill-registry-groups.ts,
|
|
server/src/app.ts
|
|
</files>
|
|
<read_first>
|
|
server/src/routes/skill-registry.ts,
|
|
server/src/routes/skill-registry-groups.ts,
|
|
server/src/app.ts,
|
|
server/src/services/agents.ts
|
|
</read_first>
|
|
<action>
|
|
**Add shared helper** — either at the top of `skill-registry.ts` (and import in groups) or in a small shared file. The helper resolves agentId to a skill directory:
|
|
|
|
```typescript
|
|
import { agentService } from "../services/agents.js";
|
|
import { resolveAdapterSkillConfig } from "@paperclipai/adapter-utils";
|
|
import os from "node:os";
|
|
import type { Db } from "@paperclipai/db";
|
|
|
|
async function resolveSkillsDirForAgent(db: Db, agentId: string): Promise<string> {
|
|
const agent = await agentService(db).getById(agentId);
|
|
if (!agent) throw Object.assign(new Error("Agent not found"), { status: 404 });
|
|
const config = resolveAdapterSkillConfig(agent.adapterType);
|
|
if (!config.supportsInstall || !config.skillDir) {
|
|
throw Object.assign(
|
|
new Error(config.unsupportedReason ?? "Adapter does not support skill install"),
|
|
{ status: 422 },
|
|
);
|
|
}
|
|
return config.skillDir.replace(/^~/, os.homedir());
|
|
}
|
|
```
|
|
|
|
**skill-registry.ts** — Change factory signature to `skillRegistryRoutes(db: Db): Router`:
|
|
|
|
1. **Install route** (`POST .../install`):
|
|
- Change body from `{ agentSkillsDir: string }` to `{ agentId: string }`
|
|
- Validate: `if (!agentId) return res.status(400).json({ error: "agentId required" });`
|
|
- Resolve: `const agentSkillsDir = await resolveSkillsDirForAgent(db, agentId);`
|
|
- Pass resolved dir to `svc.install(skillId, agentSkillsDir)`
|
|
- Wrap in try/catch — if error has `.status`, use it; otherwise 500
|
|
|
|
2. **Uninstall route** (`DELETE ...`):
|
|
- Add `req.query.agentId` (query param for DELETE, per HTTP semantics)
|
|
- Validate: `if (!agentId) return res.status(400).json({ error: "agentId required" });`
|
|
- Resolve dir, pass to `svc.uninstall(skillId, agentSkillsDir)`
|
|
|
|
3. **Rollback route** (`POST .../rollback`):
|
|
- Change body from `{ versionId, agentSkillsDir }` to `{ versionId, agentId }`
|
|
- Resolve dir, pass to `svc.rollback(skillId, versionId, agentSkillsDir)`
|
|
|
|
4. **List agent skills route** (`GET .../agents/:agentId/skills`):
|
|
- Look up agent to check if adapter is hermes_local
|
|
- If hermes: call `syncHermesNativeSkills(agentId)` before listing
|
|
- Return the full object array (not string[])
|
|
|
|
5. **Native skill protection** (HERM-02):
|
|
- In DELETE route, before calling uninstall, check if the skill's `agentSkills` row has `source === 'native'`
|
|
- If native: `return res.status(403).json({ error: "Cannot remove native skills" });`
|
|
|
|
**skill-registry-groups.ts** — Change factory signature to `skillGroupRoutes(db: Db): Router`:
|
|
|
|
1. **Assign group route** (`POST .../groups`):
|
|
- Remove `agentSkillsDir` from body — the `agentId` is already in the URL param
|
|
- Resolve: `const agentSkillsDir = await resolveSkillsDirForAgent(db, req.params.agentId);`
|
|
- Pass resolved dir to `svc.assignGroup(..., agentSkillsDir)`
|
|
|
|
2. **Remove group route** (`DELETE .../groups/:groupId`):
|
|
- Remove `agentSkillsDir` from body
|
|
- Resolve dir from URL param `agentId`
|
|
- Pass resolved dir to `svc.removeGroup(..., agentSkillsDir)`
|
|
|
|
**app.ts** — Update mount calls:
|
|
- `api.use(skillRegistryRoutes(db));` (was: `skillRegistryRoutes()`)
|
|
- `api.use(skillGroupRoutes(db));` (was: `skillGroupRoutes()`)
|
|
- Add `Db` import if not present
|
|
|
|
**Error handling pattern** for resolveSkillsDirForAgent errors in routes:
|
|
```typescript
|
|
try {
|
|
const agentSkillsDir = await resolveSkillsDirForAgent(db, agentId);
|
|
// ... proceed
|
|
} catch (err: any) {
|
|
const status = err.status ?? 500;
|
|
return res.status(status).json({ error: err.message });
|
|
}
|
|
```
|
|
</action>
|
|
<verify>
|
|
<automated>cd nexus && pnpm tsc --noEmit --project server/tsconfig.json 2>&1 | head -30</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- skillRegistryRoutes accepts db: Db parameter
|
|
- skillGroupRoutes accepts db: Db parameter
|
|
- app.ts passes db to both route factories
|
|
- Install route reads agentId from body, not agentSkillsDir
|
|
- Uninstall route reads agentId from query params
|
|
- Rollback route reads agentId from body, not agentSkillsDir
|
|
- Group routes resolve dir from URL agentId param
|
|
- resolveSkillsDirForAgent helper exists and uses resolveAdapterSkillConfig
|
|
- DELETE route checks source=native and returns 403
|
|
- List agent skills route calls syncHermesNativeSkills for hermes agents
|
|
- No reference to agentSkillsDir in request bodies
|
|
- No reference to defaultSkillsDir anywhere
|
|
- TypeScript compiles without errors
|
|
</acceptance_criteria>
|
|
<done>All route handlers resolve skill directories server-side from agentId, app.ts wires db to both factories, native skill DELETE returns 403</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: Route-level integration tests</name>
|
|
<files>
|
|
server/src/__tests__/skill-registry-routes-adapter.test.ts
|
|
</files>
|
|
<read_first>
|
|
server/src/__tests__/skill-registry.test.ts (if exists — for supertest patterns),
|
|
server/src/routes/skill-registry.ts
|
|
</read_first>
|
|
<behavior>
|
|
- Test: POST install with agentId resolves correct dir and succeeds
|
|
- Test: POST install without agentId returns 400
|
|
- Test: POST install with unknown agentId returns 404
|
|
- Test: POST install with unsupported adapter returns 422
|
|
- Test: DELETE uninstall with agentId query param resolves dir and removes files
|
|
- Test: DELETE uninstall of native skill returns 403
|
|
- Test: POST rollback with agentId resolves correct dir
|
|
- Test: GET agent skills for hermes agent calls sync and returns objects with source
|
|
- Test: POST assign group resolves dir from URL agentId
|
|
</behavior>
|
|
<action>
|
|
Create `skill-registry-routes-adapter.test.ts` using supertest (following existing test patterns in `server/src/__tests__/`).
|
|
|
|
Mock `agentService(db).getById` to return agents with different adapter types. Mock `resolveAdapterSkillConfig` to return known configs. Mock filesystem operations.
|
|
|
|
Test the route-level behavior: correct status codes, correct error messages, correct delegation to service methods with resolved paths.
|
|
|
|
Run: `cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-routes-adapter.test.ts`
|
|
</action>
|
|
<verify>
|
|
<automated>cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-routes-adapter.test.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Test file exists with supertest-based route tests
|
|
- Tests cover: 400 (missing agentId), 404 (unknown agent), 422 (unsupported adapter), 403 (native skill delete)
|
|
- Tests verify install/uninstall/rollback/group routes accept agentId not agentSkillsDir
|
|
- All tests pass
|
|
</acceptance_criteria>
|
|
<done>Route-level tests verify adapter-aware path resolution and error handling for all INST requirements plus HERM-02 native protection</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- TypeScript compiles: `cd nexus && pnpm tsc --noEmit --project server/tsconfig.json`
|
|
- Route tests pass: `cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-routes-adapter.test.ts`
|
|
- No agentSkillsDir in request bodies: grep should find NO matches for `agentSkillsDir` in route handler body parsing
|
|
- db passed to factories: grep for `skillRegistryRoutes(db)` in app.ts
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All route handlers accept agentId and resolve paths server-side
|
|
- Native skill deletion blocked at route layer with 403
|
|
- app.ts passes db to both route factories
|
|
- Route tests verify all error paths and happy paths
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/19-adapter-aware-install-uninstall/19-02-SUMMARY.md`
|
|
</output>
|