@2en/clawly-plugins 1.28.0 → 1.29.0-beta.0

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
@@ -35,6 +35,7 @@ export interface ConfigPluginConfig {
35
35
  workspaceDir?: string
36
36
  defaultModel?: string
37
37
  defaultImageModel?: string
38
+ defaultHeartbeatModel?: string
38
39
  elevenlabsApiKey?: string
39
40
  modelGatewayBaseUrl?: string
40
41
  modelGatewayToken?: string
@@ -201,6 +202,20 @@ export function patchGateway(config: Record<string, unknown>): boolean {
201
202
  dirty = true
202
203
  }
203
204
 
205
+ // controlUi.allowedOrigins: enforce wildcard.
206
+ // OpenClaw v2026.2.26+ enforces origin checks for ANY browser WebSocket
207
+ // client (not just Control UI / Webchat). Without this, the Clawly web app
208
+ // fails to connect because the browser auto-sends an Origin header that
209
+ // doesn't match any allowlist entry. Wildcard is safe here — these are
210
+ // dedicated per-user sprites protected by token + device auth.
211
+ const controlUi = (gateway.controlUi ?? {}) as Record<string, unknown>
212
+ const allowedOrigins = controlUi.allowedOrigins
213
+ if (!Array.isArray(allowedOrigins) || allowedOrigins.length !== 1 || allowedOrigins[0] !== '*') {
214
+ controlUi.allowedOrigins = ['*']
215
+ gateway.controlUi = controlUi
216
+ dirty = true
217
+ }
218
+
204
219
  if (dirty) config.gateway = gateway
205
220
  return dirty
206
221
  }
@@ -454,6 +469,39 @@ export function patchMemorySearch(
454
469
  return dirty
455
470
  }
456
471
 
472
+ const DEFAULT_HEARTBEAT_MODEL = `${PROVIDER_NAME}/qwen/qwen3.5-flash-02-23`
473
+
474
+ function resolveDefaultHeartbeatModel(pc: ConfigPluginConfig): string {
475
+ return toProviderModelId(pc.defaultHeartbeatModel) || DEFAULT_HEARTBEAT_MODEL
476
+ }
477
+
478
+ export function patchHeartbeat(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
479
+ let dirty = false
480
+ const agents = (config.agents ?? {}) as Record<string, unknown>
481
+ const defaults = (agents.defaults ?? {}) as Record<string, unknown>
482
+ const heartbeat = (defaults.heartbeat ?? {}) as Record<string, unknown>
483
+
484
+ // model: set-if-missing
485
+ if (!heartbeat.model) {
486
+ heartbeat.model = resolveDefaultHeartbeatModel(pc)
487
+ dirty = true
488
+ }
489
+
490
+ // lightContext: set-if-missing
491
+ if (heartbeat.lightContext === undefined) {
492
+ heartbeat.lightContext = true
493
+ dirty = true
494
+ }
495
+
496
+ if (dirty) {
497
+ defaults.heartbeat = heartbeat
498
+ agents.defaults = defaults
499
+ config.agents = agents
500
+ }
501
+
502
+ return dirty
503
+ }
504
+
457
505
  // TODO: Re-enable patchToolPolicy once rollback-safe (deny is sticky — rolling back
458
506
  // the plugin leaves web_search permanently denied). Tracked in ENG-1493.
459
507
  // export function patchToolPolicy(config: Record<string, unknown>, pc: ConfigPluginConfig): boolean {
@@ -608,6 +656,7 @@ function reconcileRuntimeConfig(
608
656
 
609
657
  let dirty = false
610
658
  dirty = patchAgent(config, pc) || dirty
659
+ dirty = patchHeartbeat(config, pc) || dirty
611
660
  dirty = patchGateway(config) || dirty
612
661
  dirty = patchBrowser(config) || dirty
613
662
  dirty = patchSession(config) || dirty
@@ -106,7 +106,7 @@ function extractJobId(sessionKey: string): string | null {
106
106
 
107
107
  function emit(attributes: CronTelemetryAttributes, api: PluginApi) {
108
108
  // Emit structured OTel LogRecord (if provider is initialized)
109
- const otelLogger = getOtelLogger()
109
+ const otelLogger = getOtelLogger('cron-telemetry')
110
110
  if (otelLogger) {
111
111
  otelLogger.emit({
112
112
  severityNumber: SeverityNumber.INFO,
package/gateway/index.ts CHANGED
@@ -3,12 +3,13 @@ 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 {registerNodeBrowserAllowlist} from './node-browser-allowlist'
6
+ import {registerNodeDangerousAllowlist} from './node-dangerous-allowlist'
7
7
  import {registerClawhub2gateway} from './clawhub2gateway'
8
8
  import {registerConfigRepair} from './config-repair'
9
9
  import {registerConfigTimezone} from './config-timezone'
10
10
  import {registerCronDelivery} from './cron-delivery'
11
11
  import {registerCronTelemetry} from './cron-telemetry'
12
+ import {registerMessageLog} from './message-log'
12
13
  import {registerMemoryBrowser} from './memory'
13
14
  import {registerNotification} from './notification'
14
15
  import {registerOfflinePush} from './offline-push'
@@ -55,6 +56,7 @@ export function registerGateway(api: PluginApi) {
55
56
  registerOfflinePush(api)
56
57
  registerCronDelivery(api)
57
58
  registerCronTelemetry(api)
59
+ registerMessageLog(api)
58
60
  registerAnalytics(api)
59
61
  registerConfigRepair(api)
60
62
  registerConfigTimezone(api)
@@ -62,6 +64,6 @@ export function registerGateway(api: PluginApi) {
62
64
  registerPairing(api)
63
65
  registerVersion(api)
64
66
  registerAudit(api)
65
- registerNodeBrowserAllowlist(api)
67
+ registerNodeDangerousAllowlist(api)
66
68
  registerCalendarNative(api)
67
69
  }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Message lifecycle logging — captures LLM inputs, outputs, and channel
3
+ * deliveries via OTel structured logs and human-readable api.logger output.
4
+ *
5
+ * Gated by `messageLogEnabled` in pluginConfig (dev-only).
6
+ * Content is never logged — only metadata (model, provider, counts, success).
7
+ *
8
+ * Hooks:
9
+ * - llm_input — logs model, provider, history size, images count
10
+ * - llm_output — logs model, provider, content length
11
+ * - message_sent — logs channel delivery outcome
12
+ */
13
+
14
+ import {SeverityNumber} from '@opentelemetry/api-logs'
15
+ import type {PluginApi} from '../types'
16
+ import {getOtelLogger} from './otel'
17
+ import {readTelemetryPluginConfig} from './telemetry-config'
18
+
19
+ // ── Structured telemetry attributes (OTel LogRecord) ────────────
20
+
21
+ type MessageLogAttributes = {
22
+ 'message.lifecycle': 'llm_input' | 'llm_output' | 'message_sent'
23
+ 'message.model'?: string
24
+ 'message.provider'?: string
25
+ 'message.session_key'?: string
26
+ 'message.session_id'?: string
27
+ 'message.content_length'?: number
28
+ 'message.history_count'?: number
29
+ 'message.images_count'?: number
30
+ 'message.to'?: string
31
+ 'message.success'?: boolean
32
+ 'message.error'?: string
33
+ 'message.channel_id'?: string
34
+ 'instance.id'?: string
35
+ }
36
+
37
+ // ── Helpers ──────────────────────────────────────────────────────
38
+
39
+ function emit(attributes: MessageLogAttributes, api: PluginApi) {
40
+ const otelLogger = getOtelLogger('message-log')
41
+ if (otelLogger) {
42
+ otelLogger.emit({
43
+ severityNumber: SeverityNumber.INFO,
44
+ severityText: 'INFO',
45
+ body: `message.${attributes['message.lifecycle']}`,
46
+ attributes: attributes as Record<string, unknown>,
47
+ })
48
+ }
49
+
50
+ const a = attributes
51
+ switch (a['message.lifecycle']) {
52
+ case 'llm_input':
53
+ api.logger.info(
54
+ `message-log[llm-input]: model=${a['message.model'] ?? '?'} provider=${a['message.provider'] ?? '?'} history=${a['message.history_count'] ?? 0} images=${a['message.images_count'] ?? 0}`,
55
+ )
56
+ break
57
+ case 'llm_output':
58
+ api.logger.info(
59
+ `message-log[llm-output]: model=${a['message.model'] ?? '?'} provider=${a['message.provider'] ?? '?'} len=${a['message.content_length'] ?? 0}`,
60
+ )
61
+ break
62
+ case 'message_sent':
63
+ api.logger.info(
64
+ `message-log[sent]: to=${a['message.to'] ?? '?'} success=${a['message.success'] ?? '?'} channel=${a['message.channel_id'] ?? '?'}${a['message.error'] ? ` error="${a['message.error']}"` : ''}`,
65
+ )
66
+ break
67
+ }
68
+ }
69
+
70
+ // ── Registration ─────────────────────────────────────────────────
71
+
72
+ export function registerMessageLog(api: PluginApi) {
73
+ const cfg = readTelemetryPluginConfig(api.pluginConfig)
74
+
75
+ if (!cfg.messageLogEnabled) {
76
+ api.logger.info('message-log: disabled (messageLogEnabled not set in pluginConfig)')
77
+ return
78
+ }
79
+
80
+ const instanceId = cfg.instanceId ?? process.env.INSTANCE_ID
81
+
82
+ // llm_input: capture what goes into the LLM (metadata only)
83
+ api.on('llm_input', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
84
+ const model = typeof event.model === 'string' ? event.model : undefined
85
+ const provider = typeof event.provider === 'string' ? event.provider : undefined
86
+ const prompt = typeof event.prompt === 'string' ? event.prompt : ''
87
+ const historyMessages = Array.isArray(event.historyMessages) ? event.historyMessages : []
88
+ const imagesCount = typeof event.imagesCount === 'number' ? event.imagesCount : 0
89
+ const sessionKey = typeof ctx?.sessionKey === 'string' ? ctx.sessionKey : undefined
90
+ const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
91
+
92
+ emit(
93
+ {
94
+ 'message.lifecycle': 'llm_input',
95
+ ...(model ? {'message.model': model} : {}),
96
+ ...(provider ? {'message.provider': provider} : {}),
97
+ 'message.content_length': prompt.length,
98
+ 'message.history_count': historyMessages.length,
99
+ 'message.images_count': imagesCount,
100
+ ...(sessionKey ? {'message.session_key': sessionKey} : {}),
101
+ ...(sessionId ? {'message.session_id': sessionId} : {}),
102
+ ...(instanceId ? {'instance.id': instanceId} : {}),
103
+ },
104
+ api,
105
+ )
106
+ })
107
+
108
+ // llm_output: capture LLM response metadata
109
+ api.on('llm_output', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
110
+ const model = typeof event.model === 'string' ? event.model : undefined
111
+ const provider = typeof event.provider === 'string' ? event.provider : undefined
112
+ const assistantTexts = Array.isArray(event.assistantTexts) ? event.assistantTexts : []
113
+ const contentLength = assistantTexts
114
+ .filter((t): t is string => typeof t === 'string')
115
+ .reduce((sum, t) => sum + t.length, 0)
116
+ const sessionKey = typeof ctx?.sessionKey === 'string' ? ctx.sessionKey : undefined
117
+ const sessionId = typeof ctx?.sessionId === 'string' ? ctx.sessionId : undefined
118
+
119
+ emit(
120
+ {
121
+ 'message.lifecycle': 'llm_output',
122
+ ...(model ? {'message.model': model} : {}),
123
+ ...(provider ? {'message.provider': provider} : {}),
124
+ 'message.content_length': contentLength,
125
+ ...(sessionKey ? {'message.session_key': sessionKey} : {}),
126
+ ...(sessionId ? {'message.session_id': sessionId} : {}),
127
+ ...(instanceId ? {'instance.id': instanceId} : {}),
128
+ },
129
+ api,
130
+ )
131
+ })
132
+
133
+ // message_sent: capture channel delivery outcome
134
+ api.on('message_sent', (event: Record<string, unknown>, ctx?: Record<string, unknown>) => {
135
+ const to = typeof event.to === 'string' ? event.to : undefined
136
+ const content = typeof event.content === 'string' ? event.content : ''
137
+ const success = typeof event.success === 'boolean' ? event.success : undefined
138
+ const error = typeof event.error === 'string' ? event.error : undefined
139
+ const channelId = typeof ctx?.channelId === 'string' ? ctx.channelId : undefined
140
+
141
+ emit(
142
+ {
143
+ 'message.lifecycle': 'message_sent',
144
+ ...(to ? {'message.to': to} : {}),
145
+ 'message.content_length': content.length,
146
+ ...(success != null ? {'message.success': success} : {}),
147
+ ...(error ? {'message.error': error} : {}),
148
+ ...(channelId ? {'message.channel_id': channelId} : {}),
149
+ ...(instanceId ? {'instance.id': instanceId} : {}),
150
+ },
151
+ api,
152
+ )
153
+ })
154
+
155
+ api.logger.info('message-log: registered llm_input + llm_output + message_sent hooks')
156
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Ensures all dangerous node commands are in gateway.nodes.allowCommands
3
+ * so the AI agent can invoke them on connected Clawly nodes.
4
+ *
5
+ * Dangerous commands are defined by OpenClaw's node-command-policy and
6
+ * must be explicitly allowlisted. Safe commands work without allowlisting.
7
+ *
8
+ * Runs on gateway_start. Writes directly to openclaw.json via
9
+ * writeOpenclawConfig (no restart triggered — gateway.* changes are
10
+ * classified as "none" by the config watcher).
11
+ */
12
+ import path from 'node:path'
13
+
14
+ import type {PluginApi} from '../types'
15
+ import {readOpenclawConfig, writeOpenclawConfig} from '../model-gateway-setup'
16
+
17
+ const DANGEROUS_COMMANDS = [
18
+ // Browser commands (Mac nodes)
19
+ 'browser.proxy',
20
+ 'browser.navigate',
21
+ 'browser.click',
22
+ 'browser.type',
23
+ 'browser.screenshot',
24
+ 'browser.read',
25
+ 'browser.tabs',
26
+ 'browser.back',
27
+ 'browser.scroll',
28
+ 'browser.evaluate',
29
+ // Reminders + calendar (iOS nodes)
30
+ 'reminders.add',
31
+ 'calendar.add',
32
+ ]
33
+
34
+ export function registerNodeDangerousAllowlist(api: PluginApi) {
35
+ api.on('gateway_start', async () => {
36
+ let stateDir: string
37
+ try {
38
+ stateDir = api.runtime.state.resolveStateDir()
39
+ } catch {
40
+ api.logger.warn('node-dangerous-allowlist: cannot resolve state dir, skipping')
41
+ return
42
+ }
43
+
44
+ const configPath = path.join(stateDir, 'openclaw.json')
45
+ let config: Record<string, unknown>
46
+ try {
47
+ config = readOpenclawConfig(configPath)
48
+ } catch {
49
+ api.logger.warn('node-dangerous-allowlist: failed to read config, skipping')
50
+ return
51
+ }
52
+
53
+ const gateway = (config.gateway as Record<string, unknown>) ?? {}
54
+ const nodes = (gateway.nodes as Record<string, unknown>) ?? {}
55
+ const existing: string[] = Array.isArray(nodes.allowCommands)
56
+ ? (nodes.allowCommands as string[])
57
+ : []
58
+
59
+ const existingSet = new Set(existing)
60
+ const toAdd = DANGEROUS_COMMANDS.filter((cmd) => !existingSet.has(cmd))
61
+
62
+ if (toAdd.length === 0) {
63
+ api.logger.info('node-dangerous-allowlist: all commands already allowlisted')
64
+ return
65
+ }
66
+
67
+ nodes.allowCommands = [...existing, ...toAdd]
68
+ gateway.nodes = nodes
69
+ config.gateway = gateway
70
+
71
+ try {
72
+ writeOpenclawConfig(configPath, config)
73
+ } catch {
74
+ api.logger.warn('node-dangerous-allowlist: failed to write config')
75
+ return
76
+ }
77
+ api.logger.info(
78
+ `node-dangerous-allowlist: added ${toAdd.length} commands to allowlist: ${toAdd.join(', ')}`,
79
+ )
80
+ })
81
+ }
@@ -69,7 +69,15 @@ describe('initOtel', () => {
69
69
  },
70
70
  })
71
71
  expect(getOtelLogger()).toBeTruthy()
72
- expect(loggerName).toBe('cron-telemetry')
72
+ expect(loggerName).toBe('clawly-plugins')
73
+ })
74
+
75
+ test('returns scoped loggers by name', () => {
76
+ initOtel({otelToken: 'tok', otelDataset: 'ds'})
77
+ const a = getOtelLogger('cron-telemetry')
78
+ const b = getOtelLogger('message-log')
79
+ expect(a).toBeTruthy()
80
+ expect(b).toBeTruthy()
73
81
  })
74
82
 
75
83
  test('falls back to legacy env vars', () => {
package/gateway/otel.ts CHANGED
@@ -17,7 +17,7 @@ import {BatchLogRecordProcessor, LoggerProvider} from '@opentelemetry/sdk-logs'
17
17
  import {readTelemetryPluginConfig} from './telemetry-config'
18
18
 
19
19
  let provider: LoggerProvider | null = null
20
- let logger: Logger | null = null
20
+ const loggers = new Map<string, Logger>()
21
21
 
22
22
  export function initOtel(
23
23
  pluginConfig?: Record<string, unknown>,
@@ -40,11 +40,16 @@ export function initOtel(
40
40
 
41
41
  provider = new LoggerProvider({resource})
42
42
  provider.addLogRecordProcessor(new BatchLogRecordProcessor(exporter))
43
- logger = provider.getLogger('cron-telemetry')
44
43
  return true
45
44
  }
46
45
 
47
- export function getOtelLogger(): Logger | null {
46
+ export function getOtelLogger(scope = 'clawly-plugins'): Logger | null {
47
+ if (!provider) return null
48
+ let logger = loggers.get(scope)
49
+ if (!logger) {
50
+ logger = provider.getLogger(scope)
51
+ loggers.set(scope, logger)
52
+ }
48
53
  return logger
49
54
  }
50
55
 
@@ -52,6 +57,6 @@ export async function shutdownOtel(): Promise<void> {
52
57
  if (provider) {
53
58
  await provider.shutdown()
54
59
  provider = null
55
- logger = null
60
+ loggers.clear()
56
61
  }
57
62
  }
@@ -17,6 +17,7 @@ describe('readTelemetryPluginConfig', () => {
17
17
  otelDataset: 'clawly-otel-logs-dev',
18
18
  posthogApiKey: 'ph-key',
19
19
  posthogHost: 'https://us.i.posthog.com',
20
+ messageLogEnabled: false,
20
21
  })
21
22
  })
22
23
 
@@ -35,6 +36,7 @@ describe('readTelemetryPluginConfig', () => {
35
36
  otelDataset: 'clawly-otel-logs-dev',
36
37
  posthogApiKey: 'ph-key',
37
38
  posthogHost: 'https://us.i.posthog.com',
39
+ messageLogEnabled: false,
38
40
  })
39
41
  })
40
42
 
@@ -53,6 +55,7 @@ describe('readTelemetryPluginConfig', () => {
53
55
  otelDataset: undefined,
54
56
  posthogApiKey: undefined,
55
57
  posthogHost: undefined,
58
+ messageLogEnabled: false,
56
59
  })
57
60
  })
58
61
  })
@@ -4,6 +4,7 @@ export interface TelemetryPluginConfig {
4
4
  otelDataset?: string
5
5
  posthogApiKey?: string
6
6
  posthogHost?: string
7
+ messageLogEnabled?: boolean
7
8
  }
8
9
 
9
10
  function readString(value: unknown): string | undefined {
@@ -23,5 +24,6 @@ export function readTelemetryPluginConfig(
23
24
  otelDataset: readString(cfg.otelDataset),
24
25
  posthogApiKey: readString(cfg.posthogApiKey),
25
26
  posthogHost: readString(cfg.posthogHost),
27
+ messageLogEnabled: cfg.messageLogEnabled === true || cfg.messageLogEnabled === 'true',
26
28
  }
27
29
  }
package/index.ts CHANGED
@@ -34,6 +34,9 @@
34
34
  * - agent_end — sends push notification when client is offline; injects cron results into main session
35
35
  * - after_tool_call — cron telemetry: captures cron job creation/deletion
36
36
  * - agent_end (pri 100) — cron telemetry: captures cron execution outcomes with delivery/push flags
37
+ * - llm_input — message-log (dev-only): logs model, provider, history size, images count (no content)
38
+ * - llm_output — message-log (dev-only): logs model, provider, content length (no content)
39
+ * - message_sent — message-log (dev-only): logs channel delivery outcome (no content)
37
40
  * - gateway_start — auto-approves device pairing for Clawly mobile clients (clientId: openclaw-ios)
38
41
  * - gateway_start — registers auto-update cron job (0 3 * * *) for clawly-plugins
39
42
  */
@@ -56,6 +56,7 @@
56
56
  "workspaceDir": { "type": "string" },
57
57
  "defaultModel": { "type": "string" },
58
58
  "defaultImageModel": { "type": "string" },
59
+ "defaultHeartbeatModel": { "type": "string" },
59
60
  "elevenlabsApiKey": { "type": "string" },
60
61
  "otelToken": { "type": "string" },
61
62
  "otelDataset": { "type": "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@2en/clawly-plugins",
3
- "version": "1.28.0",
3
+ "version": "1.29.0-beta.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Agent tool: clawly_msg_break — split the current response into multiple messages.
3
+ *
4
+ * When called, this tool acts as a structural boundary: the text before the
5
+ * tool call becomes one assistant message, and the text after becomes a new one.
6
+ * The client relies on the existing "text reset" mechanism (post-tool-call text
7
+ * detection) to create the split — the tool itself is a no-op.
8
+ *
9
+ * This is preferred over in-band separator tokens (e.g. [MSG_BREAK]) because
10
+ * tool calls are structured and never leak to other channels (Telegram, etc.).
11
+ */
12
+
13
+ import type {PluginApi} from '../types'
14
+
15
+ const TOOL_NAME = 'clawly_msg_break'
16
+
17
+ export function registerMsgBreakTool(api: PluginApi) {
18
+ api.registerTool({
19
+ name: TOOL_NAME,
20
+ description:
21
+ 'Split the current response into multiple messages. Call this between distinct thoughts or topics to send them as separate message bubbles. The text you wrote before this call becomes one message; continue writing after the call to start a new message.',
22
+ parameters: {
23
+ type: 'object',
24
+ properties: {},
25
+ },
26
+ async execute() {
27
+ return {content: [{type: 'text', text: JSON.stringify({ok: true})}]}
28
+ },
29
+ })
30
+
31
+ api.logger.info(`tool: registered ${TOOL_NAME} agent tool`)
32
+ }
package/tools/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {PluginApi} from '../types'
2
2
  import {registerCalendarTools} from './clawly-calendar'
3
3
  import {registerIsUserOnlineTool} from './clawly-is-user-online'
4
+ import {registerMsgBreakTool} from './clawly-msg-break'
4
5
  import {registerDeepSearchTool, registerGrokSearchTool, registerSearchTool} from './clawly-search'
5
6
  import {registerSendAppPushTool} from './clawly-send-app-push'
6
7
  import {registerSendImageTool} from './clawly-send-image'
@@ -9,6 +10,7 @@ import {registerSendMessageTool} from './clawly-send-message'
9
10
  export function registerTools(api: PluginApi) {
10
11
  registerCalendarTools(api)
11
12
  registerIsUserOnlineTool(api)
13
+ registerMsgBreakTool(api)
12
14
  registerSearchTool(api)
13
15
  registerDeepSearchTool(api)
14
16
  registerGrokSearchTool(api)
@@ -1,62 +0,0 @@
1
- /**
2
- * Ensures browser.* node commands are in gateway.nodes.allowCommands
3
- * so the AI agent can invoke them on connected Clawly Mac nodes.
4
- *
5
- * Runs on gateway_start. Writes directly to openclaw.json via
6
- * writeOpenclawConfig (no restart triggered — gateway.* changes are
7
- * classified as "none" by the config watcher).
8
- */
9
- import path from 'node:path'
10
-
11
- import type {PluginApi} from '../types'
12
- import {readOpenclawConfig, writeOpenclawConfig} from '../model-gateway-setup'
13
-
14
- const BROWSER_COMMANDS = [
15
- 'browser.proxy',
16
- 'browser.navigate',
17
- 'browser.click',
18
- 'browser.type',
19
- 'browser.screenshot',
20
- 'browser.read',
21
- 'browser.tabs',
22
- 'browser.back',
23
- 'browser.scroll',
24
- 'browser.evaluate',
25
- ]
26
-
27
- export function registerNodeBrowserAllowlist(api: PluginApi) {
28
- api.on('gateway_start', async () => {
29
- let stateDir: string
30
- try {
31
- stateDir = api.runtime.state.resolveStateDir()
32
- } catch {
33
- api.logger.warn('node-browser-allowlist: cannot resolve state dir, skipping')
34
- return
35
- }
36
-
37
- const configPath = path.join(stateDir, 'openclaw.json')
38
- const config = readOpenclawConfig(configPath)
39
-
40
- // Ensure gateway.nodes.allowCommands includes browser.* commands
41
- const gateway = (config.gateway as Record<string, unknown>) ?? {}
42
- const nodes = (gateway.nodes as Record<string, unknown>) ?? {}
43
- const existing: string[] = Array.isArray(nodes.allowCommands)
44
- ? (nodes.allowCommands as string[])
45
- : []
46
-
47
- const existingSet = new Set(existing)
48
- const toAdd = BROWSER_COMMANDS.filter((cmd) => !existingSet.has(cmd))
49
-
50
- if (toAdd.length === 0) {
51
- api.logger.info('node-browser-allowlist: browser commands already allowlisted')
52
- return
53
- }
54
-
55
- nodes.allowCommands = [...existing, ...toAdd]
56
- gateway.nodes = nodes
57
- config.gateway = gateway
58
-
59
- writeOpenclawConfig(configPath, config)
60
- api.logger.info(`node-browser-allowlist: added ${toAdd.length} browser commands to allowlist`)
61
- })
62
- }