@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.
@@ -11,6 +11,7 @@ import type {
11
11
  ExtensionUIDefinition,
12
12
  ExtensionProviderDefinition,
13
13
  ExtensionConnectorDefinition,
14
+ ExtensionManagedResources,
14
15
  Session,
15
16
  ExtensionPackageManager,
16
17
  ExtensionDependencyInstallStatus,
@@ -467,11 +468,60 @@ function coerceTools(rawTools: unknown): ExtensionToolDef[] {
467
468
  return []
468
469
  }
469
470
 
471
+ function coerceManagedResources(raw: Record<string, unknown>): ExtensionManagedResources | undefined {
472
+ const explicit = isRecord(raw.managedResources)
473
+ ? raw.managedResources as Record<string, unknown>
474
+ : {}
475
+ const agents = Array.isArray(explicit.agents)
476
+ ? explicit.agents
477
+ : Array.isArray(raw.agents)
478
+ ? raw.agents
479
+ : undefined
480
+ const schedules = Array.isArray(explicit.schedules)
481
+ ? explicit.schedules
482
+ : Array.isArray(raw.schedules)
483
+ ? raw.schedules
484
+ : undefined
485
+ const routines = Array.isArray(explicit.routines)
486
+ ? explicit.routines
487
+ : Array.isArray(raw.routines)
488
+ ? raw.routines
489
+ : undefined
490
+ const localFolders = Array.isArray(explicit.localFolders)
491
+ ? explicit.localFolders
492
+ : Array.isArray(raw.localFolders)
493
+ ? raw.localFolders
494
+ : undefined
495
+ const gatewayPlatforms = Array.isArray(explicit.gatewayPlatforms)
496
+ ? explicit.gatewayPlatforms
497
+ : Array.isArray(raw.gatewayPlatforms)
498
+ ? raw.gatewayPlatforms
499
+ : undefined
500
+ const setupChecks = Array.isArray(explicit.setupChecks)
501
+ ? explicit.setupChecks
502
+ : Array.isArray(raw.setupChecks)
503
+ ? raw.setupChecks
504
+ : undefined
505
+
506
+ const managedResources: ExtensionManagedResources = {
507
+ agents: agents as ExtensionManagedResources['agents'],
508
+ schedules: schedules as ExtensionManagedResources['schedules'],
509
+ routines: routines as ExtensionManagedResources['routines'],
510
+ localFolders: localFolders as ExtensionManagedResources['localFolders'],
511
+ gatewayPlatforms: gatewayPlatforms as ExtensionManagedResources['gatewayPlatforms'],
512
+ setupChecks: setupChecks as ExtensionManagedResources['setupChecks'],
513
+ }
514
+
515
+ return Object.values(managedResources).some((value) => Array.isArray(value) && value.length > 0)
516
+ ? managedResources
517
+ : undefined
518
+ }
519
+
470
520
  function normalizeExtension(mod: unknown): Extension | null {
471
521
  const modObj = mod as Record<string, unknown>
472
522
  const raw: Record<string, unknown> = (modObj?.default as Record<string, unknown>) || modObj
473
523
 
474
- if (raw.name && (raw.hooks || raw.tools || raw.ui || raw.providers || raw.connectors)) {
524
+ if (raw.name && (raw.hooks || raw.tools || raw.ui || raw.providers || raw.connectors || raw.managedResources || raw.agents || raw.schedules || raw.routines || raw.localFolders || raw.gatewayPlatforms || raw.setupChecks)) {
475
525
  const hooks = isRecord(raw.hooks) ? (raw.hooks as ExtensionHooks) : {}
476
526
  return {
477
527
  name: raw.name as string,
@@ -484,6 +534,7 @@ function normalizeExtension(mod: unknown): Extension | null {
484
534
  ui: isRecord(raw.ui) ? (raw.ui as ExtensionUIDefinition) : undefined,
485
535
  providers: Array.isArray(raw.providers) ? (raw.providers as ExtensionProviderDefinition[]) : undefined,
486
536
  connectors: Array.isArray(raw.connectors) ? (raw.connectors as ExtensionConnectorDefinition[]) : undefined,
537
+ managedResources: coerceManagedResources(raw),
487
538
  } as Extension
488
539
  }
489
540
 
@@ -639,6 +690,7 @@ interface LoadedExtension {
639
690
  ui?: ExtensionUIDefinition
640
691
  providers?: ExtensionProviderDefinition[]
641
692
  connectors?: ExtensionConnectorDefinition[]
693
+ managedResources?: ExtensionManagedResources
642
694
  isBuiltin?: boolean
643
695
  }
644
696
 
@@ -1017,6 +1069,7 @@ class ExtensionManager {
1017
1069
  ui: p.ui,
1018
1070
  providers: p.providers,
1019
1071
  connectors: p.connectors,
1072
+ managedResources: p.managedResources || coerceManagedResources(p as unknown as Record<string, unknown>),
1020
1073
  isBuiltin: true
1021
1074
  })
1022
1075
  this.markExtensionSuccess(id)
@@ -1064,6 +1117,7 @@ class ExtensionManager {
1064
1117
  ui: ext.ui,
1065
1118
  providers: ext.providers,
1066
1119
  connectors: ext.connectors,
1120
+ managedResources: ext.managedResources,
1067
1121
  })
1068
1122
  this.markExtensionSuccess(file)
1069
1123
  } catch (err: unknown) {
@@ -1145,6 +1199,55 @@ class ExtensionManager {
1145
1199
  return allUI
1146
1200
  }
1147
1201
 
1202
+ getManagedResourceExtensions(): Array<{
1203
+ extensionId: string
1204
+ extensionName: string
1205
+ enabled: boolean
1206
+ isBuiltin: boolean
1207
+ source?: ExtensionMeta['source']
1208
+ managedResources: ExtensionManagedResources
1209
+ }> {
1210
+ this.load()
1211
+ const result: Array<{
1212
+ extensionId: string
1213
+ extensionName: string
1214
+ enabled: boolean
1215
+ isBuiltin: boolean
1216
+ source?: ExtensionMeta['source']
1217
+ managedResources: ExtensionManagedResources
1218
+ }> = []
1219
+
1220
+ for (const [id, entry] of this.extensions.entries()) {
1221
+ const managedResources = entry.managedResources
1222
+ if (!managedResources) continue
1223
+ if (!Object.values(managedResources).some((value) => Array.isArray(value) && value.length > 0)) continue
1224
+ result.push({
1225
+ extensionId: id,
1226
+ extensionName: entry.meta.name,
1227
+ enabled: entry.meta.enabled,
1228
+ isBuiltin: entry.isBuiltin === true,
1229
+ source: entry.meta.source,
1230
+ managedResources,
1231
+ })
1232
+ }
1233
+
1234
+ return result
1235
+ }
1236
+
1237
+ getManagedResources(extensionId: string): ExtensionManagedResources | null {
1238
+ this.load()
1239
+ const candidateIds = expandExtensionIds([extensionId])
1240
+ for (const id of candidateIds) {
1241
+ const loaded = this.extensions.get(id)
1242
+ if (loaded?.managedResources) return loaded.managedResources
1243
+ const builtin = this.builtins.get(id)
1244
+ if (builtin) {
1245
+ return builtin.managedResources || coerceManagedResources(builtin as unknown as Record<string, unknown>) || null
1246
+ }
1247
+ }
1248
+ return null
1249
+ }
1250
+
1148
1251
  listExtensionIds(): string[] {
1149
1252
  this.load()
1150
1253
  return Array.from(this.extensions.keys())
@@ -1842,11 +1945,14 @@ class ExtensionManager {
1842
1945
  const failures = this.readFailureState()
1843
1946
  const metas: ExtensionMeta[] = []
1844
1947
 
1845
- const describeCapabilities = (loaded?: LoadedExtension, fallback?: Extension): Pick<ExtensionMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount' | 'settingsFields'> => {
1948
+ const describeCapabilities = (loaded?: LoadedExtension, fallback?: Extension): Pick<ExtensionMeta, 'toolCount' | 'hookCount' | 'hasUI' | 'providerCount' | 'connectorCount' | 'settingsFields' | 'managedAgentCount' | 'managedScheduleCount' | 'localFolderCount' | 'gatewayPlatformCount' | 'setupCheckCount'> => {
1846
1949
  const tools = loaded?.tools || fallback?.tools || []
1847
1950
  const hooks = loaded?.hooks || fallback?.hooks || {}
1848
1951
  const providers = loaded?.providers || fallback?.providers || []
1849
1952
  const connectors = loaded?.connectors || fallback?.connectors || []
1953
+ const managedResources = loaded?.managedResources
1954
+ || fallback?.managedResources
1955
+ || (fallback ? coerceManagedResources(fallback as unknown as Record<string, unknown>) : undefined)
1850
1956
  const hasUi = !!(loaded?.ui || fallback?.ui)
1851
1957
  const settingsFields = loaded?.ui?.settingsFields || fallback?.ui?.settingsFields
1852
1958
  return {
@@ -1855,6 +1961,11 @@ class ExtensionManager {
1855
1961
  hasUI: hasUi,
1856
1962
  providerCount: Array.isArray(providers) ? providers.length : 0,
1857
1963
  connectorCount: Array.isArray(connectors) ? connectors.length : 0,
1964
+ managedAgentCount: managedResources?.agents?.length || 0,
1965
+ managedScheduleCount: (managedResources?.schedules?.length || 0) + (managedResources?.routines?.length || 0),
1966
+ localFolderCount: managedResources?.localFolders?.length || 0,
1967
+ gatewayPlatformCount: managedResources?.gatewayPlatforms?.length || 0,
1968
+ setupCheckCount: managedResources?.setupChecks?.length || 0,
1858
1969
  settingsFields: settingsFields?.length ? settingsFields : undefined,
1859
1970
  }
1860
1971
  }
@@ -138,6 +138,54 @@ module.exports = {
138
138
  }
139
139
  ],
140
140
 
141
+ // --- Managed Resources (Paperclip-compatible) ---
142
+ managedResources: {
143
+ agents: [
144
+ {
145
+ agentKey: 'researcher',
146
+ displayName: 'Managed Researcher',
147
+ description: 'Reusable agent this extension can provision.',
148
+ systemPrompt: 'Research carefully and cite durable evidence.',
149
+ provider: 'openai',
150
+ model: 'gpt-4o-mini',
151
+ capabilities: ['research', 'analysis'],
152
+ extensions: ['web', 'memory']
153
+ }
154
+ ],
155
+ schedules: [
156
+ {
157
+ scheduleKey: 'daily-digest',
158
+ displayName: 'Daily Digest',
159
+ agentRef: { resourceKind: 'agent', resourceKey: 'researcher' },
160
+ taskPrompt: 'Prepare the daily digest.',
161
+ scheduleType: 'cron',
162
+ cron: '0 9 * * *',
163
+ timezone: 'UTC',
164
+ status: 'paused'
165
+ }
166
+ ],
167
+ localFolders: [
168
+ {
169
+ folderKey: 'workspace',
170
+ displayName: 'Workspace Folder',
171
+ access: 'readWrite',
172
+ requiredDirectories: ['inputs', 'outputs']
173
+ }
174
+ ],
175
+ gatewayPlatforms: [
176
+ {
177
+ platformKey: 'openai-compatible-api',
178
+ displayName: 'OpenAI-compatible API',
179
+ transport: 'http',
180
+ endpoint: 'http://127.0.0.1:8642/v1',
181
+ authMode: 'bearer'
182
+ }
183
+ ],
184
+ setupChecks: [
185
+ { checkKey: 'api-key', displayName: 'API key configured', kind: 'env', target: 'OPENAI_API_KEY', required: true }
186
+ ]
187
+ },
188
+
141
189
  // --- Real OpenClaw Format (register API) ---
142
190
  register(api) {
143
191
  api.registerHook('agent:start', (ctx) => {
@@ -162,6 +210,8 @@ Key rules:
162
210
  - If your extension needs npm/pnpm/yarn/bun packages, include a packageJson object during scaffold or call install_dependencies later.
163
211
  - Dependency installs are run by the extension manager inside a per-extension workspace using the selected package manager with scripts disabled.
164
212
  - Extension settings are declared through ui.settingsFields and stored per extension ID
213
+ - Managed resources let an extension declare provisionable agents, schedules/routines, trusted local folders, gateway platforms, and setup checks. Operators reconcile them through Extensions > Managed Resources or /api/extensions/managed-resources.
214
+ - Paperclip-compatible top-level agents, routines, and localFolders are also accepted; SwarmClaw reconciles routines as schedules when they include schedule timing.
165
215
  - Keep extensions focused: one clear purpose per extension
166
216
  `
167
217
  }
@@ -2,6 +2,7 @@ import type { ProviderId, ProviderType, OllamaMode } from './provider'
2
2
  import type { SessionResetMode, IdentityContinuityState } from './session'
3
3
  import type { SkillAllowlistMode } from './skill'
4
4
  import type { DreamConfig } from './dream'
5
+ import type { ExtensionManagedResourceMarker } from './extension'
5
6
 
6
7
  // --- SwarmFeed Heartbeat ---
7
8
 
@@ -237,6 +238,7 @@ export interface Agent {
237
238
  swarmfeedLastAutoPostAt?: number | null
238
239
  origin?: 'swarmdock' | 'swarmfeed' | 'swarmclaw' | 'external'
239
240
  swarmfeedHeartbeat?: SwarmFeedHeartbeatConfig | null
241
+ managedByExtension?: ExtensionManagedResourceMarker | null
240
242
 
241
243
  // SwarmDock (marketplace integration)
242
244
  swarmdockEnabled?: boolean
@@ -1,4 +1,5 @@
1
1
  import type { SessionResetMode } from './session'
2
+ import type { ExtensionManagedLocalFolderDeclaration } from './extension'
2
3
 
3
4
  // --- App Settings ---
4
5
  export type LoopMode = 'bounded' | 'ongoing'
@@ -150,6 +151,13 @@ export interface AppSettings {
150
151
  toolLoopCircuitBreaker?: number
151
152
  // Per-extension settings (keyed by extensionId)
152
153
  extensionSettings?: Record<string, Record<string, unknown>>
154
+ // Extension-managed resource settings such as trusted local folder roots.
155
+ extensionManagedResources?: Record<string, {
156
+ localFolders?: Record<string, ExtensionManagedLocalFolderDeclaration & {
157
+ path?: string | null
158
+ updatedAt?: number
159
+ }>
160
+ }>
153
161
  // Approval policies — opt-in governance gates for sensitive operations
154
162
  approvalPolicies?: {
155
163
  requireApprovalForAgentCreate?: boolean
@@ -251,6 +251,125 @@ export interface ExtensionUIDefinition {
251
251
  }>
252
252
  }
253
253
 
254
+ export type ExtensionManagedResourceKind = 'agent' | 'schedule' | 'local_folder'
255
+
256
+ export interface ExtensionManagedResourceRef {
257
+ extensionId?: string
258
+ resourceKind: 'agent' | 'schedule'
259
+ resourceKey: string
260
+ }
261
+
262
+ export interface ExtensionManagedResourceMarker {
263
+ extensionId: string
264
+ extensionName?: string | null
265
+ resourceKind: ExtensionManagedResourceKind
266
+ resourceKey: string
267
+ declarationHash?: string | null
268
+ reconciledAt: number
269
+ }
270
+
271
+ export interface ExtensionManagedAgentDeclaration {
272
+ agentKey: string
273
+ displayName: string
274
+ description?: string | null
275
+ systemPrompt?: string | null
276
+ instructions?: {
277
+ content?: string | null
278
+ entryFile?: string | null
279
+ assetPath?: string | null
280
+ } | null
281
+ provider?: ProviderId | string | null
282
+ model?: string | null
283
+ apiEndpoint?: string | null
284
+ credentialId?: string | null
285
+ fallbackCredentialIds?: string[]
286
+ gatewayProfileId?: string | null
287
+ preferredGatewayTags?: string[]
288
+ preferredGatewayUseCase?: string | null
289
+ capabilities?: string[] | string | null
290
+ tools?: string[]
291
+ extensions?: string[]
292
+ skills?: string[]
293
+ skillIds?: string[]
294
+ mcpServerIds?: string[]
295
+ monthlyBudget?: number | null
296
+ dailyBudget?: number | null
297
+ hourlyBudget?: number | null
298
+ disabled?: boolean
299
+ heartbeatEnabled?: boolean
300
+ planningMode?: 'off' | 'strict' | null
301
+ }
302
+
303
+ export interface ExtensionManagedScheduleTrigger {
304
+ kind?: 'schedule' | 'api' | 'webhook'
305
+ label?: string | null
306
+ enabled?: boolean
307
+ cronExpression?: string | null
308
+ timezone?: string | null
309
+ }
310
+
311
+ export interface ExtensionManagedScheduleDeclaration {
312
+ scheduleKey?: string
313
+ routineKey?: string
314
+ displayName?: string
315
+ title?: string
316
+ description?: string | null
317
+ taskPrompt?: string | null
318
+ message?: string | null
319
+ taskMode?: 'task' | 'wake_only' | 'protocol'
320
+ agentId?: string | null
321
+ agentRef?: ExtensionManagedResourceRef | null
322
+ assigneeRef?: ExtensionManagedResourceRef | null
323
+ scheduleType?: 'cron' | 'interval' | 'once'
324
+ cron?: string | null
325
+ intervalMs?: number | null
326
+ runAt?: number | null
327
+ timezone?: string | null
328
+ status?: 'active' | 'paused' | 'completed' | 'failed' | 'archived'
329
+ priority?: string | null
330
+ triggers?: ExtensionManagedScheduleTrigger[]
331
+ }
332
+
333
+ export interface ExtensionManagedLocalFolderDeclaration {
334
+ folderKey: string
335
+ displayName: string
336
+ description?: string | null
337
+ access?: 'read' | 'readWrite'
338
+ requiredDirectories?: string[]
339
+ requiredFiles?: string[]
340
+ }
341
+
342
+ export interface ExtensionGatewayPlatformDeclaration {
343
+ platformKey: string
344
+ displayName: string
345
+ description?: string | null
346
+ transport?: 'http' | 'ws' | 'stdio' | 'cli' | 'gateway' | 'custom'
347
+ endpoint?: string | null
348
+ authMode?: 'none' | 'bearer' | 'api_key' | 'oauth' | 'custom'
349
+ setupCheckKey?: string | null
350
+ capabilities?: string[]
351
+ }
352
+
353
+ export interface ExtensionSetupCheckDeclaration {
354
+ checkKey: string
355
+ displayName: string
356
+ description?: string | null
357
+ kind: 'env' | 'command' | 'url' | 'manual'
358
+ target?: string | null
359
+ required?: boolean
360
+ }
361
+
362
+ export interface ExtensionManagedResources {
363
+ agents?: ExtensionManagedAgentDeclaration[]
364
+ schedules?: ExtensionManagedScheduleDeclaration[]
365
+ /** Paperclip-compatible alias. SwarmClaw reconciles routines as managed schedules. */
366
+ routines?: ExtensionManagedScheduleDeclaration[]
367
+ localFolders?: ExtensionManagedLocalFolderDeclaration[]
368
+ /** Hermes-style gateway/platform declaration metadata for setup and diagnostics surfaces. */
369
+ gatewayPlatforms?: ExtensionGatewayPlatformDeclaration[]
370
+ setupChecks?: ExtensionSetupCheckDeclaration[]
371
+ }
372
+
254
373
  export interface ExtensionProviderDefinition {
255
374
  id: string
256
375
  name: string
@@ -300,6 +419,14 @@ export interface Extension {
300
419
  ui?: ExtensionUIDefinition
301
420
  providers?: ExtensionProviderDefinition[]
302
421
  connectors?: ExtensionConnectorDefinition[]
422
+ managedResources?: ExtensionManagedResources
423
+ /** Paperclip-compatible top-level aliases. Prefer managedResources for new SwarmClaw extensions. */
424
+ agents?: ExtensionManagedAgentDeclaration[]
425
+ schedules?: ExtensionManagedScheduleDeclaration[]
426
+ routines?: ExtensionManagedScheduleDeclaration[]
427
+ localFolders?: ExtensionManagedLocalFolderDeclaration[]
428
+ gatewayPlatforms?: ExtensionGatewayPlatformDeclaration[]
429
+ setupChecks?: ExtensionSetupCheckDeclaration[]
303
430
  }
304
431
 
305
432
  export interface ExtensionMeta {
@@ -325,6 +452,11 @@ export interface ExtensionMeta {
325
452
  hasUI?: boolean
326
453
  providerCount?: number
327
454
  connectorCount?: number
455
+ managedAgentCount?: number
456
+ managedScheduleCount?: number
457
+ localFolderCount?: number
458
+ gatewayPlatformCount?: number
459
+ setupCheckCount?: number
328
460
  createdByAgentId?: string | null
329
461
  settingsFields?: ExtensionSettingsField[]
330
462
  hasDependencyManifest?: boolean
@@ -1,3 +1,5 @@
1
+ import type { ExtensionManagedResourceMarker } from './extension'
2
+
1
3
  export type ScheduleType = 'cron' | 'interval' | 'once'
2
4
  export type ScheduleStatus = 'active' | 'paused' | 'completed' | 'failed' | 'archived'
3
5
  export type ScheduleTaskMode = 'task' | 'wake_only' | 'protocol'
@@ -54,6 +56,7 @@ export interface Schedule {
54
56
  followupThreadId?: string | null
55
57
  followupSenderId?: string | null
56
58
  followupSenderName?: string | null
59
+ managedByExtension?: ExtensionManagedResourceMarker | null
57
60
  createdAt: number
58
61
  updatedAt?: number
59
62
  }
@@ -8,14 +8,43 @@ import { toast } from 'sonner'
8
8
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
9
9
  import { errorMessage } from '@/lib/shared-utils'
10
10
 
11
+ type ManagedResourceStatus = 'declared' | 'resolved' | 'missing' | 'missing_ref' | 'unsupported_trigger'
12
+ type ManagedResourceSummary = {
13
+ extensions: Array<{
14
+ extensionId: string
15
+ extensionName: string
16
+ enabled: boolean
17
+ isBuiltin: boolean
18
+ agents: Array<{ resourceKey: string; displayName: string; status: ManagedResourceStatus; resourceId: string | null }>
19
+ schedules: Array<{ resourceKey: string; displayName: string; status: ManagedResourceStatus; resourceId: string | null }>
20
+ localFolders: Array<{ resourceKey: string; displayName: string; status: ManagedResourceStatus; configured?: boolean; healthy?: boolean }>
21
+ gatewayPlatforms: Array<{ platformKey: string; displayName: string; transport?: string; endpoint?: string | null }>
22
+ setupChecks: Array<{ checkKey: string; displayName: string; kind: string; required: boolean }>
23
+ }>
24
+ totals: {
25
+ extensions: number
26
+ agents: number
27
+ schedules: number
28
+ localFolders: number
29
+ gatewayPlatforms: number
30
+ setupChecks: number
31
+ resolvedAgents: number
32
+ resolvedSchedules: number
33
+ healthyLocalFolders: number
34
+ }
35
+ }
36
+
11
37
  export function ExtensionManager() {
12
- const [tab, setTab] = useState<'installed' | 'marketplace' | 'url'>('installed')
38
+ const [tab, setTab] = useState<'installed' | 'managed' | 'marketplace' | 'url'>('installed')
13
39
  const [extensions, setExtensions] = useState<ExtensionMeta[]>([])
40
+ const [managedResources, setManagedResources] = useState<ManagedResourceSummary | null>(null)
14
41
  const [marketplace, setMarketplace] = useState<MarketplaceExtension[]>([])
15
42
  const [loading, setLoading] = useState(false)
43
+ const [managedLoading, setManagedLoading] = useState(false)
16
44
  const [installing, setInstalling] = useState<string | null>(null)
17
45
  const [updating, setUpdating] = useState<string | null>(null)
18
46
  const [updatingAll, setUpdatingAll] = useState(false)
47
+ const [reconciling, setReconciling] = useState<string | null>(null)
19
48
  const [urlInput, setUrlInput] = useState('')
20
49
  const [urlFilename, setUrlFilename] = useState('')
21
50
  const [urlStatus, setUrlStatus] = useState<{ ok: boolean; message: string } | null>(null)
@@ -29,6 +58,15 @@ export function ExtensionManager() {
29
58
  } catch { /* ignore */ }
30
59
  }, [])
31
60
 
61
+ const loadManagedResources = useCallback(async () => {
62
+ setManagedLoading(true)
63
+ try {
64
+ const data = await api<ManagedResourceSummary>('GET', '/extensions/managed-resources')
65
+ setManagedResources(data)
66
+ } catch { /* ignore */ }
67
+ setManagedLoading(false)
68
+ }, [])
69
+
32
70
  const loadMarketplace = useCallback(async (q = '') => {
33
71
  setLoading(true)
34
72
  try {
@@ -39,6 +77,7 @@ export function ExtensionManager() {
39
77
  }, [])
40
78
 
41
79
  useEffect(() => { loadExtensions() }, [loadExtensions])
80
+ useEffect(() => { if (tab === 'managed') loadManagedResources() }, [tab, loadManagedResources])
42
81
  useEffect(() => { if (tab === 'marketplace') loadMarketplace(marketplaceQuery) }, [tab, loadMarketplace, marketplaceQuery])
43
82
 
44
83
  const toggleExtension = async (filename: string, enabled: boolean) => {
@@ -87,6 +126,25 @@ export function ExtensionManager() {
87
126
  }
88
127
  }
89
128
 
129
+ const handleReconcileManaged = async (extensionId?: string) => {
130
+ const id = extensionId || 'all'
131
+ setReconciling(id)
132
+ try {
133
+ const result = await api<{ createdAgents: string[]; updatedAgents: string[]; createdSchedules: string[]; updatedSchedules: string[] }>(
134
+ 'POST',
135
+ '/extensions/managed-resources',
136
+ { action: 'reconcile', ...(extensionId ? { extensionId } : {}) },
137
+ { timeoutMs: 30_000 },
138
+ )
139
+ await Promise.all([loadExtensions(), loadManagedResources()])
140
+ toast.success(`Reconciled ${result.createdAgents.length + result.updatedAgents.length} agents and ${result.createdSchedules.length + result.updatedSchedules.length} schedules`)
141
+ } catch (err: unknown) {
142
+ toast.error(errorMessage(err))
143
+ } finally {
144
+ setReconciling(null)
145
+ }
146
+ }
147
+
90
148
  const installFromMarketplace = async (p: MarketplaceExtension) => {
91
149
  setInstalling(p.id)
92
150
  const toastId = toast.loading(`Installing ${p.name}...`)
@@ -188,6 +246,14 @@ export function ExtensionManager() {
188
246
  </span>
189
247
  </div>
190
248
  )}
249
+ {((p.managedAgentCount || 0) + (p.managedScheduleCount || 0) + (p.localFolderCount || 0) + (p.gatewayPlatformCount || 0) + (p.setupCheckCount || 0)) > 0 && (
250
+ <div className="mt-1.5 flex items-center gap-1.5 text-[10px] font-700 uppercase tracking-[0.08em] flex-wrap">
251
+ {(p.managedAgentCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-300">{p.managedAgentCount} agents</span>}
252
+ {(p.managedScheduleCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-300">{p.managedScheduleCount} routines</span>}
253
+ {(p.localFolderCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-sky-500/10 text-sky-300">{p.localFolderCount} folders</span>}
254
+ {(p.gatewayPlatformCount || 0) > 0 && <span className="px-1.5 py-0.5 rounded bg-amber-500/10 text-amber-300">{p.gatewayPlatformCount} gateways</span>}
255
+ </div>
256
+ )}
191
257
  {p.autoDisabled && (
192
258
  <div className="text-[11px] text-amber-400/90 mt-1.5 p-2 rounded-[8px] bg-amber-500/[0.03] border border-amber-500/10">
193
259
  {p.lastFailureStage ? `Error at ${p.lastFailureStage}:` : 'Last error:'} {p.lastFailureError}
@@ -242,6 +308,7 @@ export function ExtensionManager() {
242
308
  </div>
243
309
  <div className="flex bg-surface p-1.5 rounded-[14px] border border-white/[0.04]">
244
310
  <button onClick={() => setTab('installed')} className={tabClass('installed')}>Installed</button>
311
+ <button onClick={() => setTab('managed')} className={tabClass('managed')}>Managed</button>
245
312
  <button onClick={() => setTab('marketplace')} className={tabClass('marketplace')}>Marketplace</button>
246
313
  <button onClick={() => setTab('url')} className={tabClass('url')}>Manual</button>
247
314
  </div>
@@ -302,6 +369,95 @@ export function ExtensionManager() {
302
369
  </div>
303
370
  )}
304
371
 
372
+ {tab === 'managed' && (
373
+ <div className="space-y-6">
374
+ <div className="flex items-center justify-between gap-4 px-1">
375
+ <div>
376
+ <h3 className="text-[13px] font-700 text-text-2">Managed Resources</h3>
377
+ <p className="text-[12px] text-text-3/50 mt-0.5">Extension-declared agents, routines, folders, gateways, and setup checks.</p>
378
+ </div>
379
+ <button
380
+ onClick={() => handleReconcileManaged()}
381
+ disabled={!!reconciling || managedLoading}
382
+ className="h-9 px-4 rounded-[10px] bg-accent-bright text-white text-[11px] font-800 uppercase tracking-[0.08em] border-none cursor-pointer disabled:opacity-50"
383
+ >
384
+ {reconciling === 'all' ? 'Reconciling...' : 'Reconcile All'}
385
+ </button>
386
+ </div>
387
+
388
+ {managedLoading ? (
389
+ <div className="py-20 flex justify-center">
390
+ <div className="w-8 h-8 border-2 border-accent-bright/20 border-t-accent-bright rounded-full animate-spin" />
391
+ </div>
392
+ ) : !managedResources || managedResources.extensions.length === 0 ? (
393
+ <div className="py-20 text-center rounded-[24px] border border-dashed border-white/[0.06]">
394
+ <p className="text-[14px] text-text-3/50">No managed resource declarations found</p>
395
+ </div>
396
+ ) : (
397
+ <div className="space-y-4">
398
+ <div className="grid grid-cols-2 md:grid-cols-5 gap-3">
399
+ {[
400
+ ['Agents', `${managedResources.totals.resolvedAgents}/${managedResources.totals.agents}`],
401
+ ['Routines', `${managedResources.totals.resolvedSchedules}/${managedResources.totals.schedules}`],
402
+ ['Folders', `${managedResources.totals.healthyLocalFolders}/${managedResources.totals.localFolders}`],
403
+ ['Gateways', String(managedResources.totals.gatewayPlatforms)],
404
+ ['Setup', String(managedResources.totals.setupChecks)],
405
+ ].map(([label, value]) => (
406
+ <div key={label} className="rounded-[12px] bg-surface border border-white/[0.06] p-4">
407
+ <div className="text-[10px] font-800 uppercase tracking-[0.12em] text-text-3/50">{label}</div>
408
+ <div className="text-[22px] font-800 text-text mt-1">{value}</div>
409
+ </div>
410
+ ))}
411
+ </div>
412
+
413
+ {managedResources.extensions.map((entry) => (
414
+ <div key={entry.extensionId} className="rounded-[18px] bg-surface border border-white/[0.06] p-5">
415
+ <div className="flex items-center justify-between gap-4 mb-4">
416
+ <div className="min-w-0">
417
+ <div className="flex items-center gap-2">
418
+ <h4 className="text-[14px] font-800 text-text truncate">{entry.extensionName}</h4>
419
+ <span className="text-[10px] font-mono text-text-3/50 truncate">{entry.extensionId}</span>
420
+ </div>
421
+ <div className="text-[11px] text-text-3/50 mt-0.5">
422
+ {entry.agents.length} agents, {entry.schedules.length} routines, {entry.localFolders.length} folders, {entry.gatewayPlatforms.length} gateways
423
+ </div>
424
+ </div>
425
+ <button
426
+ onClick={() => handleReconcileManaged(entry.extensionId)}
427
+ disabled={!!reconciling}
428
+ className="h-8 px-3 rounded-[9px] bg-white/[0.05] hover:bg-white/[0.08] text-text-2 text-[10px] font-800 uppercase tracking-[0.08em] border border-white/[0.06] cursor-pointer disabled:opacity-50"
429
+ >
430
+ {reconciling === entry.extensionId ? 'Reconciling...' : 'Reconcile'}
431
+ </button>
432
+ </div>
433
+
434
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
435
+ {[
436
+ ['Agents', entry.agents.map((item) => `${item.displayName} - ${item.status}`)],
437
+ ['Routines', entry.schedules.map((item) => `${item.displayName} - ${item.status}`)],
438
+ ['Folders', entry.localFolders.map((item) => `${item.displayName} - ${item.configured ? 'configured' : 'missing'}`)],
439
+ ].map(([label, rows]) => (
440
+ <div key={label as string} className="rounded-[12px] bg-bg/60 border border-white/[0.04] p-3 min-h-[96px]">
441
+ <div className="text-[10px] font-800 uppercase tracking-[0.12em] text-text-3/50 mb-2">{label as string}</div>
442
+ {(rows as string[]).length > 0 ? (
443
+ <div className="space-y-1">
444
+ {(rows as string[]).slice(0, 4).map((row) => (
445
+ <div key={row} className="text-[11px] text-text-3/80 truncate">{row}</div>
446
+ ))}
447
+ </div>
448
+ ) : (
449
+ <div className="text-[11px] text-text-3/35">None</div>
450
+ )}
451
+ </div>
452
+ ))}
453
+ </div>
454
+ </div>
455
+ ))}
456
+ </div>
457
+ )}
458
+ </div>
459
+ )}
460
+
305
461
  {tab === 'marketplace' && (
306
462
  <div className="space-y-6">
307
463
  <div className="relative group">