@swarmclawai/swarmclaw 0.7.5 → 0.7.7
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 +41 -10
- package/package.json +2 -2
- package/src/app/api/agents/[id]/route.ts +16 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/route.ts +21 -1
- package/src/app/api/chats/route.ts +12 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
- package/src/app/api/external-agents/[id]/route.ts +38 -6
- package/src/app/api/external-agents/route.ts +17 -1
- package/src/app/api/gateways/[id]/health/route.ts +8 -0
- package/src/app/api/gateways/[id]/route.ts +53 -1
- package/src/app/api/gateways/route.ts +53 -0
- package/src/app/api/openclaw/deploy/route.ts +240 -0
- package/src/cli/index.js +53 -0
- package/src/cli/index.test.js +102 -0
- package/src/cli/spec.js +79 -0
- package/src/components/agents/agent-sheet.tsx +97 -19
- package/src/components/auth/setup-wizard.tsx +111 -54
- package/src/components/gateways/gateway-sheet.tsx +202 -10
- package/src/components/openclaw/openclaw-deploy-panel.tsx +1208 -0
- package/src/components/providers/provider-list.tsx +321 -22
- package/src/lib/server/agent-runtime-config.ts +142 -7
- package/src/lib/server/agent-thread-session.ts +9 -1
- package/src/lib/server/chat-execution.ts +8 -2
- package/src/lib/server/heartbeat-service.ts +5 -1
- package/src/lib/server/openclaw-deploy.test.ts +75 -0
- package/src/lib/server/openclaw-deploy.ts +1384 -0
- package/src/lib/server/orchestrator.ts +9 -0
- package/src/lib/server/queue.ts +45 -2
- package/src/lib/setup-defaults.ts +2 -2
- package/src/lib/validation/schemas.ts +9 -0
- package/src/types/index.ts +65 -0
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
18
18
|
|
|
19
19
|
- [Getting Started](https://swarmclaw.ai/docs/getting-started) - install and first-run setup
|
|
20
20
|
- [Providers](https://swarmclaw.ai/docs/providers) - provider setup and failover options
|
|
21
|
+
- [OpenClaw Setup](https://swarmclaw.ai/docs/openclaw-setup) - local, VPS, and hosted OpenClaw deployment paths
|
|
21
22
|
- [Agents](https://swarmclaw.ai/docs/agents) - agent configuration, tools, and platform capabilities
|
|
22
23
|
- [Tools](https://swarmclaw.ai/docs/tools) - built-in tool reference and guardrails
|
|
23
24
|
- [Orchestration](https://swarmclaw.ai/docs/orchestration) - multi-agent flows, checkpoints, and restore
|
|
@@ -39,11 +40,29 @@ SwarmClaw includes the `openclaw` CLI as a bundled dependency, so there is no se
|
|
|
39
40
|
|
|
40
41
|
The Providers screen now supports named OpenClaw gateway profiles with discovery, health checks, default-gateway selection, and an External Agent Runtimes view for remote workers that register/heartbeat into SwarmClaw.
|
|
41
42
|
|
|
43
|
+
SwarmClaw now also includes **Smart Deploy** for OpenClaw in three places:
|
|
44
|
+
|
|
45
|
+
- **Onboarding** - non-technical users can launch a local OpenClaw runtime or generate a remote bundle before they finish first-run setup
|
|
46
|
+
- **Providers -> OpenClaw Gateways** - operators can deploy or prepare more gateways later without leaving the main app
|
|
47
|
+
- **Gateway editor** - every gateway profile includes the same deploy panel for local restarts, VPS bundles, and hosted repo-backed deployments
|
|
48
|
+
|
|
49
|
+
The deployment flow stays **in-house and official-only**:
|
|
50
|
+
|
|
51
|
+
- local deploys run the bundled official `openclaw` CLI directly from SwarmClaw
|
|
52
|
+
- VPS deploys use the official OpenClaw Docker image with prefilled `.env`, `docker-compose.yml`, `bootstrap.sh`, and `cloud-init.yaml`
|
|
53
|
+
- hosted templates target the official OpenClaw repo for Render, Fly.io, and Railway
|
|
54
|
+
|
|
55
|
+
Supported VPS presets currently include Hetzner, DigitalOcean, Vultr, Linode, Lightsail, Google Cloud, Azure, OCI, and a generic Ubuntu host path. Smart defaults prefill the gateway token, endpoint, storage paths, and copy-paste commands so the resulting gateway can be saved into SwarmClaw with minimal manual editing.
|
|
56
|
+
|
|
57
|
+
For existing hosts, SwarmClaw can also push the same official-image bundle **over SSH** and then keep remote lifecycle controls attached to that saved gateway profile: start, stop, restart, upgrade, backup, restore, and token rotation.
|
|
58
|
+
|
|
42
59
|
The OpenClaw Control Plane in SwarmClaw adds:
|
|
43
60
|
- Reload mode switching (`hot`, `hybrid`, `full`)
|
|
44
61
|
- Config issue detection and guided repair
|
|
45
62
|
- Remote history sync
|
|
46
63
|
- Live execution approval handling
|
|
64
|
+
- Gateway import/export JSON, clone flows, and richer external runtime fleet visibility
|
|
65
|
+
- Agent and route-target preferences for steering work toward OpenClaw gateways by tags or use case (`local-dev`, `single-vps`, `private-tailnet`, `browser-heavy`, `team-control`)
|
|
47
66
|
|
|
48
67
|
The Agent Inspector Panel lets you edit OpenClaw files (`SOUL.md`, `IDENTITY.md`, `USER.md`), tune personality/system behavior, and manage OpenClaw-compatible skills. SwarmClaw also supports importing OpenClaw `SKILL.md` files from URL.
|
|
49
68
|
|
|
@@ -59,6 +78,19 @@ Each agent can point to a **different** OpenClaw gateway profile or direct endpo
|
|
|
59
78
|
|
|
60
79
|
URLs without a protocol are auto-prefixed with `http://`. For remote gateways with TLS, use `https://` explicitly.
|
|
61
80
|
|
|
81
|
+
CLI operators can use the same deploy surface without opening the UI:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
swarmclaw openclaw deploy-status
|
|
85
|
+
swarmclaw openclaw deploy-local-start --data '{"port":18789}'
|
|
86
|
+
swarmclaw openclaw deploy-local-restart --data '{"port":18789}'
|
|
87
|
+
swarmclaw openclaw deploy-bundle --data '{"template":"docker","provider":"hetzner","target":"openclaw.example.com"}'
|
|
88
|
+
swarmclaw openclaw deploy-ssh --data '{"target":"openclaw.example.com","provider":"hetzner","ssh":{"host":"your-vps-ip"}}'
|
|
89
|
+
swarmclaw openclaw remote-backup --data '{"ssh":{"host":"your-vps-ip"}}'
|
|
90
|
+
swarmclaw openclaw remote-restore --data '{"backupPath":"/opt/openclaw/backups/openclaw-backup-123.tgz","ssh":{"host":"your-vps-ip"}}'
|
|
91
|
+
swarmclaw openclaw deploy-verify --data '{"endpoint":"https://openclaw.example.com/v1"}'
|
|
92
|
+
```
|
|
93
|
+
|
|
62
94
|
## SwarmClaw ClawHub Skill
|
|
63
95
|
|
|
64
96
|
Use the `swarmclaw` ClawHub skill when you want an OpenClaw agent to operate your SwarmClaw control plane directly from chat: list agents, dispatch tasks, check chats, run diagnostics, and coordinate multi-agent work.
|
|
@@ -78,7 +110,6 @@ Skill source and runbook: [`swarmclaw/SKILL.md`](swarmclaw/SKILL.md).
|
|
|
78
110
|
|
|
79
111
|
## Requirements
|
|
80
112
|
|
|
81
|
-
- **Node.js** 22.6+
|
|
82
113
|
- **Node.js** 22.6+
|
|
83
114
|
- One of: **npm** 10+, **pnpm**, **Yarn**, or **Bun**
|
|
84
115
|
- **Claude Code CLI** (optional, for `claude-cli` provider) — [Install](https://docs.anthropic.com/en/docs/claude-code/overview)
|
|
@@ -117,7 +148,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
117
148
|
```
|
|
118
149
|
|
|
119
150
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
120
|
-
To pin a version: `SWARMCLAW_VERSION=v0.7.
|
|
151
|
+
To pin a version: `SWARMCLAW_VERSION=v0.7.7 curl ... | bash`
|
|
121
152
|
|
|
122
153
|
Or run locally from the repo (friendly for non-technical users):
|
|
123
154
|
|
|
@@ -670,7 +701,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
670
701
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
671
702
|
|
|
672
703
|
```bash
|
|
673
|
-
# example patch release (v0.7.
|
|
704
|
+
# example patch release (v0.7.7 style)
|
|
674
705
|
npm version patch
|
|
675
706
|
git push origin main --follow-tags
|
|
676
707
|
```
|
|
@@ -680,15 +711,15 @@ On `v*` tags, GitHub Actions will:
|
|
|
680
711
|
2. Create a GitHub Release
|
|
681
712
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
682
713
|
|
|
683
|
-
#### v0.7.
|
|
714
|
+
#### v0.7.7 Release Readiness Notes
|
|
684
715
|
|
|
685
|
-
Before shipping `v0.7.
|
|
716
|
+
Before shipping `v0.7.7`, confirm the following user-facing changes are reflected in docs:
|
|
686
717
|
|
|
687
|
-
1.
|
|
688
|
-
2.
|
|
689
|
-
3. Site and README install/version strings are updated to `v0.7.
|
|
690
|
-
4. Release notes summarize the user-visible
|
|
691
|
-
5. CLI and
|
|
718
|
+
1. OpenClaw docs cover Smart Deploy end-to-end: onboarding, Providers, gateway editor, official-only SSH/VPS flows, safe exposure presets, and restore/backup lifecycle controls.
|
|
719
|
+
2. Agent and provider docs explain gateway routing by tags/use-case, richer external runtime visibility, and import/export/clone flows for saved gateways.
|
|
720
|
+
3. Site and README install/version strings are updated to `v0.7.7`, including install snippets, release notes index text, and sidebar/footer labels.
|
|
721
|
+
4. Release notes summarize the user-visible operator changes from the current worktree, especially SSH deploy, remote lifecycle controls, routing preferences, and onboarding persistence.
|
|
722
|
+
5. CLI docs include the expanded `openclaw deploy-*`, `openclaw remote-*`, and verify surfaces and do not reference removed or unofficial deployment paths.
|
|
692
723
|
|
|
693
724
|
## CLI
|
|
694
725
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
|
|
58
58
|
"cli": "node ./bin/swarmclaw.js",
|
|
59
59
|
"test:cli": "node --test src/cli/*.test.js bin/*.test.js",
|
|
60
|
-
"test:openclaw": "tsx --test src/lib/openclaw-agent-id.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw-agent-resolver.test.ts src/lib/server/openclaw-skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts",
|
|
60
|
+
"test:openclaw": "tsx --test src/lib/openclaw-agent-id.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw-agent-resolver.test.ts src/lib/server/openclaw-deploy.test.ts src/lib/server/openclaw-skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts",
|
|
61
61
|
"test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
|
|
62
62
|
"postinstall": "node ./scripts/postinstall.mjs"
|
|
63
63
|
},
|
|
@@ -23,6 +23,16 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
23
23
|
body.apiEndpoint,
|
|
24
24
|
)
|
|
25
25
|
}
|
|
26
|
+
if (body.preferredGatewayTags !== undefined) {
|
|
27
|
+
agent.preferredGatewayTags = Array.isArray(body.preferredGatewayTags)
|
|
28
|
+
? body.preferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
29
|
+
: []
|
|
30
|
+
}
|
|
31
|
+
if (body.preferredGatewayUseCase !== undefined) {
|
|
32
|
+
agent.preferredGatewayUseCase = typeof body.preferredGatewayUseCase === 'string' && body.preferredGatewayUseCase.trim()
|
|
33
|
+
? body.preferredGatewayUseCase.trim()
|
|
34
|
+
: null
|
|
35
|
+
}
|
|
26
36
|
if (body.routingTargets !== undefined && Array.isArray(body.routingTargets)) {
|
|
27
37
|
agent.routingTargets = body.routingTargets.map((target: Record<string, unknown>, index: number) => ({
|
|
28
38
|
id: typeof target.id === 'string' && target.id.trim() ? target.id.trim() : `route-${index + 1}`,
|
|
@@ -37,6 +47,12 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
37
47
|
typeof target.apiEndpoint === 'string' ? target.apiEndpoint : null,
|
|
38
48
|
),
|
|
39
49
|
gatewayProfileId: target.gatewayProfileId ?? null,
|
|
50
|
+
preferredGatewayTags: Array.isArray(target.preferredGatewayTags)
|
|
51
|
+
? target.preferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
52
|
+
: [],
|
|
53
|
+
preferredGatewayUseCase: typeof target.preferredGatewayUseCase === 'string' && target.preferredGatewayUseCase.trim()
|
|
54
|
+
? target.preferredGatewayUseCase.trim()
|
|
55
|
+
: null,
|
|
40
56
|
priority: typeof target.priority === 'number' ? target.priority : index + 1,
|
|
41
57
|
}))
|
|
42
58
|
}
|
|
@@ -64,6 +64,8 @@ export async function POST(req: Request) {
|
|
|
64
64
|
fallbackCredentialIds: body.fallbackCredentialIds,
|
|
65
65
|
apiEndpoint: normalizeProviderEndpoint(body.provider, body.apiEndpoint || null),
|
|
66
66
|
gatewayProfileId: body.gatewayProfileId,
|
|
67
|
+
preferredGatewayTags: body.preferredGatewayTags,
|
|
68
|
+
preferredGatewayUseCase: body.preferredGatewayUseCase,
|
|
67
69
|
routingStrategy: body.routingStrategy,
|
|
68
70
|
routingTargets: body.routingTargets?.map((target) => ({
|
|
69
71
|
...target,
|
|
@@ -18,7 +18,20 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
|
|
21
|
-
const
|
|
21
|
+
const routePreferredGatewayTags = updates.routePreferredGatewayTags !== undefined
|
|
22
|
+
? (Array.isArray(updates.routePreferredGatewayTags)
|
|
23
|
+
? updates.routePreferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
24
|
+
: [])
|
|
25
|
+
: (sessions[id].routePreferredGatewayTags || [])
|
|
26
|
+
const routePreferredGatewayUseCase = updates.routePreferredGatewayUseCase !== undefined
|
|
27
|
+
? (typeof updates.routePreferredGatewayUseCase === 'string' && updates.routePreferredGatewayUseCase.trim()
|
|
28
|
+
? updates.routePreferredGatewayUseCase.trim()
|
|
29
|
+
: null)
|
|
30
|
+
: (sessions[id].routePreferredGatewayUseCase || null)
|
|
31
|
+
const linkedRoute = linkedAgent ? resolvePrimaryAgentRoute(linkedAgent, undefined, {
|
|
32
|
+
preferredGatewayTags: routePreferredGatewayTags,
|
|
33
|
+
preferredGatewayUseCase: routePreferredGatewayUseCase,
|
|
34
|
+
}) : null
|
|
22
35
|
|
|
23
36
|
if (updates.name !== undefined) sessions[id].name = updates.name
|
|
24
37
|
if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
|
|
@@ -39,6 +52,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
39
52
|
if (updates.gatewayProfileId !== undefined) sessions[id].gatewayProfileId = updates.gatewayProfileId
|
|
40
53
|
else if (agentIdUpdateProvided && linkedRoute) sessions[id].gatewayProfileId = linkedRoute.gatewayProfileId ?? null
|
|
41
54
|
|
|
55
|
+
if (updates.routePreferredGatewayTags !== undefined) {
|
|
56
|
+
sessions[id].routePreferredGatewayTags = routePreferredGatewayTags
|
|
57
|
+
}
|
|
58
|
+
if (updates.routePreferredGatewayUseCase !== undefined) {
|
|
59
|
+
sessions[id].routePreferredGatewayUseCase = routePreferredGatewayUseCase
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
if (updates.plugins !== undefined) sessions[id].plugins = updates.plugins
|
|
43
63
|
else if (agentIdUpdateProvided && linkedAgent) sessions[id].plugins = Array.isArray(linkedAgent.plugins) ? linkedAgent.plugins : []
|
|
44
64
|
|
|
@@ -61,7 +61,16 @@ export async function POST(req: Request) {
|
|
|
61
61
|
const id = body.id || genId()
|
|
62
62
|
const sessions = loadSessions()
|
|
63
63
|
const agent = body.agentId ? loadAgents()[body.agentId] : null
|
|
64
|
-
const
|
|
64
|
+
const routePreferredGatewayTags = Array.isArray(body.routePreferredGatewayTags)
|
|
65
|
+
? body.routePreferredGatewayTags.filter((tag: unknown): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
66
|
+
: []
|
|
67
|
+
const routePreferredGatewayUseCase = typeof body.routePreferredGatewayUseCase === 'string' && body.routePreferredGatewayUseCase.trim()
|
|
68
|
+
? body.routePreferredGatewayUseCase.trim()
|
|
69
|
+
: null
|
|
70
|
+
const resolvedRoute = agent ? resolvePrimaryAgentRoute(agent, undefined, {
|
|
71
|
+
preferredGatewayTags: routePreferredGatewayTags,
|
|
72
|
+
preferredGatewayUseCase: routePreferredGatewayUseCase,
|
|
73
|
+
}) : null
|
|
65
74
|
const requestedPlugins = Array.isArray(body.plugins) ? body.plugins : (Array.isArray(body.tools) ? body.tools : null)
|
|
66
75
|
const resolvedPlugins = requestedPlugins ?? (Array.isArray(agent?.plugins) ? agent.plugins : (Array.isArray(agent?.tools) ? agent.tools : []))
|
|
67
76
|
|
|
@@ -83,6 +92,8 @@ export async function POST(req: Request) {
|
|
|
83
92
|
body.provider || agent?.provider || 'claude-cli',
|
|
84
93
|
body.apiEndpoint || agent?.apiEndpoint || null,
|
|
85
94
|
),
|
|
95
|
+
routePreferredGatewayTags,
|
|
96
|
+
routePreferredGatewayUseCase,
|
|
86
97
|
claudeSessionId: null,
|
|
87
98
|
codexThreadId: null,
|
|
88
99
|
opencodeSessionId: null,
|
|
@@ -15,6 +15,9 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
15
15
|
runtime.lastSeenAt = now
|
|
16
16
|
runtime.updatedAt = now
|
|
17
17
|
runtime.status = body.status || 'online'
|
|
18
|
+
if (typeof body.lifecycleState === 'string' && body.lifecycleState) runtime.lifecycleState = body.lifecycleState
|
|
19
|
+
if (typeof body.version === 'string') runtime.version = body.version || null
|
|
20
|
+
if (typeof body.lastHealthNote === 'string') runtime.lastHealthNote = body.lastHealthNote || null
|
|
18
21
|
if (body.tokenStats && typeof body.tokenStats === 'object') {
|
|
19
22
|
runtime.tokenStats = {
|
|
20
23
|
...(runtime.tokenStats || {}),
|
|
@@ -10,12 +10,44 @@ const ops: CollectionOps<any> = { load: loadExternalAgents, save: saveExternalAg
|
|
|
10
10
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
11
11
|
const { id } = await params
|
|
12
12
|
const body = await req.json().catch(() => ({}))
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
const now = Date.now()
|
|
14
|
+
const result = mutateItem(ops, id, (runtime) => {
|
|
15
|
+
const action = typeof body.action === 'string' ? body.action : ''
|
|
16
|
+
const nextMetadata = body.metadata && typeof body.metadata === 'object'
|
|
17
|
+
? { ...(runtime.metadata || {}), ...body.metadata }
|
|
18
|
+
: runtime.metadata
|
|
19
|
+
|
|
20
|
+
const next = {
|
|
21
|
+
...runtime,
|
|
22
|
+
...body,
|
|
23
|
+
id,
|
|
24
|
+
metadata: nextMetadata,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (action === 'activate') {
|
|
29
|
+
next.lifecycleState = 'active'
|
|
30
|
+
next.lastHealthNote = body.lastHealthNote || 'Runtime returned to active service.'
|
|
31
|
+
} else if (action === 'drain') {
|
|
32
|
+
next.lifecycleState = 'draining'
|
|
33
|
+
next.lastHealthNote = body.lastHealthNote || 'Runtime draining after current work.'
|
|
34
|
+
} else if (action === 'cordon') {
|
|
35
|
+
next.lifecycleState = 'cordoned'
|
|
36
|
+
next.lastHealthNote = body.lastHealthNote || 'Runtime cordoned from new work.'
|
|
37
|
+
} else if (action === 'restart') {
|
|
38
|
+
next.metadata = {
|
|
39
|
+
...(next.metadata || {}),
|
|
40
|
+
controlRequest: {
|
|
41
|
+
action: 'restart',
|
|
42
|
+
requestedAt: now,
|
|
43
|
+
source: 'swarmclaw',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
next.lastHealthNote = body.lastHealthNote || 'Restart requested from SwarmClaw.'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return next
|
|
50
|
+
})
|
|
19
51
|
if (!result) return notFound()
|
|
20
52
|
return NextResponse.json(result)
|
|
21
53
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { formatZodError, ExternalAgentRegisterSchema } from '@/lib/validation/schemas'
|
|
4
|
-
import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
|
|
4
|
+
import { loadExternalAgents, loadGatewayProfiles, saveExternalAgents } from '@/lib/server/storage'
|
|
5
5
|
import { notify } from '@/lib/server/ws-hub'
|
|
6
6
|
import type { ExternalAgentRuntime } from '@/types'
|
|
7
7
|
import { z } from 'zod'
|
|
@@ -13,9 +13,20 @@ function withDerivedStatus(record: ExternalAgentRuntime): ExternalAgentRuntime {
|
|
|
13
13
|
const staleMs = 3 * 60_000
|
|
14
14
|
if (!lastSeenAt) return { ...record, status: record.status || 'offline' }
|
|
15
15
|
if (record.status === 'offline') return record
|
|
16
|
+
const gateways = loadGatewayProfiles()
|
|
17
|
+
const gateway = record.gatewayProfileId ? gateways[record.gatewayProfileId] as Record<string, unknown> | undefined : undefined
|
|
18
|
+
const gatewayTags = Array.isArray(gateway?.tags)
|
|
19
|
+
? gateway?.tags.filter((tag): tag is string => typeof tag === 'string' && tag.trim().length > 0)
|
|
20
|
+
: []
|
|
21
|
+
const gatewayUseCase = gateway?.deployment && typeof gateway.deployment === 'object' && typeof (gateway.deployment as Record<string, unknown>).useCase === 'string'
|
|
22
|
+
? (gateway.deployment as Record<string, unknown>).useCase as string
|
|
23
|
+
: null
|
|
16
24
|
return {
|
|
17
25
|
...record,
|
|
18
26
|
status: now - lastSeenAt > staleMs ? 'stale' : (record.status || 'online'),
|
|
27
|
+
lifecycleState: record.lifecycleState || 'active',
|
|
28
|
+
gatewayTags: record.gatewayTags?.length ? record.gatewayTags : gatewayTags,
|
|
29
|
+
gatewayUseCase: record.gatewayUseCase || gatewayUseCase,
|
|
19
30
|
}
|
|
20
31
|
}
|
|
21
32
|
|
|
@@ -53,6 +64,11 @@ export async function POST(req: Request) {
|
|
|
53
64
|
gatewayProfileId: body.gatewayProfileId || null,
|
|
54
65
|
capabilities: body.capabilities,
|
|
55
66
|
labels: body.labels,
|
|
67
|
+
lifecycleState: body.lifecycleState || existing?.lifecycleState || 'active',
|
|
68
|
+
gatewayTags: body.gatewayTags,
|
|
69
|
+
gatewayUseCase: body.gatewayUseCase || null,
|
|
70
|
+
version: body.version || null,
|
|
71
|
+
lastHealthNote: body.lastHealthNote || null,
|
|
56
72
|
metadata: body.metadata,
|
|
57
73
|
tokenStats: body.tokenStats,
|
|
58
74
|
lastHeartbeatAt: existing?.lastHeartbeatAt || now,
|
|
@@ -20,6 +20,14 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
20
20
|
gateway.lastCheckedAt = Date.now()
|
|
21
21
|
gateway.lastError = result.ok ? null : (result.error || result.hint || 'Gateway health check failed.')
|
|
22
22
|
gateway.lastModelCount = Array.isArray(result.models) ? result.models.length : 0
|
|
23
|
+
gateway.deployment = {
|
|
24
|
+
...(gateway.deployment || {}),
|
|
25
|
+
lastVerifiedAt: Date.now(),
|
|
26
|
+
lastVerifiedOk: result.ok,
|
|
27
|
+
lastVerifiedMessage: result.ok
|
|
28
|
+
? `Verified ${Array.isArray(result.models) ? result.models.length : 0} model${result.models?.length === 1 ? '' : 's'}`
|
|
29
|
+
: (result.error || result.hint || 'Gateway health check failed.'),
|
|
30
|
+
}
|
|
23
31
|
gateway.updatedAt = Date.now()
|
|
24
32
|
saveGatewayProfiles(gateways)
|
|
25
33
|
notify('gateways')
|
|
@@ -2,7 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
|
|
3
3
|
import { loadAgents, loadGatewayProfiles, saveAgents, saveGatewayProfiles } from '@/lib/server/storage'
|
|
4
4
|
import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
5
|
-
import type { Agent, AgentRoutingTarget, GatewayProfile } from '@/types'
|
|
5
|
+
import type { Agent, AgentRoutingTarget, GatewayProfile, OpenClawDeploymentConfig, OpenClawGatewayStats } from '@/types'
|
|
6
6
|
|
|
7
7
|
const ops: CollectionOps<GatewayProfile> = {
|
|
8
8
|
load: loadGatewayProfiles,
|
|
@@ -17,6 +17,56 @@ function normalizeTags(value: unknown): string[] {
|
|
|
17
17
|
.filter(Boolean)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
function normalizeText(value: unknown): string | null {
|
|
21
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeNullableNumber(value: unknown): number | null {
|
|
25
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
|
|
29
|
+
if (!value || typeof value !== 'object') return null
|
|
30
|
+
const deployment = value as Record<string, unknown>
|
|
31
|
+
return {
|
|
32
|
+
method: normalizeText(deployment.method) as OpenClawDeploymentConfig['method'],
|
|
33
|
+
provider: normalizeText(deployment.provider) as OpenClawDeploymentConfig['provider'],
|
|
34
|
+
remoteTarget: normalizeText(deployment.remoteTarget) as OpenClawDeploymentConfig['remoteTarget'],
|
|
35
|
+
useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
|
|
36
|
+
exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
|
|
37
|
+
managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
|
|
38
|
+
targetHost: normalizeText(deployment.targetHost),
|
|
39
|
+
sshHost: normalizeText(deployment.sshHost),
|
|
40
|
+
sshUser: normalizeText(deployment.sshUser),
|
|
41
|
+
sshPort: normalizeNullableNumber(deployment.sshPort),
|
|
42
|
+
sshKeyPath: normalizeText(deployment.sshKeyPath),
|
|
43
|
+
sshTargetDir: normalizeText(deployment.sshTargetDir),
|
|
44
|
+
image: normalizeText(deployment.image),
|
|
45
|
+
version: normalizeText(deployment.version),
|
|
46
|
+
lastDeployAt: normalizeNullableNumber(deployment.lastDeployAt),
|
|
47
|
+
lastDeployAction: normalizeText(deployment.lastDeployAction),
|
|
48
|
+
lastDeployProcessId: normalizeText(deployment.lastDeployProcessId),
|
|
49
|
+
lastDeploySummary: normalizeText(deployment.lastDeploySummary),
|
|
50
|
+
lastVerifiedAt: normalizeNullableNumber(deployment.lastVerifiedAt),
|
|
51
|
+
lastVerifiedOk: typeof deployment.lastVerifiedOk === 'boolean' ? deployment.lastVerifiedOk : null,
|
|
52
|
+
lastVerifiedMessage: normalizeText(deployment.lastVerifiedMessage),
|
|
53
|
+
lastBackupPath: normalizeText(deployment.lastBackupPath),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeStats(value: unknown): OpenClawGatewayStats | null {
|
|
58
|
+
if (!value || typeof value !== 'object') return null
|
|
59
|
+
const stats = value as Record<string, unknown>
|
|
60
|
+
return {
|
|
61
|
+
nodeCount: normalizeNullableNumber(stats.nodeCount) ?? undefined,
|
|
62
|
+
connectedNodeCount: normalizeNullableNumber(stats.connectedNodeCount) ?? undefined,
|
|
63
|
+
pendingNodePairings: normalizeNullableNumber(stats.pendingNodePairings) ?? undefined,
|
|
64
|
+
pairedDeviceCount: normalizeNullableNumber(stats.pairedDeviceCount) ?? undefined,
|
|
65
|
+
pendingDevicePairings: normalizeNullableNumber(stats.pendingDevicePairings) ?? undefined,
|
|
66
|
+
externalRuntimeCount: normalizeNullableNumber(stats.externalRuntimeCount) ?? undefined,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
20
70
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
21
71
|
const { id } = await params
|
|
22
72
|
const body = await req.json().catch(() => ({}))
|
|
@@ -39,6 +89,8 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
39
89
|
if (body.lastModelCount !== undefined) gateway.lastModelCount = body.lastModelCount || null
|
|
40
90
|
if (body.discoveredHost !== undefined) gateway.discoveredHost = body.discoveredHost || null
|
|
41
91
|
if (body.discoveredPort !== undefined) gateway.discoveredPort = body.discoveredPort || null
|
|
92
|
+
if (body.deployment !== undefined) gateway.deployment = { ...(gateway.deployment || {}), ...(normalizeDeployment(body.deployment) || {}) }
|
|
93
|
+
if (body.stats !== undefined) gateway.stats = { ...(gateway.stats || {}), ...(normalizeStats(body.stats) || {}) }
|
|
42
94
|
if (body.isDefault !== undefined) gateway.isDefault = body.isDefault === true
|
|
43
95
|
gateway.updatedAt = Date.now()
|
|
44
96
|
return gateway
|
|
@@ -4,6 +4,7 @@ import { normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
|
|
|
4
4
|
import { getGatewayProfiles } from '@/lib/server/agent-runtime-config'
|
|
5
5
|
import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
|
|
6
6
|
import { notify } from '@/lib/server/ws-hub'
|
|
7
|
+
import type { OpenClawDeploymentConfig, OpenClawGatewayStats } from '@/types'
|
|
7
8
|
export const dynamic = 'force-dynamic'
|
|
8
9
|
|
|
9
10
|
function normalizeTags(value: unknown): string[] {
|
|
@@ -13,6 +14,56 @@ function normalizeTags(value: unknown): string[] {
|
|
|
13
14
|
.filter(Boolean)
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function normalizeText(value: unknown): string | null {
|
|
18
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeNullableNumber(value: unknown): number | null {
|
|
22
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
|
|
26
|
+
if (!value || typeof value !== 'object') return null
|
|
27
|
+
const deployment = value as Record<string, unknown>
|
|
28
|
+
return {
|
|
29
|
+
method: normalizeText(deployment.method) as OpenClawDeploymentConfig['method'],
|
|
30
|
+
provider: normalizeText(deployment.provider) as OpenClawDeploymentConfig['provider'],
|
|
31
|
+
remoteTarget: normalizeText(deployment.remoteTarget) as OpenClawDeploymentConfig['remoteTarget'],
|
|
32
|
+
useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
|
|
33
|
+
exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
|
|
34
|
+
managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
|
|
35
|
+
targetHost: normalizeText(deployment.targetHost),
|
|
36
|
+
sshHost: normalizeText(deployment.sshHost),
|
|
37
|
+
sshUser: normalizeText(deployment.sshUser),
|
|
38
|
+
sshPort: normalizeNullableNumber(deployment.sshPort),
|
|
39
|
+
sshKeyPath: normalizeText(deployment.sshKeyPath),
|
|
40
|
+
sshTargetDir: normalizeText(deployment.sshTargetDir),
|
|
41
|
+
image: normalizeText(deployment.image),
|
|
42
|
+
version: normalizeText(deployment.version),
|
|
43
|
+
lastDeployAt: normalizeNullableNumber(deployment.lastDeployAt),
|
|
44
|
+
lastDeployAction: normalizeText(deployment.lastDeployAction),
|
|
45
|
+
lastDeployProcessId: normalizeText(deployment.lastDeployProcessId),
|
|
46
|
+
lastDeploySummary: normalizeText(deployment.lastDeploySummary),
|
|
47
|
+
lastVerifiedAt: normalizeNullableNumber(deployment.lastVerifiedAt),
|
|
48
|
+
lastVerifiedOk: typeof deployment.lastVerifiedOk === 'boolean' ? deployment.lastVerifiedOk : null,
|
|
49
|
+
lastVerifiedMessage: normalizeText(deployment.lastVerifiedMessage),
|
|
50
|
+
lastBackupPath: normalizeText(deployment.lastBackupPath),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeStats(value: unknown): OpenClawGatewayStats | null {
|
|
55
|
+
if (!value || typeof value !== 'object') return null
|
|
56
|
+
const stats = value as Record<string, unknown>
|
|
57
|
+
return {
|
|
58
|
+
nodeCount: normalizeNullableNumber(stats.nodeCount) ?? undefined,
|
|
59
|
+
connectedNodeCount: normalizeNullableNumber(stats.connectedNodeCount) ?? undefined,
|
|
60
|
+
pendingNodePairings: normalizeNullableNumber(stats.pendingNodePairings) ?? undefined,
|
|
61
|
+
pairedDeviceCount: normalizeNullableNumber(stats.pairedDeviceCount) ?? undefined,
|
|
62
|
+
pendingDevicePairings: normalizeNullableNumber(stats.pendingDevicePairings) ?? undefined,
|
|
63
|
+
externalRuntimeCount: normalizeNullableNumber(stats.externalRuntimeCount) ?? undefined,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
16
67
|
export async function GET() {
|
|
17
68
|
return NextResponse.json(getGatewayProfiles('openclaw'))
|
|
18
69
|
}
|
|
@@ -46,6 +97,8 @@ export async function POST(req: Request) {
|
|
|
46
97
|
lastModelCount: null,
|
|
47
98
|
discoveredHost: typeof body.discoveredHost === 'string' ? body.discoveredHost : null,
|
|
48
99
|
discoveredPort: typeof body.discoveredPort === 'number' ? body.discoveredPort : null,
|
|
100
|
+
deployment: normalizeDeployment(body.deployment),
|
|
101
|
+
stats: normalizeStats(body.stats),
|
|
49
102
|
isDefault,
|
|
50
103
|
createdAt: now,
|
|
51
104
|
updatedAt: now,
|