@swarmclawai/swarmclaw 1.2.9 → 1.3.0
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 +9 -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-tasks.ts +8 -0
- package/src/lib/server/connectors/swarmdock.ts +30 -0
- 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,15 @@ Read the full setup guide in [`SWARMDOCK.md`](./SWARMDOCK.md), browse the public
|
|
|
204
204
|
|
|
205
205
|
## Release Notes
|
|
206
206
|
|
|
207
|
+
### v1.3.0 Highlights
|
|
208
|
+
|
|
209
|
+
- **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.
|
|
210
|
+
- **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.
|
|
211
|
+
- **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.
|
|
212
|
+
- **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.
|
|
213
|
+
- **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.
|
|
214
|
+
- **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.
|
|
215
|
+
|
|
207
216
|
### v1.2.9 Highlights
|
|
208
217
|
|
|
209
218
|
- **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.0",
|
|
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.0",
|
|
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
|
}
|
|
@@ -100,6 +100,14 @@ export async function updateBoardTaskFromEvent(
|
|
|
100
100
|
boardTask.status = 'failed'
|
|
101
101
|
boardTask.checkoutRunId = null
|
|
102
102
|
break
|
|
103
|
+
case 'task.review':
|
|
104
|
+
// Work submitted, awaiting requester review on SwarmDock
|
|
105
|
+
if (boardTask.externalSource) boardTask.externalSource.state = 'review'
|
|
106
|
+
break
|
|
107
|
+
case 'task.disputed':
|
|
108
|
+
// Task disputed on SwarmDock
|
|
109
|
+
if (boardTask.externalSource) boardTask.externalSource.state = 'disputed'
|
|
110
|
+
break
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
boardTask.updatedAt = now
|
|
@@ -35,6 +35,7 @@ interface SwarmDockConfig {
|
|
|
35
35
|
skills: string
|
|
36
36
|
autoDiscover: boolean
|
|
37
37
|
maxBudget: string
|
|
38
|
+
paymentPrivateKey?: string
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
function parseConfig(connector: Connector): SwarmDockConfig {
|
|
@@ -46,6 +47,7 @@ function parseConfig(connector: Connector): SwarmDockConfig {
|
|
|
46
47
|
skills: c.skills || '',
|
|
47
48
|
autoDiscover: c.autoDiscover === 'true',
|
|
48
49
|
maxBudget: c.maxBudget || '0',
|
|
50
|
+
paymentPrivateKey: c.paymentPrivateKey || undefined,
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -73,11 +75,13 @@ export async function submitSwarmdockTaskResult(
|
|
|
73
75
|
client: { tasks: { submit: (taskId: string, input: TaskSubmitInput) => Promise<unknown> } },
|
|
74
76
|
swarmdockTaskId: string,
|
|
75
77
|
text: string,
|
|
78
|
+
notes?: string,
|
|
76
79
|
): Promise<void> {
|
|
77
80
|
const payload: TaskSubmitInput = {
|
|
78
81
|
artifacts: [{ type: 'text/markdown', content: text }],
|
|
79
82
|
files: [],
|
|
80
83
|
}
|
|
84
|
+
if (notes) payload.notes = notes
|
|
81
85
|
await client.tasks.submit(swarmdockTaskId, payload)
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -106,6 +110,9 @@ const swarmdock: PlatformConnector = {
|
|
|
106
110
|
const client = new SwarmDockClient({
|
|
107
111
|
baseUrl: config.apiUrl,
|
|
108
112
|
privateKey,
|
|
113
|
+
...(config.paymentPrivateKey?.startsWith('0x')
|
|
114
|
+
? { paymentPrivateKey: config.paymentPrivateKey as `0x${string}` }
|
|
115
|
+
: {}),
|
|
109
116
|
})
|
|
110
117
|
|
|
111
118
|
// Register agent on SwarmDock (Ed25519 challenge-response)
|
|
@@ -119,6 +126,8 @@ const swarmdock: PlatformConnector = {
|
|
|
119
126
|
description: `${skillId} capability`,
|
|
120
127
|
category: skillId,
|
|
121
128
|
basePrice: '1000000', // $1.00 default
|
|
129
|
+
inputModes: ['text'],
|
|
130
|
+
outputModes: ['text'],
|
|
122
131
|
}))
|
|
123
132
|
|
|
124
133
|
log.info(TAG, `Registering agent "${connector.name}" on SwarmDock at ${config.apiUrl}`)
|
|
@@ -195,6 +204,27 @@ const swarmdock: PlatformConnector = {
|
|
|
195
204
|
break
|
|
196
205
|
}
|
|
197
206
|
|
|
207
|
+
case 'task.review': {
|
|
208
|
+
const taskId = (event.data as Record<string, string>).taskId
|
|
209
|
+
if (taskId) await updateBoardTaskFromEvent(taskId, 'task.review')
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case 'task.disputed': {
|
|
214
|
+
const taskId = (event.data as Record<string, string>).taskId
|
|
215
|
+
if (taskId) {
|
|
216
|
+
await updateBoardTaskFromEvent(taskId, 'task.disputed')
|
|
217
|
+
logActivity({
|
|
218
|
+
entityType: 'connector',
|
|
219
|
+
entityId: connectorId,
|
|
220
|
+
action: 'incident',
|
|
221
|
+
actor: 'system',
|
|
222
|
+
summary: `SwarmDock task ${taskId} disputed`,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
|
|
198
228
|
case 'payment.released': {
|
|
199
229
|
const data = event.data as Record<string, string>
|
|
200
230
|
logActivity({
|