@swarmclawai/swarmclaw 1.9.2 → 1.9.3

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 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,6 +399,16 @@ Operational docs: https://swarmclaw.ai/docs/observability
399
399
 
400
400
  ## Releases
401
401
 
402
+ ### v1.9.3 Highlights
403
+
404
+ Bundled extension-orchestration release: Paperclip-style managed plugin resources, Hermes-style gateway/setup declarations, and safer local folder access in one release cycle.
405
+
406
+ - **Managed extension resources.** Extensions can now declare provisionable agents, schedules/routines, local folders, gateway platforms, and setup checks through `managedResources` or Paperclip-compatible top-level aliases.
407
+ - **Deterministic reconciliation.** `/api/extensions/managed-resources` can preview and reconcile extension-owned agents and routines with stable IDs and `managedByExtension` markers.
408
+ - **Trusted local folders.** Extension-declared local folders support root-bounded inspection and recursive listing with traversal and symlink-escape protection.
409
+ - **Operator UI.** The Extensions screen now shows managed-resource badges and a Managed tab with totals plus per-extension reconcile controls.
410
+ - **Extension authoring spec.** `extension_creator` now documents managed resources, gateway declarations, setup checks, and Paperclip-compatible manifest aliases.
411
+
402
412
  ### v1.9.2 Highlights
403
413
 
404
414
  Bundled competitor-parity release: Hermes-style reasoning hygiene, deterministic delegation routing, Mission Control task workflow polish, OpenClaw export hardening, and Paperclip-style timeout hygiene.
@@ -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.3",
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
+ }
package/src/cli/index.js CHANGED
@@ -526,6 +526,8 @@ const COMMAND_GROUPS = [
526
526
  cmd('settings-set', 'PUT', '/extensions/settings', 'Set extension settings (use --query extensionId=extension_name and --data JSON)', { expectsJsonBody: true }),
527
527
  cmd('ui', 'GET', '/extensions/ui', 'List extension UI modules (use --query type=sidebar|header|chat_actions|connectors)'),
528
528
  cmd('builtins', 'GET', '/extensions/builtins', 'List built-in extensions'),
529
+ cmd('managed-resources', 'GET', '/extensions/managed-resources', 'Preview extension-managed agents, routines, folders, gateways, and setup checks'),
530
+ cmd('managed-resources-action', 'POST', '/extensions/managed-resources', 'Reconcile or inspect extension-managed resources', { expectsJsonBody: true }),
529
531
  ],
530
532
  },
531
533
  {
package/src/cli/spec.js CHANGED
@@ -386,6 +386,8 @@ const COMMAND_GROUPS = {
386
386
  install: { description: 'Install extension by URL', method: 'POST', path: '/extensions/install' },
387
387
  'settings-get': { description: 'Read extension settings (supports --query extensionId=...)', method: 'GET', path: '/extensions/settings' },
388
388
  'settings-set': { description: 'Write extension settings (supports --query extensionId=... and --data JSON)', method: 'PUT', path: '/extensions/settings' },
389
+ 'managed-resources': { description: 'Preview extension-managed agents, routines, folders, gateways, and setup checks', method: 'GET', path: '/extensions/managed-resources' },
390
+ 'managed-resources-action': { description: 'Reconcile or inspect extension-managed resources', method: 'POST', path: '/extensions/managed-resources' },
389
391
  },
390
392
  },
391
393
  providers: {