@swarmclawai/swarmclaw 1.2.9 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/package.json +2 -2
- package/src/app/api/activity/route.ts +9 -23
- package/src/app/api/agents/route.ts +17 -1
- package/src/app/api/approvals/route.ts +13 -5
- package/src/app/api/credentials/[id]/route.ts +2 -0
- package/src/app/api/credentials/route.ts +4 -1
- package/src/app/api/goals/[id]/route.ts +28 -0
- package/src/app/api/goals/route.ts +33 -0
- package/src/app/api/protocols/templates/[id]/route.ts +2 -1
- package/src/app/api/protocols/templates/route.ts +2 -1
- package/src/app/api/settings/route.ts +13 -0
- package/src/app/home/page.tsx +3 -0
- package/src/cli/index.js +11 -0
- package/src/cli/spec.js +10 -0
- package/src/lib/server/activity/activity-log.ts +16 -1
- package/src/lib/server/agents/agent-service.ts +24 -11
- package/src/lib/server/approval-match.ts +14 -0
- package/src/lib/server/approvals/approval-hooks.ts +81 -0
- package/src/lib/server/approvals.ts +11 -0
- package/src/lib/server/connectors/swarmdock-bidding.ts +2 -9
- package/src/lib/server/connectors/swarmdock-payloads.test.ts +18 -1
- package/src/lib/server/connectors/swarmdock-tasks.ts +10 -11
- package/src/lib/server/connectors/swarmdock.ts +111 -43
- package/src/lib/server/execution-brief.ts +18 -0
- package/src/lib/server/goals/goal-repository.ts +19 -0
- package/src/lib/server/goals/goal-service.ts +143 -0
- package/src/lib/server/storage-normalization.ts +5 -0
- package/src/lib/server/storage.ts +57 -0
- package/src/lib/server/usage/cost-rollup.ts +124 -0
- package/src/lib/server/usage/usage-repository.ts +6 -0
- package/src/lib/validation/schemas.ts +3 -30
- package/src/lib/validation/server-schemas.ts +35 -0
- package/src/types/agent.ts +10 -0
- package/src/types/app-settings.ts +6 -0
- package/src/types/approval.ts +3 -0
- package/src/types/goal.ts +30 -0
- package/src/types/index.ts +1 -0
- package/src/types/misc.ts +2 -2
- package/src/types/task.ts +2 -0
package/README.md
CHANGED
|
@@ -204,6 +204,23 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
|
|
|
204
204
|
|
|
205
205
|
## Release Notes
|
|
206
206
|
|
|
207
|
+
### v1.3.1 Highlights
|
|
208
|
+
|
|
209
|
+
- **SwarmDock SDK v0.2.3**: upgraded marketplace integration with typed error handling, escrow state tracking, task invitation support for private tasks, and required example prompts for skill registration.
|
|
210
|
+
- **SDK error resilience**: registration now gracefully handles already-registered agents by falling back to authentication; heartbeat catches expired tokens and re-authenticates automatically.
|
|
211
|
+
- **Escrow event tracking**: new `escrow.releasing`, `escrow.refunding`, `escrow.release_failed`, and `escrow.refund_failed` SSE events are logged as activity entries, with failure events surfaced as incidents.
|
|
212
|
+
- **Private task invitations**: when a SwarmDock task invites this agent directly, auto-discovery now evaluates it alongside public `task.created` events.
|
|
213
|
+
- **SDK type imports**: replaced inlined SwarmDock type stubs with proper imports from `@swarmdock/shared`, eliminating type drift.
|
|
214
|
+
|
|
215
|
+
### v1.3.0 Highlights
|
|
216
|
+
|
|
217
|
+
- **SwarmDock SDK v0.2.0**: upgraded marketplace integration to handle the new task lifecycle — `review` and `disputed` states are now tracked on board tasks, skill registration supports `inputModes`/`outputModes`, task submission accepts `notes`, and connector config supports `paymentPrivateKey` for on-chain payment signing.
|
|
218
|
+
- **Comprehensive audit logging**: activity log now covers approval decisions, settings changes, budget modifications, and credential operations, with SQL-indexed paginated queries replacing the in-memory full-collection scan.
|
|
219
|
+
- **Push-based cost rollups**: agent spend fields (`spentHourlyCents`, `spentDailyCents`, `spentMonthlyCents`) update atomically on every usage event, with automatic budget warning/exceeded activity entries and window reset detection — replacing the pull-based full-scan approach.
|
|
220
|
+
- **Goal hierarchy**: new goals system with organization → team → project → agent → task levels, parent-child chains, and automatic injection of the "why chain" into agent execution briefs. Full CRUD API and CLI support.
|
|
221
|
+
- **Extended approval workflows**: new `agent_create`, `budget_change`, and `delegation_enable` approval categories with configurable policies in settings. When enabled, agent creation returns a pending approval instead of creating the agent directly.
|
|
222
|
+
- **Shared validation schemas**: Zod schemas in `src/lib/validation/schemas.ts` are now safe for client-side import (server-only DAG validation moved to `server-schemas.ts`), enabling form-level pre-validation.
|
|
223
|
+
|
|
207
224
|
### v1.2.9 Highlights
|
|
208
225
|
|
|
209
226
|
- **SwarmDock marketplace connector**: SwarmClaw agents can now register on SwarmDock, auto-bid on matching work, receive assignments as board tasks, and submit results back through the connector runtime.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Self-hosted AI runtime for OpenClaw, delegation, autonomy, runtime skills, crypto wallets, and chat platform connectors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@multiavatar/multiavatar": "^1.0.7",
|
|
88
88
|
"@playwright/mcp": "^0.0.68",
|
|
89
89
|
"@slack/bolt": "^4.6.0",
|
|
90
|
-
"@swarmdock/sdk": "^0.
|
|
90
|
+
"@swarmdock/sdk": "^0.2.3",
|
|
91
91
|
"@tailwindcss/postcss": "^4",
|
|
92
92
|
"@tanstack/react-query": "^5.91.0",
|
|
93
93
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -1,32 +1,18 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { queryActivity } from '@/lib/server/activity/activity-log'
|
|
3
3
|
export const dynamic = 'force-dynamic'
|
|
4
4
|
|
|
5
5
|
export async function GET(req: Request) {
|
|
6
6
|
const { searchParams } = new URL(req.url)
|
|
7
|
-
const entityType = searchParams.get('entityType')
|
|
8
|
-
const entityId = searchParams.get('entityId')
|
|
9
|
-
const actor = searchParams.get('actor')
|
|
10
|
-
const action = searchParams.get('action')
|
|
11
|
-
const
|
|
7
|
+
const entityType = searchParams.get('entityType') ?? undefined
|
|
8
|
+
const entityId = searchParams.get('entityId') ?? undefined
|
|
9
|
+
const actor = searchParams.get('actor') ?? undefined
|
|
10
|
+
const action = searchParams.get('action') ?? undefined
|
|
11
|
+
const sinceRaw = searchParams.get('since')
|
|
12
|
+
const since = sinceRaw ? Number(sinceRaw) : undefined
|
|
12
13
|
const limit = Math.min(200, Math.max(1, Number(searchParams.get('limit')) || 50))
|
|
14
|
+
const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
|
|
13
15
|
|
|
14
|
-
const
|
|
15
|
-
let entries = Object.values(all) as unknown as Array<Record<string, unknown>>
|
|
16
|
-
|
|
17
|
-
if (entityType) entries = entries.filter((e) => e.entityType === entityType)
|
|
18
|
-
if (entityId) entries = entries.filter((e) => e.entityId === entityId)
|
|
19
|
-
if (actor) entries = entries.filter((e) => e.actor === actor)
|
|
20
|
-
if (action) entries = entries.filter((e) => e.action === action)
|
|
21
|
-
if (since) {
|
|
22
|
-
const sinceMs = Number(since)
|
|
23
|
-
if (Number.isFinite(sinceMs)) {
|
|
24
|
-
entries = entries.filter((e) => typeof e.timestamp === 'number' && e.timestamp >= sinceMs)
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
entries.sort((a, b) => (b.timestamp as number) - (a.timestamp as number))
|
|
29
|
-
entries = entries.slice(0, limit)
|
|
30
|
-
|
|
16
|
+
const entries = queryActivity({ entityType, entityId, actor, action, since, limit, offset })
|
|
31
17
|
return NextResponse.json(entries)
|
|
32
18
|
}
|
|
@@ -5,6 +5,8 @@ import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
|
5
5
|
import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
8
|
+
import { loadSettings } from '@/lib/server/storage'
|
|
9
|
+
import { requestApproval } from '@/lib/server/approvals'
|
|
8
10
|
export const dynamic = 'force-dynamic'
|
|
9
11
|
|
|
10
12
|
|
|
@@ -36,6 +38,20 @@ export async function POST(req: Request) {
|
|
|
36
38
|
if (!parsed.success) {
|
|
37
39
|
return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
|
|
38
40
|
}
|
|
39
|
-
const
|
|
41
|
+
const body = parsed.data as unknown as Record<string, unknown>
|
|
42
|
+
|
|
43
|
+
// Check approval policy — if enabled, create an approval request instead of the agent
|
|
44
|
+
const settings = loadSettings()
|
|
45
|
+
if (settings.approvalPolicies?.requireApprovalForAgentCreate) {
|
|
46
|
+
const approval = requestApproval({
|
|
47
|
+
category: 'agent_create',
|
|
48
|
+
title: `Create agent: ${body.name}`,
|
|
49
|
+
description: `Request to create agent "${body.name}" with provider ${body.provider}`,
|
|
50
|
+
data: { pendingAgentConfig: body, agentName: String(body.name || ''), provider: String(body.provider || '') },
|
|
51
|
+
})
|
|
52
|
+
return NextResponse.json({ pendingApproval: true, approvalId: approval.id }, { status: 202 })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const agent = createAgent({ body, rawRecord })
|
|
40
56
|
return NextResponse.json(agent)
|
|
41
57
|
}
|
|
@@ -2,11 +2,22 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { listPendingApprovals, submitDecision } from '@/lib/server/approvals'
|
|
3
3
|
import { loadApprovals } from '@/lib/server/storage'
|
|
4
4
|
import { errorMessage } from '@/lib/shared-utils'
|
|
5
|
+
import type { ApprovalCategory } from '@/types'
|
|
5
6
|
|
|
6
7
|
export const dynamic = 'force-dynamic'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
const ALLOWED_CATEGORIES: ApprovalCategory[] = [
|
|
10
|
+
'human_loop', 'tool_access', 'extension_scaffold', 'extension_install',
|
|
11
|
+
'task_tool', 'connector_sender', 'agent_create', 'budget_change', 'delegation_enable',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
export async function GET(req: Request) {
|
|
15
|
+
const { searchParams } = new URL(req.url)
|
|
16
|
+
const categoryParam = searchParams.get('category') as ApprovalCategory | null
|
|
17
|
+
const category = categoryParam && ALLOWED_CATEGORIES.includes(categoryParam)
|
|
18
|
+
? categoryParam
|
|
19
|
+
: undefined
|
|
20
|
+
return NextResponse.json(listPendingApprovals(category))
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
export async function POST(req: Request) {
|
|
@@ -20,9 +31,6 @@ export async function POST(req: Request) {
|
|
|
20
31
|
if (!approval) {
|
|
21
32
|
return NextResponse.json({ error: 'approval not found' }, { status: 404 })
|
|
22
33
|
}
|
|
23
|
-
if (approval.category !== 'human_loop') {
|
|
24
|
-
return NextResponse.json({ error: 'only human-loop approvals are supported here' }, { status: 400 })
|
|
25
|
-
}
|
|
26
34
|
await submitDecision(id, approved)
|
|
27
35
|
return NextResponse.json({ ok: true })
|
|
28
36
|
} catch (err: unknown) {
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { deleteCredentialRecord } from '@/lib/server/credentials/credential-service'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { log } from '@/lib/server/logger'
|
|
5
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
5
6
|
|
|
6
7
|
const TAG = 'api-credentials'
|
|
7
8
|
|
|
@@ -11,5 +12,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
|
|
|
11
12
|
return notFound()
|
|
12
13
|
}
|
|
13
14
|
log.info(TAG, `deleted ${credId}`)
|
|
15
|
+
logActivity({ entityType: 'credential', entityId: credId, action: 'deleted', actor: 'user', summary: `Credential deleted: ${credId}` })
|
|
14
16
|
return new NextResponse('OK')
|
|
15
17
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { createCredentialRecord, listCredentialSummaries } from '@/lib/server/credentials/credential-service'
|
|
3
3
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
4
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
4
5
|
export const dynamic = 'force-dynamic'
|
|
5
6
|
|
|
6
7
|
|
|
@@ -16,7 +17,9 @@ export async function POST(req: Request) {
|
|
|
16
17
|
return NextResponse.json({ error: 'provider and apiKey are required' }, { status: 400 })
|
|
17
18
|
}
|
|
18
19
|
try {
|
|
19
|
-
|
|
20
|
+
const result = createCredentialRecord({ provider, name, apiKey })
|
|
21
|
+
logActivity({ entityType: 'credential', entityId: result.id, action: 'created', actor: 'user', summary: `Credential created: "${name}" (${provider})` })
|
|
22
|
+
return NextResponse.json(result)
|
|
20
23
|
} catch (err: unknown) {
|
|
21
24
|
return NextResponse.json(
|
|
22
25
|
{ error: err instanceof Error ? err.message : 'Failed to create credential' },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
3
|
+
import { getGoalById, updateGoal, deleteGoal, getGoalChain } from '@/lib/server/goals/goal-service'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
export const dynamic = 'force-dynamic'
|
|
6
|
+
|
|
7
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
8
|
+
const { id } = await params
|
|
9
|
+
const goal = getGoalById(id)
|
|
10
|
+
if (!goal) return notFound()
|
|
11
|
+
const chain = getGoalChain(id)
|
|
12
|
+
return NextResponse.json({ ...goal, chain })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
16
|
+
const { id } = await params
|
|
17
|
+
const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
18
|
+
if (error) return error
|
|
19
|
+
const updated = updateGoal(id, body)
|
|
20
|
+
if (!updated) return notFound()
|
|
21
|
+
return NextResponse.json(updated)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
25
|
+
const { id } = await params
|
|
26
|
+
if (!deleteGoal(id)) return notFound()
|
|
27
|
+
return new NextResponse('OK')
|
|
28
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
4
|
+
import { getAllGoals, createGoal } from '@/lib/server/goals/goal-service'
|
|
5
|
+
import { formatZodError } from '@/lib/validation/schemas'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
const GoalCreateSchema = z.object({
|
|
9
|
+
title: z.string().min(1, 'Goal title is required'),
|
|
10
|
+
description: z.string().optional().default(''),
|
|
11
|
+
level: z.enum(['organization', 'team', 'project', 'agent', 'task']),
|
|
12
|
+
parentGoalId: z.string().nullable().optional().default(null),
|
|
13
|
+
projectId: z.string().nullable().optional().default(null),
|
|
14
|
+
agentId: z.string().nullable().optional().default(null),
|
|
15
|
+
taskId: z.string().nullable().optional().default(null),
|
|
16
|
+
objective: z.string().min(1, 'Objective is required'),
|
|
17
|
+
constraints: z.array(z.string()).optional().default([]),
|
|
18
|
+
successMetric: z.string().nullable().optional().default(null),
|
|
19
|
+
budgetUsd: z.number().positive().nullable().optional().default(null),
|
|
20
|
+
deadlineAt: z.number().nullable().optional().default(null),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export async function GET() {
|
|
24
|
+
return NextResponse.json(getAllGoals())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function POST(req: Request) {
|
|
28
|
+
const { data: body, error } = await safeParseBody<Record<string, unknown>>(req)
|
|
29
|
+
if (error) return error
|
|
30
|
+
const parsed = GoalCreateSchema.safeParse(body)
|
|
31
|
+
if (!parsed.success) return NextResponse.json(formatZodError(parsed.error), { status: 400 })
|
|
32
|
+
return NextResponse.json(createGoal(parsed.data))
|
|
33
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
|
-
import { formatZodError
|
|
4
|
+
import { formatZodError } from '@/lib/validation/schemas'
|
|
5
|
+
import { ProtocolTemplateUpsertSchema } from '@/lib/validation/server-schemas'
|
|
5
6
|
import {
|
|
6
7
|
deleteProtocolTemplateById,
|
|
7
8
|
loadProtocolTemplateById,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
|
-
import { formatZodError
|
|
3
|
+
import { formatZodError } from '@/lib/validation/schemas'
|
|
4
|
+
import { ProtocolTemplateUpsertSchema } from '@/lib/validation/server-schemas'
|
|
4
5
|
import {
|
|
5
6
|
createProtocolTemplate,
|
|
6
7
|
listProtocolTemplates,
|
|
@@ -6,6 +6,7 @@ import { loadPublicSettings, loadSettings, saveSettings } from '@/lib/server/sto
|
|
|
6
6
|
import { normalizeRuntimeSettingFields } from '@/lib/runtime/runtime-loop'
|
|
7
7
|
import { normalizeSupervisorSettings } from '@/lib/autonomy/supervisor-settings'
|
|
8
8
|
import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
|
|
9
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
9
10
|
export const dynamic = 'force-dynamic'
|
|
10
11
|
|
|
11
12
|
|
|
@@ -164,6 +165,18 @@ export async function PUT(req: Request) {
|
|
|
164
165
|
|
|
165
166
|
saveSettings(settings)
|
|
166
167
|
|
|
168
|
+
const changedKeys = Object.keys(sanitizedBody).filter((k) => !SECRET_SETTING_KEYS.includes(k as typeof SECRET_SETTING_KEYS[number]))
|
|
169
|
+
if (changedKeys.length > 0) {
|
|
170
|
+
logActivity({
|
|
171
|
+
entityType: 'settings',
|
|
172
|
+
entityId: 'app',
|
|
173
|
+
action: 'configured',
|
|
174
|
+
actor: 'user',
|
|
175
|
+
summary: `Settings updated: ${changedKeys.join(', ')}`,
|
|
176
|
+
detail: { changedKeys },
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
if ('daemonAutostartEnabled' in sanitizedBody && settings.daemonAutostartEnabled) {
|
|
168
181
|
void ensureDaemonProcessRunning('api/settings:put:daemon-autostart', { manualStart: true }).catch(() => {
|
|
169
182
|
// The daemon launcher may not be available during early bootstrap.
|
package/src/app/home/page.tsx
CHANGED
|
@@ -42,6 +42,9 @@ const ACTIVITY_ICONS: Record<ActivityEntry['action'], string> = {
|
|
|
42
42
|
incident: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4m0 4h.01',
|
|
43
43
|
running: 'M12 2v4m0 12v4m10-10h-4M6 12H2',
|
|
44
44
|
claimed: 'M9 12l2 2 4-4m6 2a10 10 0 1 1-20 0 10 10 0 0 1 20 0z',
|
|
45
|
+
configured: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z',
|
|
46
|
+
budget_exceeded: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20m0 6v4m0 4h.01',
|
|
47
|
+
budget_warning: 'M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4m0 4h.01',
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const ACTIVITY_COLORS: Record<string, string> = {
|
package/src/cli/index.js
CHANGED
|
@@ -757,6 +757,17 @@ const COMMAND_GROUPS = [
|
|
|
757
757
|
cmd('delete', 'DELETE', '/wallets/:id', 'Delete a wallet'),
|
|
758
758
|
],
|
|
759
759
|
},
|
|
760
|
+
{
|
|
761
|
+
name: 'goals',
|
|
762
|
+
description: 'Manage goal hierarchy',
|
|
763
|
+
commands: [
|
|
764
|
+
cmd('list', 'GET', '/goals', 'List goals'),
|
|
765
|
+
cmd('get', 'GET', '/goals/:id', 'Get goal by id'),
|
|
766
|
+
cmd('create', 'POST', '/goals', 'Create a goal', { expectsJsonBody: true }),
|
|
767
|
+
cmd('update', 'PATCH', '/goals/:id', 'Update a goal', { expectsJsonBody: true }),
|
|
768
|
+
cmd('delete', 'DELETE', '/goals/:id', 'Delete a goal'),
|
|
769
|
+
],
|
|
770
|
+
},
|
|
760
771
|
]
|
|
761
772
|
|
|
762
773
|
const GROUP_MAP = new Map(COMMAND_GROUPS.map((group) => [group.name, group]))
|
package/src/cli/spec.js
CHANGED
|
@@ -535,6 +535,16 @@ const COMMAND_GROUPS = {
|
|
|
535
535
|
delete: { description: 'Delete a wallet', method: 'DELETE', path: '/wallets/:id', params: ['id'] },
|
|
536
536
|
},
|
|
537
537
|
},
|
|
538
|
+
goals: {
|
|
539
|
+
description: 'Manage goal hierarchy',
|
|
540
|
+
commands: {
|
|
541
|
+
list: { description: 'List goals', method: 'GET', path: '/goals' },
|
|
542
|
+
get: { description: 'Get goal by id', method: 'GET', path: '/goals/:id', params: ['id'] },
|
|
543
|
+
create: { description: 'Create a goal', method: 'POST', path: '/goals' },
|
|
544
|
+
update: { description: 'Update a goal', method: 'PATCH', path: '/goals/:id', params: ['id'], body: true },
|
|
545
|
+
delete: { description: 'Delete a goal', method: 'DELETE', path: '/goals/:id', params: ['id'] },
|
|
546
|
+
},
|
|
547
|
+
},
|
|
538
548
|
}
|
|
539
549
|
|
|
540
550
|
const GROUP_NAMES = Object.keys(COMMAND_GROUPS)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadActivity as loadStoredActivity, logActivity as writeActivityLog } from '@/lib/server/storage'
|
|
1
|
+
import { loadActivity as loadStoredActivity, logActivity as writeActivityLog, queryActivity as queryStoredActivity } from '@/lib/server/storage'
|
|
2
2
|
import { perf } from '@/lib/server/runtime/perf'
|
|
3
3
|
|
|
4
4
|
export function loadActivity() {
|
|
@@ -19,3 +19,18 @@ export function logActivity(entry: {
|
|
|
19
19
|
action: entry.action,
|
|
20
20
|
})
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
/** Paginated activity query — uses SQL WHERE + LIMIT/OFFSET for efficiency. */
|
|
24
|
+
export function queryActivity(filters: {
|
|
25
|
+
entityType?: string
|
|
26
|
+
entityId?: string
|
|
27
|
+
actor?: string
|
|
28
|
+
action?: string
|
|
29
|
+
since?: number
|
|
30
|
+
limit?: number
|
|
31
|
+
offset?: number
|
|
32
|
+
}): unknown[] {
|
|
33
|
+
return perf.measureSync('repository', 'activity.query', () => queryStoredActivity(filters), {
|
|
34
|
+
entityType: filters.entityType ?? 'all',
|
|
35
|
+
})
|
|
36
|
+
}
|
|
@@ -66,19 +66,22 @@ function detachAgentSessions(agentId: string): number {
|
|
|
66
66
|
|
|
67
67
|
export function listAgentsForApi(): Record<string, Agent> {
|
|
68
68
|
const agents = loadAgents()
|
|
69
|
-
const sessions = listSessions()
|
|
70
|
-
const usage = loadUsage()
|
|
71
|
-
const now = Date.now()
|
|
72
69
|
for (const agent of Object.values(agents)) {
|
|
73
|
-
|
|
74
|
-
(typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0)
|
|
70
|
+
const hasBudget = (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0)
|
|
75
71
|
|| (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0)
|
|
76
72
|
|| (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0)
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
if (!hasBudget) continue
|
|
74
|
+
|
|
75
|
+
// Use persisted spend fields when available (push-based rollup)
|
|
76
|
+
if (typeof agent.lastSpendRollupAt === 'number' && agent.lastSpendRollupAt > 0) {
|
|
77
|
+
if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) agent.monthlySpend = (agent.spentMonthlyCents ?? 0) / 100
|
|
78
|
+
if (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0) agent.dailySpend = (agent.spentDailyCents ?? 0) / 100
|
|
79
|
+
if (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0) agent.hourlySpend = (agent.spentHourlyCents ?? 0) / 100
|
|
80
|
+
} else {
|
|
81
|
+
// Fallback: full scan for agents that predate the rollup system
|
|
82
|
+
const sessions = listSessions()
|
|
83
|
+
const usage = loadUsage()
|
|
84
|
+
const spend = getAgentSpendWindows(agent.id, Date.now(), { sessions, usage })
|
|
82
85
|
if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) agent.monthlySpend = spend.monthly
|
|
83
86
|
if (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0) agent.dailySpend = spend.daily
|
|
84
87
|
if (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0) agent.hourlySpend = spend.hourly
|
|
@@ -301,7 +304,17 @@ export function updateAgent(agentId: string, body: Record<string, unknown>): Age
|
|
|
301
304
|
updateThreadShortcutSession(agentId, updated)
|
|
302
305
|
}
|
|
303
306
|
|
|
304
|
-
|
|
307
|
+
const budgetFields = ['monthlyBudget', 'dailyBudget', 'hourlyBudget', 'budgetAction'] as const
|
|
308
|
+
const budgetChanges: Record<string, unknown> = {}
|
|
309
|
+
for (const key of budgetFields) {
|
|
310
|
+
if (key in body) budgetChanges[key] = body[key]
|
|
311
|
+
}
|
|
312
|
+
const detail: Record<string, unknown> = {}
|
|
313
|
+
if (Object.keys(budgetChanges).length > 0) detail.budgetChanges = budgetChanges
|
|
314
|
+
logActivity({ entityType: 'agent', entityId: agentId, action: 'updated', actor: 'user', summary: `Agent updated: "${updated.name}"`, detail: Object.keys(detail).length > 0 ? detail : undefined })
|
|
315
|
+
if (Object.keys(budgetChanges).length > 0) {
|
|
316
|
+
logActivity({ entityType: 'budget', entityId: agentId, action: 'configured', actor: 'user', summary: `Budget updated for agent "${updated.name}"`, detail: budgetChanges })
|
|
317
|
+
}
|
|
305
318
|
return updated
|
|
306
319
|
}
|
|
307
320
|
|
|
@@ -71,6 +71,20 @@ export function buildApprovalComparablePayload(
|
|
|
71
71
|
toolName: trimString(data.toolName),
|
|
72
72
|
args: canonicalizeValue(data.args),
|
|
73
73
|
}
|
|
74
|
+
case 'agent_create':
|
|
75
|
+
return {
|
|
76
|
+
agentName: trimString(data.agentName),
|
|
77
|
+
provider: trimString(data.provider),
|
|
78
|
+
}
|
|
79
|
+
case 'budget_change':
|
|
80
|
+
return {
|
|
81
|
+
agentId: trimString(data.agentId),
|
|
82
|
+
budgetChanges: canonicalizeValue(data.budgetChanges),
|
|
83
|
+
}
|
|
84
|
+
case 'delegation_enable':
|
|
85
|
+
return {
|
|
86
|
+
agentId: trimString(data.agentId),
|
|
87
|
+
}
|
|
74
88
|
default:
|
|
75
89
|
return canonicalizeValue(data) as Record<string, unknown>
|
|
76
90
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ApprovalRequest } from '@/types'
|
|
2
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
3
|
+
import { log } from '@/lib/server/logger'
|
|
4
|
+
|
|
5
|
+
const TAG = 'approval-hooks'
|
|
6
|
+
|
|
7
|
+
type ApprovalHookHandler = (request: ApprovalRequest) => void
|
|
8
|
+
|
|
9
|
+
const approvalHandlers: Partial<Record<string, ApprovalHookHandler>> = {
|
|
10
|
+
agent_create: onAgentCreateDecided,
|
|
11
|
+
budget_change: onBudgetChangeDecided,
|
|
12
|
+
delegation_enable: onDelegationEnableDecided,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Dispatch lifecycle hooks when an approval decision is made.
|
|
17
|
+
* Called from submitDecision() after the approval is persisted.
|
|
18
|
+
*/
|
|
19
|
+
export function onApprovalDecision(request: ApprovalRequest): void {
|
|
20
|
+
const handler = approvalHandlers[request.category]
|
|
21
|
+
if (!handler) return
|
|
22
|
+
try {
|
|
23
|
+
handler(request)
|
|
24
|
+
} catch (err) {
|
|
25
|
+
log.error(TAG, `Error in approval hook for ${request.category}: ${err}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function onAgentCreateDecided(request: ApprovalRequest): void {
|
|
30
|
+
if (request.status !== 'approved') return
|
|
31
|
+
const pendingConfig = request.data.pendingAgentConfig as Record<string, unknown> | undefined
|
|
32
|
+
if (!pendingConfig) return
|
|
33
|
+
|
|
34
|
+
// Dynamically import to avoid circular dependency
|
|
35
|
+
import('@/lib/server/agents/agent-service').then(({ createAgent }) => {
|
|
36
|
+
const agent = createAgent({ body: pendingConfig })
|
|
37
|
+
logActivity({
|
|
38
|
+
entityType: 'agent',
|
|
39
|
+
entityId: agent.id,
|
|
40
|
+
action: 'created',
|
|
41
|
+
actor: 'system',
|
|
42
|
+
summary: `Agent "${agent.name}" created after approval ${request.id}`,
|
|
43
|
+
detail: { approvalId: request.id },
|
|
44
|
+
})
|
|
45
|
+
}).catch((err) => {
|
|
46
|
+
log.error(TAG, `Failed to create agent after approval: ${err}`)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onBudgetChangeDecided(request: ApprovalRequest): void {
|
|
51
|
+
if (request.status !== 'approved') return
|
|
52
|
+
const agentId = request.data.agentId as string | undefined
|
|
53
|
+
const budgetChanges = request.data.budgetChanges as Record<string, unknown> | undefined
|
|
54
|
+
if (!agentId || !budgetChanges) return
|
|
55
|
+
|
|
56
|
+
import('@/lib/server/agents/agent-service').then(({ updateAgent }) => {
|
|
57
|
+
updateAgent(agentId, budgetChanges)
|
|
58
|
+
logActivity({
|
|
59
|
+
entityType: 'budget',
|
|
60
|
+
entityId: agentId,
|
|
61
|
+
action: 'configured',
|
|
62
|
+
actor: 'system',
|
|
63
|
+
summary: `Budget updated for agent after approval ${request.id}`,
|
|
64
|
+
detail: { approvalId: request.id, budgetChanges },
|
|
65
|
+
})
|
|
66
|
+
}).catch((err) => {
|
|
67
|
+
log.error(TAG, `Failed to apply budget change after approval: ${err}`)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onDelegationEnableDecided(request: ApprovalRequest): void {
|
|
72
|
+
if (request.status !== 'approved') return
|
|
73
|
+
const agentId = request.data.agentId as string | undefined
|
|
74
|
+
if (!agentId) return
|
|
75
|
+
|
|
76
|
+
import('@/lib/server/agents/agent-service').then(({ updateAgent }) => {
|
|
77
|
+
updateAgent(agentId, { delegationEnabled: true })
|
|
78
|
+
}).catch((err) => {
|
|
79
|
+
log.error(TAG, `Failed to enable delegation after approval: ${err}`)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
@@ -5,6 +5,8 @@ import { notify } from './ws-hub'
|
|
|
5
5
|
import { dispatchWake } from '@/lib/server/runtime/wake-dispatcher'
|
|
6
6
|
import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
|
|
7
7
|
import { enqueueSessionRun } from '@/lib/server/runtime/session-run-manager'
|
|
8
|
+
import { logActivity } from '@/lib/server/activity/activity-log'
|
|
9
|
+
import { onApprovalDecision } from '@/lib/server/approvals/approval-hooks'
|
|
8
10
|
|
|
9
11
|
function trimToString(value: unknown): string {
|
|
10
12
|
return typeof value === 'string' ? value.trim() : ''
|
|
@@ -131,6 +133,15 @@ export async function submitDecision(id: string, approved: boolean): Promise<App
|
|
|
131
133
|
if (request.status === (approved ? 'approved' : 'rejected')) return request
|
|
132
134
|
if (request.status !== 'pending') return request
|
|
133
135
|
const updated = await persistApprovalDecision(request, approved)
|
|
136
|
+
logActivity({
|
|
137
|
+
entityType: 'approval',
|
|
138
|
+
entityId: id,
|
|
139
|
+
action: approved ? 'approved' : 'rejected',
|
|
140
|
+
actor: 'user',
|
|
141
|
+
summary: `Approval ${approved ? 'approved' : 'rejected'}: ${request.title}`,
|
|
142
|
+
detail: { category: request.category, agentId: request.agentId, sessionId: request.sessionId },
|
|
143
|
+
})
|
|
144
|
+
onApprovalDecision(updated)
|
|
134
145
|
wakeForApprovalDecision(updated, approved)
|
|
135
146
|
return updated
|
|
136
147
|
}
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { log } from '@/lib/server/logger'
|
|
2
|
-
import type { BidCreateInput } from '@swarmdock/shared'
|
|
2
|
+
import type { Task, BidCreateInput } from '@swarmdock/shared'
|
|
3
3
|
|
|
4
4
|
const TAG = 'swarmdock-bid'
|
|
5
5
|
|
|
6
|
-
interface SwarmDockTask {
|
|
7
|
-
id: string
|
|
8
|
-
title: string
|
|
9
|
-
skillRequirements: string[]
|
|
10
|
-
budgetMax: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
6
|
interface SwarmDockConfig {
|
|
14
7
|
skills: string
|
|
15
8
|
maxBudget: string
|
|
@@ -20,7 +13,7 @@ interface SwarmDockConfig {
|
|
|
20
13
|
* Determine if the agent should auto-bid on a discovered task.
|
|
21
14
|
* Checks skill overlap and budget limits.
|
|
22
15
|
*/
|
|
23
|
-
export function shouldAutoBid(task:
|
|
16
|
+
export function shouldAutoBid(task: Task, config: SwarmDockConfig): boolean {
|
|
24
17
|
if (!config.autoDiscover) return false
|
|
25
18
|
|
|
26
19
|
// Check budget
|
|
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import test from 'node:test'
|
|
3
3
|
|
|
4
4
|
import { submitAutoBid } from '@/lib/server/connectors/swarmdock-bidding'
|
|
5
|
-
import { submitSwarmdockTaskResult } from '@/lib/server/connectors/swarmdock'
|
|
5
|
+
import { submitSwarmdockTaskResult, generateExamplePrompts } from '@/lib/server/connectors/swarmdock'
|
|
6
6
|
|
|
7
7
|
test('submitAutoBid includes empty portfolio refs for SDK compatibility', async () => {
|
|
8
8
|
const seen: {
|
|
@@ -37,6 +37,23 @@ test('submitAutoBid includes empty portfolio refs for SDK compatibility', async
|
|
|
37
37
|
})
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
+
test('generateExamplePrompts returns exactly 5 non-empty strings', () => {
|
|
41
|
+
const prompts = generateExamplePrompts('data-analysis')
|
|
42
|
+
assert.equal(prompts.length, 5)
|
|
43
|
+
for (const prompt of prompts) {
|
|
44
|
+
assert.equal(typeof prompt, 'string')
|
|
45
|
+
assert.ok(prompt.length > 0, 'prompt must be non-empty')
|
|
46
|
+
assert.ok(prompt.includes('data analysis'), 'prompt should include the humanized skill name')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Single-word skill
|
|
50
|
+
const simple = generateExamplePrompts('coding')
|
|
51
|
+
assert.equal(simple.length, 5)
|
|
52
|
+
for (const prompt of simple) {
|
|
53
|
+
assert.ok(prompt.includes('coding'))
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
40
57
|
test('submitSwarmdockTaskResult includes empty files and propagates submit errors', async () => {
|
|
41
58
|
const seen: {
|
|
42
59
|
taskId?: string
|