@swarmclawai/swarmclaw 1.9.2 → 1.9.4

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.
Files changed (32) hide show
  1. package/README.md +23 -3
  2. package/electron-dist/main.js +218 -0
  3. package/package.json +2 -2
  4. package/src/app/api/extensions/managed-resources/route.test.ts +117 -0
  5. package/src/app/api/extensions/managed-resources/route.ts +116 -0
  6. package/src/app/api/gateways/[id]/environments/[environmentId]/route.ts +16 -0
  7. package/src/app/api/gateways/[id]/environments/route.ts +13 -0
  8. package/src/app/api/gateways/topology-route.test.ts +30 -0
  9. package/src/app/api/tasks/task-workspace-route.test.ts +4 -0
  10. package/src/cli/index.js +4 -0
  11. package/src/cli/spec.js +4 -0
  12. package/src/components/providers/provider-list.tsx +34 -1
  13. package/src/components/tasks/task-sheet.tsx +50 -0
  14. package/src/features/gateways/queries.ts +3 -0
  15. package/src/lib/server/extension-managed-resources.test.ts +159 -0
  16. package/src/lib/server/extension-managed-resources.ts +905 -0
  17. package/src/lib/server/extensions.ts +113 -2
  18. package/src/lib/server/gateways/gateway-profile-service.ts +2 -0
  19. package/src/lib/server/gateways/gateway-topology.test.ts +59 -3
  20. package/src/lib/server/gateways/gateway-topology.ts +129 -3
  21. package/src/lib/server/operations/operation-pulse.test.ts +29 -0
  22. package/src/lib/server/operations/operation-pulse.ts +9 -0
  23. package/src/lib/server/session-tools/extension-creator.ts +50 -0
  24. package/src/lib/server/tasks/task-execution-workspace.test.ts +14 -0
  25. package/src/lib/server/tasks/task-execution-workspace.ts +133 -6
  26. package/src/types/agent.ts +2 -0
  27. package/src/types/app-settings.ts +8 -0
  28. package/src/types/extension.ts +132 -0
  29. package/src/types/misc.ts +31 -0
  30. package/src/types/schedule.ts +3 -0
  31. package/src/types/task.ts +30 -0
  32. package/src/views/settings/extension-manager.tsx +157 -1
package/README.md CHANGED
@@ -185,7 +185,7 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
185
185
  - **Wallets**: linked Base wallet generation, address management, approval-oriented limits, and agent payout identity.
186
186
  - **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, SwarmFeed, and more.
187
187
  - **MCP Servers**: connect any Model Context Protocol server (stdio, SSE, or streamable HTTP) and inject its tools into agents alongside built-ins. Configure, test, and assign per-agent from the MCP Servers panel.
188
- - **Extensions**: external tool extensions, UI modules, hooks, and install/update flows.
188
+ - **Extensions**: external tool extensions, UI modules, hooks, install/update flows, and managed resource manifests for extension-owned agents, routines, local folders, gateways, and setup checks.
189
189
 
190
190
  ## What SwarmClaw Focuses On
191
191
 
@@ -399,9 +399,29 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.9.4 Highlights
403
+
404
+ Bundled runtime-environment release: gateway execution visibility, task context handoff, and operator triage in one release cycle.
405
+
406
+ - **OpenClaw environments.** Gateway topology now calls `environments.list`, stores available environment counts, exposes `/api/gateways/:id/environments`, and adds CLI commands for list/status checks.
407
+ - **Provider dashboard visibility.** The Providers screen now shows fleet-wide and per-gateway execution environment availability alongside nodes, sessions, presence, and pairings.
408
+ - **Task context packets.** Prepared task workspaces now write `context.json` with task, preview, runtime, blocker, tag, and upstream-result context for external workers.
409
+ - **Runtime env handoff.** Workspaces now include `.env.swarmclaw` plus SwarmClaw, portable task/workspace, and `AGENT_HOME` env hints without embedding secrets.
410
+ - **Operations Pulse triage.** Gateway actions now surface zero-available-environment states as high-priority operator work.
411
+
412
+ ### v1.9.3 Highlights
413
+
414
+ Bundled extension-orchestration release: managed plugin resources, gateway/setup declarations, and safer local folder access in one release cycle.
415
+
416
+ - **Managed extension resources.** Extensions can now declare provisionable agents, schedules/routines, local folders, gateway platforms, and setup checks through `managedResources` or top-level manifest aliases.
417
+ - **Deterministic reconciliation.** `/api/extensions/managed-resources` can preview and reconcile extension-owned agents and routines with stable IDs and `managedByExtension` markers.
418
+ - **Trusted local folders.** Extension-declared local folders support root-bounded inspection and recursive listing with traversal and symlink-escape protection.
419
+ - **Operator UI.** The Extensions screen now shows managed-resource badges and a Managed tab with totals plus per-extension reconcile controls.
420
+ - **Extension authoring spec.** `extension_creator` now documents managed resources, gateway declarations, setup checks, and manifest aliases.
421
+
402
422
  ### v1.9.2 Highlights
403
423
 
404
- Bundled competitor-parity release: Hermes-style reasoning hygiene, deterministic delegation routing, Mission Control task workflow polish, OpenClaw export hardening, and Paperclip-style timeout hygiene.
424
+ Bundled runtime-polish release: reasoning hygiene, deterministic delegation routing, task workflow polish, OpenClaw export hardening, and timeout hygiene.
405
425
 
406
426
  - **Stateful reasoning tag scrubber.** String-streamed `<think>`, `<thinking>`, `<reasoning>`, `<thought>`, and `<REASONING_SCRATCHPAD>` blocks are removed across split deltas and routed into SwarmClaw's thinking stream instead of leaking into visible answers.
407
427
  - **Deterministic delegation profiles.** `manage_tasks` now accepts explicit `workType` and `requiredCapabilities` routing hints, returns a stable `routeKey`, and can auto-assign unowned work without a classifier call when the profile is explicit.
@@ -411,7 +431,7 @@ Bundled competitor-parity release: Hermes-style reasoning hygiene, deterministic
411
431
 
412
432
  ### v1.9.1 Highlights
413
433
 
414
- Task execution workspace release: the first Paperclip-style work-control slice for task-scoped workspaces, preview handoffs, and liveness evidence.
434
+ Task execution workspace release: task-scoped workspaces, preview handoffs, and liveness evidence.
415
435
 
416
436
  - **Task-scoped execution workspaces.** Tasks can now provision a deterministic workspace under the SwarmClaw workspace root, preserving source cwd and project context while creating a task-local README for artifacts and handoffs.
417
437
  - **Preview and runtime metadata.** Tasks can carry preview links and runtime services, and the task board surfaces those links directly on task cards and sheets.
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const electron_1 = require("electron");
40
+ const node_fs_1 = __importDefault(require("node:fs"));
41
+ const node_path_1 = __importDefault(require("node:path"));
42
+ const paths_1 = require("./paths");
43
+ const server_lifecycle_1 = require("./server-lifecycle");
44
+ const menu_1 = require("./menu");
45
+ const DEV_URL_DEFAULT = 'http://127.0.0.1:3456';
46
+ const LOG_TAIL_BYTES = 1500;
47
+ let mainWindow = null;
48
+ let serverHandle = null;
49
+ let serverLogFile = null;
50
+ let isQuitting = false;
51
+ const gotLock = electron_1.app.requestSingleInstanceLock();
52
+ if (!gotLock) {
53
+ electron_1.app.quit();
54
+ }
55
+ else {
56
+ electron_1.app.on('second-instance', () => {
57
+ if (mainWindow) {
58
+ if (mainWindow.isMinimized())
59
+ mainWindow.restore();
60
+ mainWindow.focus();
61
+ }
62
+ });
63
+ electron_1.app.on('ready', () => void onReady());
64
+ electron_1.app.on('window-all-closed', () => {
65
+ if (process.platform !== 'darwin')
66
+ electron_1.app.quit();
67
+ });
68
+ electron_1.app.on('activate', () => {
69
+ if (mainWindow !== null)
70
+ return;
71
+ if (serverHandle) {
72
+ createMainWindow(serverHandle.url);
73
+ }
74
+ else if (!electron_1.app.isPackaged) {
75
+ createMainWindow(process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT);
76
+ }
77
+ });
78
+ electron_1.app.on('before-quit', () => {
79
+ isQuitting = true;
80
+ });
81
+ electron_1.app.on('will-quit', async (event) => {
82
+ if (!serverHandle)
83
+ return;
84
+ event.preventDefault();
85
+ try {
86
+ await serverHandle.stop();
87
+ }
88
+ finally {
89
+ serverHandle = null;
90
+ electron_1.app.exit(0);
91
+ }
92
+ });
93
+ }
94
+ async function onReady() {
95
+ const paths = (0, paths_1.resolveRuntimePaths)();
96
+ (0, menu_1.buildAppMenu)(paths, () => mainWindow);
97
+ const iconPath = resolveIconPath();
98
+ if (process.platform === 'darwin' && iconPath && electron_1.app.dock) {
99
+ const img = electron_1.nativeImage.createFromPath(iconPath);
100
+ if (!img.isEmpty())
101
+ electron_1.app.dock.setIcon(img);
102
+ }
103
+ if (!electron_1.app.isPackaged) {
104
+ const devUrl = process.env.SWARMCLAW_DEV_URL || DEV_URL_DEFAULT;
105
+ console.log(`[swarmclaw] dev mode, loading ${devUrl}`);
106
+ createMainWindow(devUrl);
107
+ return;
108
+ }
109
+ serverLogFile = node_path_1.default.join(electron_1.app.getPath('userData'), 'logs', 'server.log');
110
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(serverLogFile), { recursive: true });
111
+ try {
112
+ serverHandle = await (0, server_lifecycle_1.startEmbeddedServer)({
113
+ paths,
114
+ logFile: serverLogFile,
115
+ onStdout: (c) => process.stdout.write(`[swarmclaw] ${c}`),
116
+ onStderr: (c) => process.stderr.write(`[swarmclaw] ${c}`),
117
+ onExit: (code, signal) => {
118
+ if (!isQuitting) {
119
+ console.error(`[swarmclaw] server exited unexpectedly (code=${code}, signal=${signal ?? 'none'})`);
120
+ void showServerCrashDialog(code, signal);
121
+ }
122
+ },
123
+ });
124
+ }
125
+ catch (err) {
126
+ await showStartupFailureDialog(err, paths);
127
+ electron_1.app.exit(1);
128
+ return;
129
+ }
130
+ createMainWindow(serverHandle.url);
131
+ void Promise.resolve().then(() => __importStar(require('./updater'))).then((m) => m.initAutoUpdater());
132
+ }
133
+ function resolveIconPath() {
134
+ const candidate = electron_1.app.isPackaged
135
+ ? node_path_1.default.join(process.resourcesPath, 'icon.png')
136
+ : node_path_1.default.join(__dirname, '..', 'resources', 'icon.png');
137
+ return node_fs_1.default.existsSync(candidate) ? candidate : undefined;
138
+ }
139
+ function createMainWindow(startUrl) {
140
+ const iconPath = resolveIconPath();
141
+ mainWindow = new electron_1.BrowserWindow({
142
+ width: 1440,
143
+ height: 900,
144
+ minWidth: 1024,
145
+ minHeight: 640,
146
+ backgroundColor: '#0b0b0f',
147
+ show: true,
148
+ ...(iconPath ? { icon: iconPath } : {}),
149
+ webPreferences: {
150
+ contextIsolation: true,
151
+ nodeIntegration: false,
152
+ sandbox: false,
153
+ },
154
+ });
155
+ const wc = mainWindow.webContents;
156
+ if (!electron_1.app.isPackaged)
157
+ wc.openDevTools({ mode: 'detach' });
158
+ wc.on('did-start-loading', () => console.log('[swarmclaw] did-start-loading'));
159
+ wc.on('did-finish-load', () => console.log('[swarmclaw] did-finish-load'));
160
+ wc.on('did-fail-load', (_e, code, desc, url) => console.error(`[swarmclaw] did-fail-load code=${code} desc=${desc} url=${url}`));
161
+ wc.on('render-process-gone', (_e, details) => console.error(`[swarmclaw] render-process-gone reason=${details.reason}`));
162
+ wc.on('unresponsive', () => console.error('[swarmclaw] webContents unresponsive'));
163
+ mainWindow.on('closed', () => {
164
+ mainWindow = null;
165
+ });
166
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
167
+ if (url.startsWith(startUrl))
168
+ return { action: 'allow' };
169
+ void electron_1.shell.openExternal(url);
170
+ return { action: 'deny' };
171
+ });
172
+ void mainWindow.loadURL(startUrl).catch((err) => {
173
+ console.error('[swarmclaw] loadURL rejected:', err);
174
+ });
175
+ }
176
+ async function showServerCrashDialog(code, signal) {
177
+ const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
178
+ const quitButtonId = buttons.length - 1;
179
+ const detail = buildLogDetail(`code=${code ?? 'null'} signal=${signal ?? 'none'}`);
180
+ const res = await electron_1.dialog.showMessageBox({
181
+ type: 'error',
182
+ buttons,
183
+ defaultId: quitButtonId,
184
+ cancelId: quitButtonId,
185
+ title: 'SwarmClaw stopped',
186
+ message: 'The SwarmClaw server exited unexpectedly.',
187
+ detail,
188
+ });
189
+ if (serverLogFile && res.response === 0)
190
+ electron_1.shell.showItemInFolder(serverLogFile);
191
+ electron_1.app.exit(1);
192
+ }
193
+ async function showStartupFailureDialog(err, paths) {
194
+ const message = err instanceof Error ? err.message : String(err);
195
+ const base = `${message}\n\nStandalone entry: ${paths.standaloneEntry}\nData dir: ${paths.dataDir}`;
196
+ const detail = buildLogDetail(base);
197
+ const buttons = serverLogFile ? ['Open Logs Folder', 'Quit'] : ['Quit'];
198
+ const quitButtonId = buttons.length - 1;
199
+ const res = await electron_1.dialog.showMessageBox({
200
+ type: 'error',
201
+ buttons,
202
+ defaultId: quitButtonId,
203
+ cancelId: quitButtonId,
204
+ title: 'SwarmClaw failed to start',
205
+ message: 'The embedded server did not start.',
206
+ detail,
207
+ });
208
+ if (serverLogFile && res.response === 0)
209
+ electron_1.shell.showItemInFolder(serverLogFile);
210
+ }
211
+ function buildLogDetail(base) {
212
+ if (!serverLogFile)
213
+ return base;
214
+ const tail = (0, server_lifecycle_1.tailLogFile)(serverLogFile, LOG_TAIL_BYTES).trim();
215
+ if (!tail)
216
+ return `${base}\n\nLog file: ${serverLogFile}\n(no output captured yet)`;
217
+ return `${base}\n\nLog tail (${serverLogFile}):\n${tail}`;
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "main": "electron-dist/main.js",
6
6
  "license": "MIT",
@@ -87,7 +87,7 @@
87
87
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/electron-after-pack.test.mjs scripts/ensure-sandbox-browser-image.test.mjs scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
88
88
  "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
89
89
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/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/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/gateways/gateway-topology.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/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openai.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/gateways/topology-route.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
90
- "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
90
+ "test:runtime": "tsx --test src/lib/a2a/agent-card.test.ts src/lib/strip-internal-metadata.test.ts src/lib/provider-sets.test.ts src/lib/providers/opencode-cli.test.ts src/lib/providers/cli-provider-metadata.test.ts src/lib/providers/cli-utils.test.ts src/lib/providers/generic-cli.test.ts src/lib/server/agents/delegation-advisory.test.ts src/lib/server/cli-provider-readiness.test.ts src/lib/server/provider-health.test.ts src/lib/server/mcp-gateway-runtime.test.ts src/lib/server/mcp-connection-pool.test.ts src/lib/server/knowledge-sources.test.ts src/lib/server/extension-managed-resources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/chat-turn-preparation.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/chat-execution/post-stream-finalization.test.ts src/lib/server/chat-execution/reasoning-tag-scrubber.test.ts src/lib/server/chats/clear-undo-snapshots.test.ts src/lib/server/connectors/email.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/runtime/queue-retry-policy.test.ts src/lib/server/runs/run-brief.test.ts src/lib/server/operations/operation-pulse.test.ts src/lib/server/artifacts/artifact-resolver.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/lib/server/missions/mission-templates.test.ts src/lib/server/sharing/share-link-repository.test.ts src/lib/server/sharing/share-resolver.test.ts src/lib/server/tasks/task-execution-workspace.test.ts src/lib/server/tasks/task-service.test.ts src/lib/server/session-tools/execute.test.ts src/lib/server/session-tools/manage-tasks.test.ts src/lib/app/view-constants.test.ts src/lib/quality/quality-summary.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/tasks/tasks-route.test.ts src/app/api/tasks/task-workspace-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/chats/clear-route.test.ts src/app/api/chats/compact-route.test.ts src/app/api/chats/context-status-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/extensions/managed-resources/route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/portability/export/route.test.ts src/app/api/providers/[id]/route.test.ts src/app/api/tts/route.test.ts",
91
91
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
92
92
  "test:e2e": "node --import tsx scripts/browser-e2e-smoke.ts",
93
93
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -0,0 +1,117 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import os from 'node:os'
6
+
7
+ import { getExtensionManager } from '@/lib/server/extensions'
8
+ import { loadAgents, loadSchedules, loadSettings, saveAgents, saveSchedules, saveSettings } from '@/lib/server/storage'
9
+ import { GET, POST } from './route'
10
+
11
+ const originalAgents = loadAgents()
12
+ const originalSchedules = loadSchedules()
13
+ const originalSettings = loadSettings()
14
+
15
+ let seq = 0
16
+
17
+ function extensionId(prefix: string): string {
18
+ seq += 1
19
+ return `${prefix}_${Date.now()}_${seq}`
20
+ }
21
+
22
+ afterEach(() => {
23
+ saveAgents(originalAgents)
24
+ saveSchedules(originalSchedules)
25
+ saveSettings(originalSettings)
26
+ })
27
+
28
+ test('managed resources route reconciles one extension', async () => {
29
+ const id = extensionId('route_managed_resources')
30
+ getExtensionManager().registerBuiltin(id, {
31
+ name: 'Route Managed Fixture',
32
+ managedResources: {
33
+ agents: [
34
+ {
35
+ agentKey: 'operator',
36
+ displayName: 'Route Operator',
37
+ provider: 'openai',
38
+ model: 'gpt-4o-mini',
39
+ },
40
+ ],
41
+ },
42
+ })
43
+
44
+ const before = await GET(new Request('http://local/api/extensions/managed-resources'))
45
+ assert.equal(before.status, 200)
46
+ const beforeBody = await before.json()
47
+ const extension = beforeBody.extensions.find((entry: { extensionId: string }) => entry.extensionId === id)
48
+ assert.equal(extension.agents[0].status, 'missing')
49
+
50
+ const response = await POST(new Request('http://local/api/extensions/managed-resources', {
51
+ method: 'POST',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify({ action: 'reconcile', extensionId: id }),
54
+ }))
55
+
56
+ assert.equal(response.status, 200)
57
+ const body = await response.json()
58
+ assert.equal(body.createdAgents.length, 1)
59
+ const agent = loadAgents()[body.createdAgents[0]]
60
+ assert.ok(agent)
61
+ assert.equal(agent.managedByExtension?.extensionId, id)
62
+ })
63
+
64
+ test('managed resources route configures and lists a local folder', async () => {
65
+ const id = extensionId('route_managed_folder')
66
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-route-folder-'))
67
+ fs.mkdirSync(path.join(tempDir, 'inputs'))
68
+ fs.writeFileSync(path.join(tempDir, 'inputs', 'task.txt'), 'task\n')
69
+
70
+ getExtensionManager().registerBuiltin(id, {
71
+ name: 'Route Folder Fixture',
72
+ managedResources: {
73
+ localFolders: [
74
+ {
75
+ folderKey: 'workspace',
76
+ displayName: 'Workspace Folder',
77
+ access: 'readWrite',
78
+ requiredDirectories: ['inputs'],
79
+ requiredFiles: ['inputs/task.txt'],
80
+ },
81
+ ],
82
+ },
83
+ })
84
+
85
+ const configure = await POST(new Request('http://local/api/extensions/managed-resources', {
86
+ method: 'POST',
87
+ headers: { 'content-type': 'application/json' },
88
+ body: JSON.stringify({
89
+ action: 'configure_local_folder',
90
+ extensionId: id,
91
+ folderKey: 'workspace',
92
+ path: tempDir,
93
+ }),
94
+ }))
95
+ assert.equal(configure.status, 200)
96
+ const configured = await configure.json()
97
+ assert.equal(configured.status.healthy, true)
98
+
99
+ const listing = await GET(new Request(`http://local/api/extensions/managed-resources?action=list_local_folder&extensionId=${encodeURIComponent(id)}&folderKey=workspace&recursive=true`))
100
+ assert.equal(listing.status, 200)
101
+ const listingBody = await listing.json()
102
+ assert.ok(listingBody.entries.some((entry: { path: string }) => entry.path === 'inputs/task.txt'))
103
+
104
+ const traversal = await POST(new Request('http://local/api/extensions/managed-resources', {
105
+ method: 'POST',
106
+ headers: { 'content-type': 'application/json' },
107
+ body: JSON.stringify({
108
+ action: 'list_local_folder',
109
+ extensionId: id,
110
+ folderKey: 'workspace',
111
+ relativePath: '../outside',
112
+ }),
113
+ }))
114
+ assert.equal(traversal.status, 400)
115
+
116
+ fs.rmSync(tempDir, { recursive: true, force: true })
117
+ })
@@ -0,0 +1,116 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
3
+ import {
4
+ inspectExtensionLocalFolder,
5
+ listExtensionLocalFolderEntries,
6
+ listExtensionManagedResources,
7
+ reconcileExtensionManagedResources,
8
+ setExtensionLocalFolderConfig,
9
+ } from '@/lib/server/extension-managed-resources'
10
+ import { errorMessage } from '@/lib/shared-utils'
11
+ import '@/lib/server/builtin-extensions'
12
+
13
+ export const dynamic = 'force-dynamic'
14
+
15
+ function badRequest(message: string) {
16
+ return NextResponse.json({ error: message }, { status: 400 })
17
+ }
18
+
19
+ function optionalText(value: unknown): string | null {
20
+ return typeof value === 'string' && value.trim() ? value.trim() : null
21
+ }
22
+
23
+ export async function GET(req: Request) {
24
+ const { searchParams } = new URL(req.url)
25
+ const action = searchParams.get('action')
26
+
27
+ try {
28
+ if (action === 'inspect_local_folder') {
29
+ const extensionId = optionalText(searchParams.get('extensionId'))
30
+ const folderKey = optionalText(searchParams.get('folderKey'))
31
+ if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
32
+ return NextResponse.json(await inspectExtensionLocalFolder({
33
+ extensionId,
34
+ folderKey,
35
+ }))
36
+ }
37
+
38
+ if (action === 'list_local_folder') {
39
+ const extensionId = optionalText(searchParams.get('extensionId'))
40
+ const folderKey = optionalText(searchParams.get('folderKey'))
41
+ if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
42
+ return NextResponse.json(await listExtensionLocalFolderEntries({
43
+ extensionId,
44
+ folderKey,
45
+ relativePath: optionalText(searchParams.get('relativePath')),
46
+ recursive: searchParams.get('recursive') === 'true',
47
+ maxEntries: Number(searchParams.get('maxEntries') || 1000),
48
+ }))
49
+ }
50
+
51
+ return NextResponse.json(listExtensionManagedResources())
52
+ } catch (err: unknown) {
53
+ return badRequest(errorMessage(err))
54
+ }
55
+ }
56
+
57
+ export async function POST(req: Request) {
58
+ const { data: body, error } = await safeParseBody(req)
59
+ if (error) return error
60
+ const input = body && typeof body === 'object' ? body as Record<string, unknown> : {}
61
+ const action = optionalText(input.action)
62
+
63
+ try {
64
+ if (action === 'reconcile') {
65
+ return NextResponse.json(reconcileExtensionManagedResources(optionalText(input.extensionId)))
66
+ }
67
+
68
+ if (action === 'configure_local_folder') {
69
+ const extensionId = optionalText(input.extensionId)
70
+ const folderKey = optionalText(input.folderKey)
71
+ const folderPath = optionalText(input.path)
72
+ if (!extensionId || !folderKey || !folderPath) {
73
+ return badRequest('extensionId, folderKey, and path required')
74
+ }
75
+ const access = input.access === 'read' || input.access === 'readWrite'
76
+ ? input.access
77
+ : undefined
78
+ const config = setExtensionLocalFolderConfig({
79
+ extensionId,
80
+ folderKey,
81
+ path: folderPath,
82
+ access,
83
+ })
84
+ const status = await inspectExtensionLocalFolder({ extensionId, folderKey })
85
+ return NextResponse.json({ ok: true, config, status })
86
+ }
87
+
88
+ if (action === 'inspect_local_folder') {
89
+ const extensionId = optionalText(input.extensionId)
90
+ const folderKey = optionalText(input.folderKey)
91
+ if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
92
+ return NextResponse.json(await inspectExtensionLocalFolder({
93
+ extensionId,
94
+ folderKey,
95
+ overridePath: optionalText(input.path),
96
+ }))
97
+ }
98
+
99
+ if (action === 'list_local_folder') {
100
+ const extensionId = optionalText(input.extensionId)
101
+ const folderKey = optionalText(input.folderKey)
102
+ if (!extensionId || !folderKey) return badRequest('extensionId and folderKey required')
103
+ return NextResponse.json(await listExtensionLocalFolderEntries({
104
+ extensionId,
105
+ folderKey,
106
+ relativePath: optionalText(input.relativePath),
107
+ recursive: input.recursive === true,
108
+ maxEntries: typeof input.maxEntries === 'number' ? input.maxEntries : undefined,
109
+ }))
110
+ }
111
+
112
+ return badRequest('action required')
113
+ } catch (err: unknown) {
114
+ return badRequest(errorMessage(err))
115
+ }
116
+ }
@@ -0,0 +1,16 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { getOpenClawGatewayEnvironmentStatus } from '@/lib/server/gateways/gateway-topology'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function GET(
9
+ _req: Request,
10
+ { params }: { params: Promise<{ id: string; environmentId: string }> },
11
+ ) {
12
+ const { id, environmentId } = await params
13
+ const snapshot = await getOpenClawGatewayEnvironmentStatus(id, decodeURIComponent(environmentId))
14
+ if (!snapshot) return notFound()
15
+ return NextResponse.json(snapshot, { status: snapshot.errors.length > 0 ? 502 : 200 })
16
+ }
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from 'next/server'
2
+
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { listOpenClawGatewayEnvironments } from '@/lib/server/gateways/gateway-topology'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const snapshot = await listOpenClawGatewayEnvironments(id)
11
+ if (!snapshot) return notFound()
12
+ return NextResponse.json(snapshot)
13
+ }
@@ -18,6 +18,36 @@ test('gateway topology route returns 404 for unknown profiles', () => {
18
18
  assert.equal(output.body.error, 'Not found')
19
19
  })
20
20
 
21
+ test('gateway environments route returns 404 for unknown profiles', () => {
22
+ const output = runWithTempDataDir<{ status: number; body: { error: string } }>(`
23
+ const routeMod = await import('./src/app/api/gateways/[id]/environments/route')
24
+ const route = routeMod.default || routeMod
25
+ const response = await route.GET(
26
+ new Request('http://local/api/gateways/missing/environments'),
27
+ { params: Promise.resolve({ id: 'missing' }) },
28
+ )
29
+ console.log(JSON.stringify({ status: response.status, body: await response.json() }))
30
+ `, { prefix: 'swarmclaw-gateway-environments-route-test-' })
31
+
32
+ assert.equal(output.status, 404)
33
+ assert.equal(output.body.error, 'Not found')
34
+ })
35
+
36
+ test('gateway environment status route returns 404 for unknown profiles', () => {
37
+ const output = runWithTempDataDir<{ status: number; body: { error: string } }>(`
38
+ const routeMod = await import('./src/app/api/gateways/[id]/environments/[environmentId]/route')
39
+ const route = routeMod.default || routeMod
40
+ const response = await route.GET(
41
+ new Request('http://local/api/gateways/missing/environments/gateway'),
42
+ { params: Promise.resolve({ id: 'missing', environmentId: 'gateway' }) },
43
+ )
44
+ console.log(JSON.stringify({ status: response.status, body: await response.json() }))
45
+ `, { prefix: 'swarmclaw-gateway-environment-status-route-test-' })
46
+
47
+ assert.equal(output.status, 404)
48
+ assert.equal(output.body.error, 'Not found')
49
+ })
50
+
21
51
  test('gateway fleet route reports empty totals when no OpenClaw profiles exist', () => {
22
52
  const output = runWithTempDataDir<{
23
53
  status: number
@@ -81,9 +81,13 @@ test('PUT /api/tasks/:id provisions an execution workspace and preview links', a
81
81
  assert.equal(response.status, 200)
82
82
  const body = await response.json() as BoardTask
83
83
  assert.equal(body.executionWorkspace?.sourceCwd, '/source/repo')
84
+ assert.equal(body.executionWorkspace?.context?.taskId, 'task-route-workspace')
85
+ assert.equal(body.executionWorkspace?.envHints?.some((hint) => hint.key === 'WORKSPACE_CWD'), true)
84
86
  assert.equal(body.previewLinks?.[0]?.url, 'http://127.0.0.1:3456')
85
87
  assert.equal(body.runtimeServices?.[0]?.name, 'Next dev')
86
88
  assert.equal(fs.existsSync(body.executionWorkspace?.path || ''), true)
89
+ assert.equal(fs.existsSync(body.executionWorkspace?.contextPath || ''), true)
90
+ assert.equal(fs.existsSync(body.executionWorkspace?.envPath || ''), true)
87
91
  })
88
92
 
89
93
  test('GET /api/tasks returns computed blocked liveness without persisting a task patch', async () => {
package/src/cli/index.js CHANGED
@@ -273,6 +273,8 @@ const COMMAND_GROUPS = [
273
273
  cmd('delete', 'DELETE', '/gateways/:id', 'Delete a gateway profile'),
274
274
  cmd('health', 'GET', '/gateways/:id/health', 'Run a gateway health check'),
275
275
  cmd('topology', 'GET', '/gateways/:id/topology', 'Refresh and return one gateway topology snapshot'),
276
+ cmd('environments', 'GET', '/gateways/:id/environments', 'List OpenClaw gateway execution environments'),
277
+ cmd('environment-status', 'GET', '/gateways/:id/environments/:environmentId', 'Get one OpenClaw gateway execution environment status'),
276
278
  cmd('fleet', 'GET', '/gateways/fleet', 'Refresh and return fleet-wide gateway topology'),
277
279
  ],
278
280
  },
@@ -526,6 +528,8 @@ const COMMAND_GROUPS = [
526
528
  cmd('settings-set', 'PUT', '/extensions/settings', 'Set extension settings (use --query extensionId=extension_name and --data JSON)', { expectsJsonBody: true }),
527
529
  cmd('ui', 'GET', '/extensions/ui', 'List extension UI modules (use --query type=sidebar|header|chat_actions|connectors)'),
528
530
  cmd('builtins', 'GET', '/extensions/builtins', 'List built-in extensions'),
531
+ cmd('managed-resources', 'GET', '/extensions/managed-resources', 'Preview extension-managed agents, routines, folders, gateways, and setup checks'),
532
+ cmd('managed-resources-action', 'POST', '/extensions/managed-resources', 'Reconcile or inspect extension-managed resources', { expectsJsonBody: true }),
529
533
  ],
530
534
  },
531
535
  {
package/src/cli/spec.js CHANGED
@@ -221,6 +221,8 @@ const COMMAND_GROUPS = {
221
221
  delete: { description: 'Delete a gateway profile', method: 'DELETE', path: '/gateways/:id', params: ['id'] },
222
222
  health: { description: 'Run a gateway health check', method: 'GET', path: '/gateways/:id/health', params: ['id'] },
223
223
  topology: { description: 'Refresh and return one gateway topology snapshot', method: 'GET', path: '/gateways/:id/topology', params: ['id'] },
224
+ environments: { description: 'List OpenClaw gateway execution environments', method: 'GET', path: '/gateways/:id/environments', params: ['id'] },
225
+ 'environment-status': { description: 'Get one OpenClaw gateway execution environment status', method: 'GET', path: '/gateways/:id/environments/:environmentId', params: ['id', 'environmentId'] },
224
226
  fleet: { description: 'Refresh and return fleet-wide gateway topology', method: 'GET', path: '/gateways/fleet' },
225
227
  },
226
228
  },
@@ -386,6 +388,8 @@ const COMMAND_GROUPS = {
386
388
  install: { description: 'Install extension by URL', method: 'POST', path: '/extensions/install' },
387
389
  'settings-get': { description: 'Read extension settings (supports --query extensionId=...)', method: 'GET', path: '/extensions/settings' },
388
390
  'settings-set': { description: 'Write extension settings (supports --query extensionId=... and --data JSON)', method: 'PUT', path: '/extensions/settings' },
391
+ 'managed-resources': { description: 'Preview extension-managed agents, routines, folders, gateways, and setup checks', method: 'GET', path: '/extensions/managed-resources' },
392
+ 'managed-resources-action': { description: 'Reconcile or inspect extension-managed resources', method: 'POST', path: '/extensions/managed-resources' },
389
393
  },
390
394
  },
391
395
  providers: {