@2en/clawly-plugins 1.28.1 → 1.29.0-beta.1
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/gateway/cron-telemetry.ts +1 -1
- package/gateway/index.ts +4 -2
- package/gateway/message-log.ts +156 -0
- package/gateway/node-dangerous-allowlist.ts +81 -0
- package/gateway/otel.test.ts +9 -1
- package/gateway/otel.ts +9 -4
- package/gateway/plugins.ts +16 -373
- package/gateway/presence.ts +1 -4
- package/gateway/telemetry-config.test.ts +3 -0
- package/gateway/telemetry-config.ts +2 -0
- package/index.ts +3 -0
- package/lib/manualPluginInstall.test.ts +121 -0
- package/lib/manualPluginInstall.ts +148 -0
- package/openclaw.plugin.json +1 -0
- package/outbound.ts +12 -0
- package/package.json +3 -2
- package/skills/read-office-file/SKILL.md +41 -0
- package/tools/clawly-msg-break.ts +32 -0
- package/tools/index.ts +2 -0
- package/gateway/node-browser-allowlist.ts +0 -62
- package/gateway/plugins.test.ts +0 -472
|
@@ -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 {
|
|
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
|
-
|
|
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
|
+
}
|
package/gateway/otel.test.ts
CHANGED
|
@@ -69,7 +69,15 @@ describe('initOtel', () => {
|
|
|
69
69
|
},
|
|
70
70
|
})
|
|
71
71
|
expect(getOtelLogger()).toBeTruthy()
|
|
72
|
-
expect(loggerName).toBe('
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
loggers.clear()
|
|
56
61
|
}
|
|
57
62
|
}
|