@2en/clawly-plugins 1.30.0-beta.2 → 1.30.0-beta.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.
- package/clawly-config-defaults.json5 +107 -3
- package/config-setup.ts +140 -324
- package/gateway/index.ts +0 -2
- package/gateway/offline-push.test.ts +39 -20
- package/gateway/offline-push.ts +28 -0
- package/index.ts +1 -1
- package/outbound.ts +20 -24
- package/package.json +4 -5
- package/tools/clawly-send-file.test.ts +400 -0
- package/tools/clawly-send-file.ts +307 -0
- package/tools/index.ts +2 -2
- package/types.ts +1 -1
- package/gateway/node-dangerous-allowlist.ts +0 -84
- package/tools/clawly-send-image.ts +0 -228
package/config-setup.ts
CHANGED
|
@@ -17,10 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import fs from 'node:fs'
|
|
19
19
|
import path from 'node:path'
|
|
20
|
-
|
|
21
|
-
import type {PluginApi} from './index'
|
|
20
|
+
import JSON5 from 'json5'
|
|
22
21
|
import {autoSanitizeSession} from './gateway/session-sanitize'
|
|
23
|
-
import {
|
|
22
|
+
import type {PluginApi} from './index'
|
|
24
23
|
import {
|
|
25
24
|
ENV_KEY_API_KEY,
|
|
26
25
|
ENV_KEY_BASE,
|
|
@@ -30,16 +29,14 @@ import {
|
|
|
30
29
|
stripPathname,
|
|
31
30
|
writeOpenclawConfig,
|
|
32
31
|
} from './model-gateway-setup'
|
|
32
|
+
import {resolveGatewayCredentials} from './resolve-gateway-credentials'
|
|
33
|
+
import type {OpenClawConfig} from './types/openclaw'
|
|
33
34
|
|
|
34
35
|
export interface ConfigPluginConfig {
|
|
35
36
|
instanceId?: string
|
|
36
37
|
agentId?: string
|
|
37
38
|
agentName?: string
|
|
38
39
|
workspaceDir?: string
|
|
39
|
-
defaultModel?: string
|
|
40
|
-
defaultImageModel?: string
|
|
41
|
-
defaultHeartbeatModel?: string
|
|
42
|
-
elevenlabsApiKey?: string
|
|
43
40
|
modelGatewayBaseUrl?: string
|
|
44
41
|
modelGatewayToken?: string
|
|
45
42
|
otelToken?: string
|
|
@@ -54,7 +51,7 @@ function asObj(value: unknown): Record<string, unknown> {
|
|
|
54
51
|
: {}
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
function readFilePluginConfig(config:
|
|
54
|
+
function readFilePluginConfig(config: OpenClawConfig): ConfigPluginConfig {
|
|
58
55
|
const plugins = asObj(config.plugins)
|
|
59
56
|
const entries = asObj(plugins.entries)
|
|
60
57
|
const raw = asObj(entries['clawly-plugins'])
|
|
@@ -77,25 +74,12 @@ function mergeDefinedPluginConfig(
|
|
|
77
74
|
return merged
|
|
78
75
|
}
|
|
79
76
|
|
|
80
|
-
function toPCMerged(api: PluginApi, config:
|
|
77
|
+
function toPCMerged(api: PluginApi, config: OpenClawConfig): ConfigPluginConfig {
|
|
81
78
|
const fileConfig = readFilePluginConfig(config)
|
|
82
79
|
const runtimeConfig = (api.pluginConfig ?? {}) as ConfigPluginConfig
|
|
83
80
|
return mergeDefinedPluginConfig(fileConfig, runtimeConfig)
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
const LEGACY_DEFAULT_MODEL = `${PROVIDER_NAME}/anthropic/claude-sonnet-4.6`
|
|
87
|
-
const DEFAULT_ELEVENLABS_VOICE_ID = 'DwwuoY7Uz8AP8zrY5TAo'
|
|
88
|
-
|
|
89
|
-
function toProviderModelId(model?: string): string {
|
|
90
|
-
const normalized = model?.trim() ?? ''
|
|
91
|
-
if (!normalized) return ''
|
|
92
|
-
return normalized.startsWith(`${PROVIDER_NAME}/`) ? normalized : `${PROVIDER_NAME}/${normalized}`
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function resolveDefaultModel(pc: ConfigPluginConfig): string {
|
|
96
|
-
return toProviderModelId(pc.defaultModel) || LEGACY_DEFAULT_MODEL
|
|
97
|
-
}
|
|
98
|
-
|
|
99
83
|
// ---------------------------------------------------------------------------
|
|
100
84
|
// Domain helpers — each returns true when config was mutated
|
|
101
85
|
//
|
|
@@ -104,7 +88,7 @@ function resolveDefaultModel(pc: ConfigPluginConfig): string {
|
|
|
104
88
|
// diagnostic UI stays in sync.
|
|
105
89
|
// ---------------------------------------------------------------------------
|
|
106
90
|
|
|
107
|
-
export function patchAgent(config:
|
|
91
|
+
export function patchAgent(config: OpenClawConfig, pc: ConfigPluginConfig): boolean {
|
|
108
92
|
if (!pc.agentId) return false
|
|
109
93
|
|
|
110
94
|
let dirty = false
|
|
@@ -138,22 +122,6 @@ export function patchAgent(config: Record<string, unknown>, pc: ConfigPluginConf
|
|
|
138
122
|
dirty = true
|
|
139
123
|
}
|
|
140
124
|
|
|
141
|
-
// model: set-if-missing
|
|
142
|
-
if (!defaults.model) {
|
|
143
|
-
defaults.model = {primary: resolveDefaultModel(pc)}
|
|
144
|
-
dirty = true
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// imageModel: set-if-missing
|
|
148
|
-
const defaultImageModel = toProviderModelId(pc.defaultImageModel)
|
|
149
|
-
if (defaultImageModel && !defaults.imageModel) {
|
|
150
|
-
defaults.imageModel = {
|
|
151
|
-
primary: defaultImageModel,
|
|
152
|
-
fallbacks: [],
|
|
153
|
-
}
|
|
154
|
-
dirty = true
|
|
155
|
-
}
|
|
156
|
-
|
|
157
125
|
// sandbox.mode: enforce (only set the field we care about)
|
|
158
126
|
const sandbox = (defaults.sandbox ?? {}) as Record<string, unknown>
|
|
159
127
|
if (sandbox.mode !== 'off') {
|
|
@@ -162,12 +130,6 @@ export function patchAgent(config: Record<string, unknown>, pc: ConfigPluginConf
|
|
|
162
130
|
dirty = true
|
|
163
131
|
}
|
|
164
132
|
|
|
165
|
-
// verboseDefault: set-if-missing
|
|
166
|
-
if (defaults.verboseDefault === undefined) {
|
|
167
|
-
defaults.verboseDefault = 'on'
|
|
168
|
-
dirty = true
|
|
169
|
-
}
|
|
170
|
-
|
|
171
133
|
if (dirty) {
|
|
172
134
|
agents.defaults = defaults
|
|
173
135
|
config.agents = agents
|
|
@@ -176,7 +138,7 @@ export function patchAgent(config: Record<string, unknown>, pc: ConfigPluginConf
|
|
|
176
138
|
return dirty
|
|
177
139
|
}
|
|
178
140
|
|
|
179
|
-
export function patchGateway(config:
|
|
141
|
+
export function patchGateway(config: OpenClawConfig): boolean {
|
|
180
142
|
let dirty = false
|
|
181
143
|
const gateway = (config.gateway ?? {}) as Record<string, unknown>
|
|
182
144
|
|
|
@@ -223,7 +185,7 @@ export function patchGateway(config: Record<string, unknown>): boolean {
|
|
|
223
185
|
return dirty
|
|
224
186
|
}
|
|
225
187
|
|
|
226
|
-
export function patchBrowser(config:
|
|
188
|
+
export function patchBrowser(config: OpenClawConfig): boolean {
|
|
227
189
|
let dirty = false
|
|
228
190
|
|
|
229
191
|
// browser: enforce individual fields only
|
|
@@ -250,7 +212,7 @@ export function patchBrowser(config: Record<string, unknown>): boolean {
|
|
|
250
212
|
dirty = true
|
|
251
213
|
}
|
|
252
214
|
|
|
253
|
-
//
|
|
215
|
+
// ownerAllowFrom: append-if-missing (can't use $include — arrays concatenate)
|
|
254
216
|
const ownerAllowFrom = Array.isArray(commands.ownerAllowFrom)
|
|
255
217
|
? (commands.ownerAllowFrom as string[])
|
|
256
218
|
: []
|
|
@@ -270,264 +232,10 @@ export function patchBrowser(config: Record<string, unknown>): boolean {
|
|
|
270
232
|
return dirty
|
|
271
233
|
}
|
|
272
234
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const elevenlabs = (tts.elevenlabs ?? {}) as Record<string, unknown>
|
|
278
|
-
|
|
279
|
-
// Credentials: enforce (only when credentials are available)
|
|
280
|
-
const useProxy = !!(pc.modelGatewayBaseUrl && pc.modelGatewayToken)
|
|
281
|
-
const hasDirectKey = !!pc.elevenlabsApiKey
|
|
282
|
-
if (useProxy || hasDirectKey) {
|
|
283
|
-
const targetApiKey = useProxy ? pc.modelGatewayToken! : pc.elevenlabsApiKey!
|
|
284
|
-
const targetBaseUrl = useProxy
|
|
285
|
-
? `${pc.modelGatewayBaseUrl}/elevenlabs`
|
|
286
|
-
: 'https://api.elevenlabs.io'
|
|
287
|
-
|
|
288
|
-
if (elevenlabs.apiKey !== targetApiKey) {
|
|
289
|
-
elevenlabs.apiKey = targetApiKey
|
|
290
|
-
dirty = true
|
|
291
|
-
}
|
|
292
|
-
if (elevenlabs.baseUrl !== targetBaseUrl) {
|
|
293
|
-
elevenlabs.baseUrl = targetBaseUrl
|
|
294
|
-
dirty = true
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// voiceId: set-if-missing (never overwrite user selection)
|
|
299
|
-
// Use falsy check — old plugin versions (< beta.3) enforced voiceId = ''
|
|
300
|
-
// which is not undefined but is still an empty/invalid value.
|
|
301
|
-
// null is also treated as missing here.
|
|
302
|
-
if (!elevenlabs.voiceId) {
|
|
303
|
-
elevenlabs.voiceId = DEFAULT_ELEVENLABS_VOICE_ID
|
|
304
|
-
dirty = true
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// Tuning: set-if-missing
|
|
308
|
-
if (tts.auto === undefined) {
|
|
309
|
-
tts.auto = 'tagged'
|
|
310
|
-
dirty = true
|
|
311
|
-
}
|
|
312
|
-
if (tts.mode === undefined) {
|
|
313
|
-
tts.mode = 'final'
|
|
314
|
-
dirty = true
|
|
315
|
-
}
|
|
316
|
-
if (tts.provider === undefined) {
|
|
317
|
-
tts.provider = 'elevenlabs'
|
|
318
|
-
dirty = true
|
|
319
|
-
}
|
|
320
|
-
if (tts.summaryModel === undefined) {
|
|
321
|
-
tts.summaryModel = resolveDefaultModel(pc)
|
|
322
|
-
dirty = true
|
|
323
|
-
}
|
|
324
|
-
if (tts.modelOverrides === undefined) {
|
|
325
|
-
tts.modelOverrides = {enabled: true}
|
|
326
|
-
dirty = true
|
|
327
|
-
}
|
|
328
|
-
if (tts.maxTextLength === undefined) {
|
|
329
|
-
tts.maxTextLength = 4000
|
|
330
|
-
dirty = true
|
|
331
|
-
}
|
|
332
|
-
if (tts.timeoutMs === undefined) {
|
|
333
|
-
tts.timeoutMs = 30000
|
|
334
|
-
dirty = true
|
|
335
|
-
}
|
|
336
|
-
if (tts.prefsPath === undefined) {
|
|
337
|
-
tts.prefsPath = '~/.openclaw/settings/tts.json'
|
|
338
|
-
dirty = true
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// elevenlabs tuning: set-if-missing
|
|
342
|
-
if (elevenlabs.modelId === undefined) {
|
|
343
|
-
elevenlabs.modelId = 'eleven_multilingual_v2'
|
|
344
|
-
dirty = true
|
|
345
|
-
}
|
|
346
|
-
if (elevenlabs.seed === undefined) {
|
|
347
|
-
elevenlabs.seed = 42
|
|
348
|
-
dirty = true
|
|
349
|
-
}
|
|
350
|
-
if (elevenlabs.applyTextNormalization === undefined) {
|
|
351
|
-
elevenlabs.applyTextNormalization = 'auto'
|
|
352
|
-
dirty = true
|
|
353
|
-
}
|
|
354
|
-
if (elevenlabs.languageCode === undefined) {
|
|
355
|
-
elevenlabs.languageCode = 'en'
|
|
356
|
-
dirty = true
|
|
357
|
-
}
|
|
358
|
-
if (elevenlabs.voiceSettings === undefined) {
|
|
359
|
-
elevenlabs.voiceSettings = {
|
|
360
|
-
stability: 0.5,
|
|
361
|
-
similarityBoost: 0.75,
|
|
362
|
-
style: 0.0,
|
|
363
|
-
useSpeakerBoost: true,
|
|
364
|
-
speed: 1.0,
|
|
365
|
-
}
|
|
366
|
-
dirty = true
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (dirty) {
|
|
370
|
-
tts.elevenlabs = elevenlabs
|
|
371
|
-
messages.tts = tts
|
|
372
|
-
config.messages = messages
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return dirty
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
export function patchWebSearch(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
|
|
379
|
-
if (!pc.modelGatewayBaseUrl || !pc.modelGatewayToken) return false
|
|
380
|
-
|
|
381
|
-
let dirty = false
|
|
382
|
-
const tools = (config.tools ?? {}) as Record<string, unknown>
|
|
383
|
-
const web = (tools.web ?? {}) as Record<string, unknown>
|
|
384
|
-
const search = (web.search ?? {}) as Record<string, unknown>
|
|
385
|
-
const perplexity = (search.perplexity ?? {}) as Record<string, unknown>
|
|
386
|
-
|
|
387
|
-
// Provider: enforce
|
|
388
|
-
if (search.provider !== 'perplexity') {
|
|
389
|
-
search.provider = 'perplexity'
|
|
390
|
-
dirty = true
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Perplexity credentials: enforce (nested under search.perplexity)
|
|
394
|
-
if (perplexity.apiKey !== pc.modelGatewayToken) {
|
|
395
|
-
perplexity.apiKey = pc.modelGatewayToken
|
|
396
|
-
dirty = true
|
|
397
|
-
}
|
|
398
|
-
if (perplexity.baseUrl !== pc.modelGatewayBaseUrl) {
|
|
399
|
-
perplexity.baseUrl = pc.modelGatewayBaseUrl
|
|
400
|
-
dirty = true
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Perplexity model: set-if-missing
|
|
404
|
-
if (perplexity.model === undefined) {
|
|
405
|
-
perplexity.model = 'perplexity/sonar-pro'
|
|
406
|
-
dirty = true
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Tuning: set-if-missing
|
|
410
|
-
if (search.maxResults === undefined) {
|
|
411
|
-
search.maxResults = 5
|
|
412
|
-
dirty = true
|
|
413
|
-
}
|
|
414
|
-
if (search.timeoutSeconds === undefined) {
|
|
415
|
-
search.timeoutSeconds = 30
|
|
416
|
-
dirty = true
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (dirty) {
|
|
420
|
-
search.perplexity = perplexity
|
|
421
|
-
web.search = search
|
|
422
|
-
tools.web = web
|
|
423
|
-
config.tools = tools
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return dirty
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
export function patchMemorySearch(
|
|
430
|
-
config: Record<string, unknown>,
|
|
431
|
-
pc: ConfigPluginConfig,
|
|
432
|
-
): boolean {
|
|
433
|
-
if (!pc.modelGatewayBaseUrl || !pc.modelGatewayToken) return false
|
|
434
|
-
|
|
435
|
-
let dirty = false
|
|
436
|
-
const agents = (config.agents ?? {}) as Record<string, unknown>
|
|
437
|
-
const defaults = (agents.defaults ?? {}) as Record<string, unknown>
|
|
438
|
-
const ms = (defaults.memorySearch ?? {}) as Record<string, unknown>
|
|
439
|
-
const remote = (ms.remote ?? {}) as Record<string, unknown>
|
|
440
|
-
const batch = (remote.batch ?? {}) as Record<string, unknown>
|
|
441
|
-
|
|
442
|
-
// Provider: enforce (proxy mimics OpenAI API)
|
|
443
|
-
if (ms.provider !== 'openai') {
|
|
444
|
-
ms.provider = 'openai'
|
|
445
|
-
dirty = true
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// Model: set-if-missing
|
|
449
|
-
if (ms.model === undefined) {
|
|
450
|
-
ms.model = 'text-embedding-3-small'
|
|
451
|
-
dirty = true
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Remote credentials: enforce
|
|
455
|
-
if (remote.baseUrl !== pc.modelGatewayBaseUrl) {
|
|
456
|
-
remote.baseUrl = pc.modelGatewayBaseUrl
|
|
457
|
-
dirty = true
|
|
458
|
-
}
|
|
459
|
-
if (remote.apiKey !== pc.modelGatewayToken) {
|
|
460
|
-
remote.apiKey = pc.modelGatewayToken
|
|
461
|
-
dirty = true
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Batch API not supported through proxy: enforce disabled
|
|
465
|
-
if (batch.enabled !== false) {
|
|
466
|
-
batch.enabled = false
|
|
467
|
-
dirty = true
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (dirty) {
|
|
471
|
-
remote.batch = batch
|
|
472
|
-
ms.remote = remote
|
|
473
|
-
defaults.memorySearch = ms
|
|
474
|
-
agents.defaults = defaults
|
|
475
|
-
config.agents = agents
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return dirty
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function patchContextPruning(config: Record<string, unknown>): boolean {
|
|
482
|
-
const agents = (config.agents ?? {}) as Record<string, unknown>
|
|
483
|
-
const defaults = (agents.defaults ?? {}) as Record<string, unknown>
|
|
484
|
-
const cp = (defaults.contextPruning ?? {}) as Record<string, unknown>
|
|
485
|
-
|
|
486
|
-
// mode: set-if-missing — enable cache-ttl by default for cost savings,
|
|
487
|
-
// but don't override if an operator has explicitly configured it.
|
|
488
|
-
if (cp.mode === undefined) {
|
|
489
|
-
cp.mode = 'cache-ttl'
|
|
490
|
-
defaults.contextPruning = cp
|
|
491
|
-
agents.defaults = defaults
|
|
492
|
-
config.agents = agents
|
|
493
|
-
return true
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return false
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const DEFAULT_HEARTBEAT_MODEL = `${PROVIDER_NAME}/moonshotai/kimi-k2.5`
|
|
500
|
-
|
|
501
|
-
function resolveDefaultHeartbeatModel(pc: ConfigPluginConfig): string {
|
|
502
|
-
return toProviderModelId(pc.defaultHeartbeatModel) || DEFAULT_HEARTBEAT_MODEL
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function patchHeartbeat(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
|
|
506
|
-
let dirty = false
|
|
507
|
-
const agents = (config.agents ?? {}) as Record<string, unknown>
|
|
508
|
-
const defaults = (agents.defaults ?? {}) as Record<string, unknown>
|
|
509
|
-
const heartbeat = (defaults.heartbeat ?? {}) as Record<string, unknown>
|
|
510
|
-
|
|
511
|
-
// model: set-if-missing
|
|
512
|
-
if (!heartbeat.model) {
|
|
513
|
-
heartbeat.model = resolveDefaultHeartbeatModel(pc)
|
|
514
|
-
dirty = true
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// lightContext: set-if-missing
|
|
518
|
-
if (heartbeat.lightContext === undefined) {
|
|
519
|
-
heartbeat.lightContext = true
|
|
520
|
-
dirty = true
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if (dirty) {
|
|
524
|
-
defaults.heartbeat = heartbeat
|
|
525
|
-
agents.defaults = defaults
|
|
526
|
-
config.agents = agents
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return dirty
|
|
530
|
-
}
|
|
235
|
+
// patchTts — migrated to clawly-config-defaults.json5
|
|
236
|
+
// patchWebSearch — migrated to clawly-config-defaults.json5
|
|
237
|
+
// patchMemorySearch — migrated to clawly-config-defaults.json5
|
|
238
|
+
// patchHeartbeat — migrated to clawly-config-defaults.json5
|
|
531
239
|
|
|
532
240
|
// TODO: Re-enable patchToolPolicy once rollback-safe (deny is sticky — rolling back
|
|
533
241
|
// the plugin leaves web_search permanently denied). Tracked in ENG-1493.
|
|
@@ -552,7 +260,7 @@ export function patchHeartbeat(config: Record<string, unknown>, pc: ConfigPlugin
|
|
|
552
260
|
// }
|
|
553
261
|
|
|
554
262
|
export function patchEnvVars(
|
|
555
|
-
config:
|
|
263
|
+
config: OpenClawConfig,
|
|
556
264
|
pc: ConfigPluginConfig,
|
|
557
265
|
api: PluginApi,
|
|
558
266
|
): boolean {
|
|
@@ -589,7 +297,7 @@ export function patchEnvVars(
|
|
|
589
297
|
return dirty
|
|
590
298
|
}
|
|
591
299
|
|
|
592
|
-
export function patchSession(config:
|
|
300
|
+
export function patchSession(config: OpenClawConfig): boolean {
|
|
593
301
|
let dirty = false
|
|
594
302
|
|
|
595
303
|
// Remove deprecated session.mainKey (OpenClaw v2026.1.5 hardcoded it to "main")
|
|
@@ -638,7 +346,7 @@ const INCLUDE_PATH = './extensions/clawly-plugins/clawly-config-defaults.json5'
|
|
|
638
346
|
* Ensures our defaults file is listed in the config's `$include` array.
|
|
639
347
|
* Returns true if `$include` was modified.
|
|
640
348
|
*/
|
|
641
|
-
export function ensureInclude(config: Record<string, unknown>): boolean {
|
|
349
|
+
export function ensureInclude(config: OpenClawConfig & Record<string, unknown>): boolean {
|
|
642
350
|
const existing = config.$include
|
|
643
351
|
let includes: string[]
|
|
644
352
|
|
|
@@ -685,7 +393,7 @@ const NPM_PKG_NAME = '@2en/clawly-plugins'
|
|
|
685
393
|
* in the provision write-plugins-config step. Without this record,
|
|
686
394
|
* `openclaw plugins update` cannot function.
|
|
687
395
|
*/
|
|
688
|
-
export function patchInstallRecord(config:
|
|
396
|
+
export function patchInstallRecord(config: OpenClawConfig, stateDir: string): boolean {
|
|
689
397
|
const plugins = asObj(config.plugins)
|
|
690
398
|
const installs = asObj(plugins.installs)
|
|
691
399
|
|
|
@@ -734,11 +442,7 @@ export function patchInstallRecord(config: Record<string, unknown>, stateDir: st
|
|
|
734
442
|
return true
|
|
735
443
|
}
|
|
736
444
|
|
|
737
|
-
function repairLegacyProvisionState(
|
|
738
|
-
api: PluginApi,
|
|
739
|
-
config: Record<string, unknown>,
|
|
740
|
-
stateDir: string,
|
|
741
|
-
) {
|
|
445
|
+
function repairLegacyProvisionState(api: PluginApi, config: OpenClawConfig, stateDir: string) {
|
|
742
446
|
const installRecordPatched = patchInstallRecord(config, stateDir)
|
|
743
447
|
if (installRecordPatched) {
|
|
744
448
|
api.logger.warn('plugins.installs.clawly-plugins was missing — self-healed from disk.')
|
|
@@ -746,10 +450,102 @@ function repairLegacyProvisionState(
|
|
|
746
450
|
return installRecordPatched
|
|
747
451
|
}
|
|
748
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Remove fields from config that are identical to the included defaults.
|
|
455
|
+
* Since `$include` deep-merges with main config winning, residual values
|
|
456
|
+
* from old JS set-if-missing code "shadow" the json5 defaults and prevent
|
|
457
|
+
* future default updates from taking effect.
|
|
458
|
+
*
|
|
459
|
+
* Only prunes leaf values that exactly match.
|
|
460
|
+
* Returns true if any field was deleted.
|
|
461
|
+
*/
|
|
462
|
+
function stableStringify(v: unknown): string {
|
|
463
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
464
|
+
const obj = v as Record<string, unknown>
|
|
465
|
+
return `{${Object.keys(obj)
|
|
466
|
+
.sort()
|
|
467
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
|
468
|
+
.join(',')}}`
|
|
469
|
+
}
|
|
470
|
+
return JSON.stringify(v)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function pruneIncludedDefaults(
|
|
474
|
+
config: Record<string, unknown>,
|
|
475
|
+
defaults: Record<string, unknown>,
|
|
476
|
+
): boolean {
|
|
477
|
+
let pruned = false
|
|
478
|
+
|
|
479
|
+
function walk(defaultsNode: Record<string, unknown>, configNode: Record<string, unknown>): void {
|
|
480
|
+
for (const key of Object.keys(defaultsNode)) {
|
|
481
|
+
const defaultVal = defaultsNode[key]
|
|
482
|
+
const configVal = configNode[key]
|
|
483
|
+
if (configVal === undefined) continue
|
|
484
|
+
|
|
485
|
+
// Both are plain objects → recurse
|
|
486
|
+
if (
|
|
487
|
+
defaultVal !== null &&
|
|
488
|
+
typeof defaultVal === 'object' &&
|
|
489
|
+
!Array.isArray(defaultVal) &&
|
|
490
|
+
configVal !== null &&
|
|
491
|
+
typeof configVal === 'object' &&
|
|
492
|
+
!Array.isArray(configVal)
|
|
493
|
+
) {
|
|
494
|
+
walk(defaultVal as Record<string, unknown>, configVal as Record<string, unknown>)
|
|
495
|
+
// Clean up empty parent objects left behind after pruning
|
|
496
|
+
if (Object.keys(configVal as Record<string, unknown>).length === 0) {
|
|
497
|
+
delete configNode[key]
|
|
498
|
+
pruned = true
|
|
499
|
+
}
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Both are non-empty arrays → remove config elements that exist in defaults
|
|
504
|
+
if (Array.isArray(defaultVal) && defaultVal.length > 0 && Array.isArray(configVal)) {
|
|
505
|
+
const defaultSet = new Set(defaultVal.map((v) => stableStringify(v)))
|
|
506
|
+
const filtered = configVal.filter((v) => !defaultSet.has(stableStringify(v)))
|
|
507
|
+
if (filtered.length !== configVal.length) {
|
|
508
|
+
if (filtered.length === 0) {
|
|
509
|
+
delete configNode[key]
|
|
510
|
+
} else {
|
|
511
|
+
configNode[key] = filtered
|
|
512
|
+
}
|
|
513
|
+
pruned = true
|
|
514
|
+
}
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Leaf comparison: use stableStringify for key-order-insensitive deep equality
|
|
519
|
+
if (stableStringify(configVal) === stableStringify(defaultVal)) {
|
|
520
|
+
delete configNode[key]
|
|
521
|
+
pruned = true
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
walk(defaults, config)
|
|
527
|
+
return pruned
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Load and parse clawly-config-defaults.json5 from the plugin install directory.
|
|
532
|
+
* Uses the bundled `json5` package (declared in package.json).
|
|
533
|
+
*/
|
|
534
|
+
function loadIncludedDefaults(stateDir: string): Record<string, unknown> | null {
|
|
535
|
+
const defaultsPath = path.join(stateDir, INCLUDE_PATH)
|
|
536
|
+
try {
|
|
537
|
+
const raw = fs.readFileSync(defaultsPath, 'utf-8')
|
|
538
|
+
return JSON5.parse(raw) as Record<string, unknown>
|
|
539
|
+
} catch {
|
|
540
|
+
return null
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
749
544
|
function reconcileRuntimeConfig(
|
|
750
545
|
api: PluginApi,
|
|
751
|
-
config:
|
|
546
|
+
config: OpenClawConfig,
|
|
752
547
|
pc: ConfigPluginConfig,
|
|
548
|
+
stateDir: string,
|
|
753
549
|
): boolean {
|
|
754
550
|
// Ensure gateway credentials are available to all patchers.
|
|
755
551
|
// After a force-update, runtime pluginConfig may be empty and on-disk
|
|
@@ -764,17 +560,20 @@ function reconcileRuntimeConfig(
|
|
|
764
560
|
}
|
|
765
561
|
|
|
766
562
|
let dirty = false
|
|
767
|
-
dirty = patchEnvVars(config
|
|
563
|
+
dirty = patchEnvVars(config, pc, api) || dirty
|
|
768
564
|
dirty = patchAgent(config, pc) || dirty
|
|
769
|
-
dirty = patchContextPruning(config) || dirty
|
|
770
|
-
dirty = patchHeartbeat(config, pc) || dirty
|
|
771
565
|
dirty = patchGateway(config) || dirty
|
|
772
566
|
dirty = patchBrowser(config) || dirty
|
|
773
567
|
dirty = patchSession(config) || dirty
|
|
774
|
-
dirty =
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
568
|
+
dirty = patchModelGateway(config, api) || dirty
|
|
569
|
+
|
|
570
|
+
const defaults = loadIncludedDefaults(stateDir)
|
|
571
|
+
if (defaults) {
|
|
572
|
+
dirty = pruneIncludedDefaults(config, defaults) || dirty
|
|
573
|
+
} else {
|
|
574
|
+
api.logger.warn('Config setup: failed to load included defaults, skipping dedup.')
|
|
575
|
+
}
|
|
576
|
+
|
|
778
577
|
return dirty
|
|
779
578
|
}
|
|
780
579
|
|
|
@@ -789,6 +588,15 @@ export function setupConfig(api: PluginApi): void {
|
|
|
789
588
|
return
|
|
790
589
|
}
|
|
791
590
|
|
|
591
|
+
// OpenClaw exposes three config layers:
|
|
592
|
+
// parsed — raw JSON from openclaw.json (no $include, no env expansion)
|
|
593
|
+
// resolved — parsed + $include deep-merge + ${ENV} interpolation
|
|
594
|
+
// runtime — resolved + SecretRef resolution, cached in-memory snapshot
|
|
595
|
+
//
|
|
596
|
+
// We need parsed: patchers mutate raw fields and write back via
|
|
597
|
+
// writeConfigFile, which diffs against the runtime snapshot and projects
|
|
598
|
+
// changes onto the source file. Using resolved/runtime would inline
|
|
599
|
+
// $include defaults into openclaw.json, defeating the json5 migration.
|
|
792
600
|
const configPath = path.join(stateDir, 'openclaw.json')
|
|
793
601
|
const config = readOpenclawConfig(configPath)
|
|
794
602
|
const pc = toPCMerged(api, config)
|
|
@@ -797,12 +605,20 @@ export function setupConfig(api: PluginApi): void {
|
|
|
797
605
|
dirty = ensureInclude(config) || dirty
|
|
798
606
|
hardenIncludePermissions(stateDir, api)
|
|
799
607
|
dirty = repairLegacyProvisionState(api, config, stateDir) || dirty
|
|
800
|
-
dirty = reconcileRuntimeConfig(api, config, pc) || dirty
|
|
608
|
+
dirty = reconcileRuntimeConfig(api, config, pc, stateDir) || dirty
|
|
801
609
|
|
|
802
610
|
if (dirty) {
|
|
803
611
|
try {
|
|
804
612
|
writeOpenclawConfig(configPath, config)
|
|
805
613
|
api.logger.info('Config setup: patched openclaw.json.')
|
|
614
|
+
// Refresh the gateway's in-memory runtime config snapshot.
|
|
615
|
+
// The sync write above updates the file on disk, but the gateway
|
|
616
|
+
// caches config via runtimeConfigSnapshot (set during startup by
|
|
617
|
+
// the secrets system). Without this refresh, loadConfig() keeps
|
|
618
|
+
// returning the stale pre-patch config until the next restart.
|
|
619
|
+
void api.runtime.config.writeConfigFile(config).catch((err) => {
|
|
620
|
+
api.logger.warn(`Config setup: runtime snapshot refresh failed: ${(err as Error).message}`)
|
|
621
|
+
})
|
|
806
622
|
} catch (err) {
|
|
807
623
|
api.logger.error(`Config setup failed: ${(err as Error).message}`)
|
|
808
624
|
}
|
package/gateway/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {registerAgentSend} from './agent'
|
|
|
3
3
|
import {registerCalendarNative} from './calendar-native'
|
|
4
4
|
import {registerAnalytics} from './analytics'
|
|
5
5
|
import {registerAudit} from './audit'
|
|
6
|
-
import {registerNodeDangerousAllowlist} from './node-dangerous-allowlist'
|
|
7
6
|
import {registerClawhub2gateway} from './clawhub2gateway'
|
|
8
7
|
import {registerConfigRepair} from './config-repair'
|
|
9
8
|
import {registerConfigTimezone} from './config-timezone'
|
|
@@ -64,6 +63,5 @@ export function registerGateway(api: PluginApi) {
|
|
|
64
63
|
registerPairing(api)
|
|
65
64
|
registerVersion(api)
|
|
66
65
|
registerAudit(api)
|
|
67
|
-
registerNodeDangerousAllowlist(api)
|
|
68
66
|
registerCalendarNative(api)
|
|
69
67
|
}
|