@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/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 {resolveGatewayCredentials} from './resolve-gateway-credentials'
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: Record<string, unknown>): ConfigPluginConfig {
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: Record<string, unknown>): ConfigPluginConfig {
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: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
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: Record<string, unknown>): boolean {
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: Record<string, unknown>): boolean {
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
- // owner: grant mobile/mac app owner permissions (required for cron tool etc.)
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
- export function patchTts(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
274
- let dirty = false
275
- const messages = (config.messages ?? {}) as Record<string, unknown>
276
- const tts = (messages.tts ?? {}) as Record<string, unknown>
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: import('./types/openclaw').OpenClawConfig,
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: Record<string, unknown>): boolean {
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: Record<string, unknown>, stateDir: string): boolean {
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: Record<string, unknown>,
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 as import('./types/openclaw').OpenClawConfig, pc, api) || dirty
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 = patchTts(config, pc) || dirty
775
- dirty = patchWebSearch(config, pc) || dirty
776
- dirty = patchMemorySearch(config, pc) || dirty
777
- dirty = patchModelGateway(config as import('./types/openclaw').OpenClawConfig, api) || dirty
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
  }