Skip to content

Commit 02047dc

Browse files
waydelyleclaude
andcommitted
fix: issues found by OpenClaw agent dogfooding SwarmDock
Fixes 3 issues discovered when an OpenClaw agent autonomously tested the platform using the swarmdock skill file: 1. walletAddress not updatable via PATCH — was missing from AgentUpdateSchema, Zod silently stripped it. Added to schema. 2. No way to add skills after registration — added PUT /agents/:id/skills endpoint that replaces all skills in a transaction. Also added profile.updateSkills() to the SDK and extracted reusable AgentSkillSchema/AgentSkillsUpdateSchema from shared schemas. 3. Skill file lacked post-registration docs — added "Update Profile & Skills After Registration" section, documented PATCH and PUT endpoints, added analytics endpoint to API table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d4ee7e0 commit 02047dc

5 files changed

Lines changed: 132 additions & 22 deletions

File tree

packages/api/src/routes/agents.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
AgentVerifySchema,
1111
AgentLoginChallengeSchema,
1212
AgentUpdateSchema,
13+
AgentSkillsUpdateSchema,
1314
AgentListQuerySchema,
1415
PortfolioItemUpdateSchema,
1516
AgentKeyRotateSchema,
@@ -636,6 +637,51 @@ app.delete('/:id/portfolio/:itemId', authMiddleware, requireScope('portfolio.wri
636637
}
637638
});
638639

640+
// PUT /api/v1/agents/:id/skills — Replace agent skills
641+
app.put('/:id/skills', authMiddleware, requireScope('profile.write'), async (c) => {
642+
const id = c.req.param('id');
643+
const agent = c.get('agent');
644+
645+
if (agent.agent_id !== id) {
646+
return c.json({ error: 'Can only update your own skills' }, 403);
647+
}
648+
649+
const body = await c.req.json();
650+
const parsed = AgentSkillsUpdateSchema.safeParse(body);
651+
if (!parsed.success) {
652+
return c.json({ error: 'Validation failed', details: parsed.error.flatten() }, 400);
653+
}
654+
655+
const skills = parsed.data;
656+
657+
const inserted = await db.transaction(async (tx) => {
658+
// Delete existing skills
659+
await tx.delete(agentSkills).where(eq(agentSkills.agentId, id));
660+
661+
// Insert new skills
662+
const rows = await tx.insert(agentSkills).values(
663+
skills.map((s) => ({
664+
agentId: id,
665+
skillId: s.skillId,
666+
skillName: s.skillName,
667+
description: s.description,
668+
category: s.category,
669+
tags: s.tags,
670+
inputModes: s.inputModes,
671+
outputModes: s.outputModes,
672+
pricingModel: s.pricingModel,
673+
basePrice: BigInt(s.basePrice),
674+
examplePrompts: s.examplePrompts,
675+
})),
676+
).returning();
677+
678+
return rows;
679+
});
680+
681+
eventBus.broadcast({ type: 'agent.updated', data: { agentId: id } });
682+
return c.json({ skills: inserted, count: inserted.length });
683+
});
684+
639685
// Agent card served from index.ts at /agents/:id/.well-known/agent.json
640686

641687
// POST /api/v1/agents/match — Find best-matching agents for a task

packages/api/src/routes/docs.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ const spec = {
223223
responses: { 201: { description: 'Item created' } },
224224
},
225225
},
226+
'/api/v1/agents/{id}/skills': {
227+
put: {
228+
tags: ['Agents'], summary: 'Replace agent skills (upsert)', security: [{ bearerAuth: [] }],
229+
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } }],
230+
requestBody: { required: true, content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/Skill' } } } } },
231+
responses: { 200: { description: 'Updated skills list' }, 403: { description: 'Not your profile' } },
232+
},
233+
},
226234
'/api/v1/agents/{id}/rotate-key': {
227235
post: {
228236
tags: ['Agents'], summary: 'Rotate Ed25519 key', security: [{ bearerAuth: [] }],

packages/sdk/src/client.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,19 @@ class ProfileOperations {
426426
});
427427
}
428428

429+
async updateSkills(skills: Array<{
430+
skillId: string; skillName: string; description: string; category: string;
431+
tags?: string[]; inputModes?: string[]; outputModes?: string[];
432+
pricingModel?: string; basePrice: string; examplePrompts: string[];
433+
}>): Promise<{ skills: AgentSkill[]; count: number }> {
434+
await this.client.authenticate();
435+
const id = this.client.getAgentId();
436+
return this.client.fetch(`/api/v1/agents/${id}/skills`, {
437+
method: 'PUT',
438+
body: skills,
439+
});
440+
}
441+
429442
async ratings(agentId?: string): Promise<RatingsSummary> {
430443
if (!agentId) {
431444
await this.client.authenticate();

packages/shared/src/schemas.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,29 @@ import {
1313
const MICRO_USDC_AMOUNT_MESSAGE = 'Must be a non-negative integer amount in micro-USDC';
1414
export const MicroUsdcAmountSchema = z.string().regex(/^\d+$/, MICRO_USDC_AMOUNT_MESSAGE);
1515

16+
export const AgentSkillSchema = z.object({
17+
skillId: z.string().min(1),
18+
skillName: z.string().min(1),
19+
description: z.string().min(1),
20+
category: z.string().min(1),
21+
tags: z.array(z.string()).default([]),
22+
inputModes: z.array(z.string()).default(['text']),
23+
outputModes: z.array(z.string()).default(['text']),
24+
pricingModel: z.enum([
25+
PRICING_MODEL.PER_TASK,
26+
PRICING_MODEL.PER_HOUR,
27+
PRICING_MODEL.PER_TOKEN,
28+
PRICING_MODEL.PER_REQUEST,
29+
PRICING_MODEL.CUSTOM,
30+
]).default(PRICING_MODEL.PER_TASK),
31+
basePrice: MicroUsdcAmountSchema,
32+
examplePrompts: z.array(z.string().min(1)).min(5, 'At least 5 example prompts required per skill'),
33+
benchmarkScores: z.unknown().optional(),
34+
sampleOutputs: z.unknown().optional(),
35+
});
36+
37+
export const AgentSkillsUpdateSchema = z.array(AgentSkillSchema).min(1, 'At least one skill required');
38+
1639
// Agent registration
1740
export const AgentRegisterSchema = z.object({
1841
publicKey: z.string().min(1, 'Public key is required'),
@@ -26,26 +49,7 @@ export const AgentRegisterSchema = z.object({
2649
modelName: z.string().optional(),
2750
walletAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid Ethereum address').optional(),
2851
agentCardUrl: z.string().url().optional(),
29-
skills: z.array(z.object({
30-
skillId: z.string().min(1),
31-
skillName: z.string().min(1),
32-
description: z.string().min(1),
33-
category: z.string().min(1),
34-
tags: z.array(z.string()).default([]),
35-
inputModes: z.array(z.string()).default(['text']),
36-
outputModes: z.array(z.string()).default(['text']),
37-
pricingModel: z.enum([
38-
PRICING_MODEL.PER_TASK,
39-
PRICING_MODEL.PER_HOUR,
40-
PRICING_MODEL.PER_TOKEN,
41-
PRICING_MODEL.PER_REQUEST,
42-
PRICING_MODEL.CUSTOM,
43-
]).default(PRICING_MODEL.PER_TASK),
44-
basePrice: MicroUsdcAmountSchema, // USDC amount as string (6 decimals)
45-
examplePrompts: z.array(z.string().min(1)).min(5, 'At least 5 example prompts required per skill'),
46-
benchmarkScores: z.unknown().optional(),
47-
sampleOutputs: z.unknown().optional(),
48-
})).default([]),
52+
skills: z.array(AgentSkillSchema).default([]),
4953
});
5054

5155
export const AgentVerifySchema = z.object({
@@ -61,6 +65,7 @@ export const AgentLoginChallengeSchema = z.object({
6165
export const AgentUpdateSchema = z.object({
6266
displayName: z.string().min(1).max(200).optional(),
6367
description: z.string().max(2000).optional(),
68+
walletAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, 'Invalid EVM wallet address').optional(),
6469
avatarUrl: z.string().url().nullable().optional(),
6570
ownerDid: z.string().nullable().optional(),
6671
framework: z.string().optional(),

skills/swarmdock/SKILL.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,49 @@ await client.tasks.dispute(taskId, 'Work does not match requirements');
241241
- **Audit Log**: Hash-chained immutable log of all marketplace events
242242
- **A2A Protocol**: Agent Cards at `/.well-known/agent.json`
243243

244+
## Update Profile & Skills After Registration
245+
246+
You can update your wallet address and other profile fields anytime:
247+
248+
```typescript
249+
await client.profile.update({
250+
walletAddress: '0x1234...your_real_wallet...',
251+
description: 'Updated description',
252+
});
253+
```
254+
255+
Add or replace skills after registration:
256+
257+
```typescript
258+
await client.profile.updateSkills([{
259+
skillId: 'data-analysis',
260+
skillName: 'Data Analysis',
261+
description: 'Statistical analysis, regression, hypothesis testing',
262+
category: 'data-science',
263+
tags: ['statistics', 'ml'],
264+
inputModes: ['text', 'application/json'],
265+
outputModes: ['text', 'application/json'],
266+
basePrice: '5000000', // $5.00 USDC
267+
examplePrompts: [
268+
'analyze this dataset for outliers',
269+
'run linear regression on sales data',
270+
'test whether A/B variants are statistically significant',
271+
'build a time-series forecast for revenue',
272+
'calculate descriptive statistics and generate a summary report',
273+
],
274+
}]);
275+
```
276+
277+
Or via direct API call: `PUT /api/v1/agents/:id/skills` with a JSON array of skills.
278+
244279
## API Endpoints
245280

246281
| Method | Endpoint | Description |
247282
|--------|----------|-------------|
248-
| POST | `/api/v1/agents/register` | Register agent |
249-
| POST | `/api/v1/agents/verify` | Complete challenge-response |
283+
| POST | `/api/v1/agents/register` | Register agent (step 1: get challenge) |
284+
| POST | `/api/v1/agents/verify` | Complete challenge-response (step 2: get token) |
285+
| PATCH | `/api/v1/agents/:id` | Update profile (walletAddress, description, etc.) |
286+
| PUT | `/api/v1/agents/:id/skills` | Replace agent skills |
250287
| GET | `/api/v1/agents` | List agents |
251288
| POST | `/api/v1/agents/match` | Semantic skill matching |
252289
| GET | `/api/v1/agents/:id/portfolio` | Get agent portfolio |
@@ -260,6 +297,7 @@ await client.tasks.dispute(taskId, 'Work does not match requirements');
260297
| POST | `/api/v1/tasks/:id/dispute` | Open dispute |
261298
| GET | `/api/v1/events` | SSE event stream |
262299
| POST | `/api/v1/ratings` | Submit rating (0-1 scale) |
300+
| GET | `/api/v1/analytics/:agentId` | Agent performance metrics |
263301

264302
## Environment Variables
265303

0 commit comments

Comments
 (0)