@aspalagin/openclaw-max 0.7.2

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +424 -0
  3. package/SKILL.md +108 -0
  4. package/dist/index.js +17 -0
  5. package/dist/setup-entry.js +8 -0
  6. package/dist/src/accounts.js +148 -0
  7. package/dist/src/actions.js +421 -0
  8. package/dist/src/api.js +553 -0
  9. package/dist/src/callbacks.js +111 -0
  10. package/dist/src/channel-agent-prompt.js +38 -0
  11. package/dist/src/channel-config.js +103 -0
  12. package/dist/src/channel-directory.js +84 -0
  13. package/dist/src/channel-lifecycle.js +219 -0
  14. package/dist/src/channel-outbound.js +133 -0
  15. package/dist/src/channel-policy.js +130 -0
  16. package/dist/src/channel.js +76 -0
  17. package/dist/src/config-schema.js +133 -0
  18. package/dist/src/deliver.js +126 -0
  19. package/dist/src/dispatch.js +186 -0
  20. package/dist/src/format.js +31 -0
  21. package/dist/src/inbound-attachments.js +125 -0
  22. package/dist/src/inbound.js +362 -0
  23. package/dist/src/media-temp.js +92 -0
  24. package/dist/src/model-buttons.js +96 -0
  25. package/dist/src/monitor-types.js +27 -0
  26. package/dist/src/monitor.js +40 -0
  27. package/dist/src/onboarding.js +180 -0
  28. package/dist/src/polling.js +98 -0
  29. package/dist/src/presentation.js +374 -0
  30. package/dist/src/runtime.js +36 -0
  31. package/dist/src/russian-trusted-ca.js +84 -0
  32. package/dist/src/send.js +612 -0
  33. package/dist/src/state.js +111 -0
  34. package/dist/src/sticker-cache.js +26 -0
  35. package/dist/src/stream-draft.js +102 -0
  36. package/dist/src/types.js +5 -0
  37. package/dist/src/webhook-queue.js +215 -0
  38. package/dist/src/webhook-runner.js +233 -0
  39. package/dist/src/webhook.js +196 -0
  40. package/openclaw.plugin.json +263 -0
  41. package/package.json +100 -0
  42. package/scripts/check-cycles.mjs +38 -0
  43. package/scripts/test-api.mjs +169 -0
  44. package/scripts/test-send.mjs +162 -0
  45. package/scripts/update-schema.mjs +48 -0
  46. package/src/accounts.ts +193 -0
  47. package/src/actions.ts +501 -0
  48. package/src/api.ts +758 -0
  49. package/src/callbacks.ts +140 -0
  50. package/src/channel-agent-prompt.ts +47 -0
  51. package/src/channel-config.ts +138 -0
  52. package/src/channel-directory.ts +90 -0
  53. package/src/channel-lifecycle.ts +256 -0
  54. package/src/channel-outbound.ts +159 -0
  55. package/src/channel-policy.ts +185 -0
  56. package/src/channel.ts +99 -0
  57. package/src/config-schema.ts +154 -0
  58. package/src/deliver.ts +168 -0
  59. package/src/dispatch.ts +224 -0
  60. package/src/format.ts +33 -0
  61. package/src/inbound-attachments.ts +152 -0
  62. package/src/inbound.ts +412 -0
  63. package/src/media-temp.ts +103 -0
  64. package/src/model-buttons.ts +150 -0
  65. package/src/monitor-types.ts +61 -0
  66. package/src/monitor.ts +49 -0
  67. package/src/onboarding.ts +215 -0
  68. package/src/polling.ts +125 -0
  69. package/src/presentation.ts +452 -0
  70. package/src/runtime.ts +56 -0
  71. package/src/russian-trusted-ca.ts +86 -0
  72. package/src/send.ts +774 -0
  73. package/src/state.ts +149 -0
  74. package/src/sticker-cache.ts +28 -0
  75. package/src/stream-draft.ts +125 -0
  76. package/src/types.ts +263 -0
  77. package/src/webhook-queue.ts +248 -0
  78. package/src/webhook-runner.ts +296 -0
  79. package/src/webhook.ts +268 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * MessagePresentation → MAX message (text in MAX markdown + inline_keyboard).
3
+ *
4
+ * Contract: docs/plugins/message-presentation.md. Core adapts a presentation to
5
+ * `MAX_PRESENTATION_CAPABILITIES` and calls `renderMaxPresentation`; the result
6
+ * is an ordinary payload with `channelData.max.buttons`, sent by sendPayload.
7
+ * Channel-local funnels (agent replies, the message tool) go through the same
8
+ * `renderPresentationForDelivery` so they behave identically.
9
+ *
10
+ * Presentation actions become transport-private callback payloads (≤1024
11
+ * bytes) that `decodeMaxPresentationCallback` maps back on `message_callback`:
12
+ * - command → the command text itself ("/status"): it re-enters as a user
13
+ * message and takes core's native command path;
14
+ * - callback → "mxcb1:<value>": opaque, delivered to the agent as
15
+ * "callback_data: <value>" and never parsed as a slash command;
16
+ * - approval → "mxa1:<e|p|s>:<o|a|d>:<approvalId>": resolved through the
17
+ * canonical approval service (approval-gateway-runtime);
18
+ * - question → "mxq1:<questionId>:<optionValue>": resolved through
19
+ * question-gateway-runtime;
20
+ * - url / URL-backed web-app → a MAX `link` button.
21
+ */
22
+
23
+ import type { ChannelOutboundAdapter } from 'openclaw/plugin-sdk/channel-contract';
24
+ import type { ReplyPayload } from 'openclaw/plugin-sdk/core';
25
+ import {
26
+ type MessagePresentation,
27
+ type MessagePresentationBlock,
28
+ type MessagePresentationButton,
29
+ type MessagePresentationOption,
30
+ renderMessagePresentationFallbackText,
31
+ renderPresentationForDelivery,
32
+ type ReplyPayloadDelivery,
33
+ resolveMessagePresentationButtonAction,
34
+ resolveMessagePresentationOptionAction,
35
+ } from 'openclaw/plugin-sdk/interactive-runtime';
36
+
37
+ import type { MaxSendButton } from './send.js';
38
+
39
+ type ChannelPresentationCapabilities = NonNullable<
40
+ ChannelOutboundAdapter['presentationCapabilities']
41
+ >;
42
+
43
+ /** MAX Bot API limits (schema.yaml: Button.text, CallbackButton.payload, InlineKeyboard). */
44
+ export const MAX_BUTTON_TEXT_LIMIT = 128;
45
+ export const MAX_CALLBACK_PAYLOAD_BYTES = 1024;
46
+ export const MAX_KEYBOARD_ROWS = 30;
47
+ export const MAX_KEYBOARD_BUTTONS = 210;
48
+ export const MAX_LINK_URL_LENGTH = 2048;
49
+ export const MAX_TEXT_LIMIT = 4000;
50
+ /** Buttons per row we render: within MAX's 7 (3 for link/open_app/request_*). */
51
+ const BUTTONS_PER_ROW = 3;
52
+ const SELECT_OPTIONS_PER_ROW = 2;
53
+
54
+ export const MAX_PRESENTATION_CAPABILITIES: ChannelPresentationCapabilities = {
55
+ supported: true,
56
+ buttons: true,
57
+ selects: true,
58
+ context: true,
59
+ divider: true,
60
+ // Rendered as monospace text blocks (MAX has no native charts/tables).
61
+ charts: true,
62
+ tables: true,
63
+ limits: {
64
+ actions: {
65
+ maxActions: MAX_KEYBOARD_BUTTONS,
66
+ maxActionsPerRow: BUTTONS_PER_ROW,
67
+ maxRows: MAX_KEYBOARD_ROWS,
68
+ maxLabelLength: MAX_BUTTON_TEXT_LIMIT,
69
+ // Leave room for the private envelope prefix inside the 1024-byte payload.
70
+ maxValueBytes: 960,
71
+ supportsStyles: false,
72
+ supportsDisabled: false,
73
+ },
74
+ selects: {
75
+ maxOptions: 20,
76
+ maxLabelLength: MAX_BUTTON_TEXT_LIMIT,
77
+ maxValueBytes: 960,
78
+ },
79
+ text: {
80
+ maxLength: MAX_TEXT_LIMIT,
81
+ encoding: 'characters',
82
+ markdownDialect: 'markdown',
83
+ },
84
+ },
85
+ };
86
+
87
+ // ── Callback envelopes ──
88
+
89
+ const CALLBACK_PREFIX = 'mxcb1:';
90
+ const APPROVAL_PREFIX = 'mxa1:';
91
+ const QUESTION_PREFIX = 'mxq1:';
92
+
93
+ type ApprovalKind = 'exec' | 'plugin' | 'system-agent';
94
+ type ApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
95
+
96
+ const APPROVAL_KIND_CODES: Record<ApprovalKind, string> = {
97
+ exec: 'e',
98
+ plugin: 'p',
99
+ 'system-agent': 's',
100
+ };
101
+ const APPROVAL_DECISION_CODES: Record<ApprovalDecision, string> = {
102
+ 'allow-once': 'o',
103
+ 'allow-always': 'a',
104
+ deny: 'd',
105
+ };
106
+
107
+ export type MaxPresentationCallback =
108
+ | { kind: 'callback'; value: string }
109
+ | { kind: 'approval'; approvalId: string; approvalKind: ApprovalKind; decision: ApprovalDecision }
110
+ | { kind: 'question'; questionId: string; optionValue: string };
111
+
112
+ function fitsPayload(payload: string): boolean {
113
+ return payload.length > 0 && Buffer.byteLength(payload, 'utf8') <= MAX_CALLBACK_PAYLOAD_BYTES;
114
+ }
115
+
116
+ /** Decode a payload produced by this renderer; null for anything else (legacy buttons). */
117
+ export function decodeMaxPresentationCallback(
118
+ payload: string | undefined | null,
119
+ ): MaxPresentationCallback | null {
120
+ if (!payload) return null;
121
+ if (payload.startsWith(CALLBACK_PREFIX)) {
122
+ const value = payload.slice(CALLBACK_PREFIX.length);
123
+ return value ? { kind: 'callback', value } : null;
124
+ }
125
+ if (payload.startsWith(APPROVAL_PREFIX)) {
126
+ const match = /^mxa1:([eps]):([oad]):(.+)$/su.exec(payload);
127
+ if (!match) return null;
128
+ const approvalKind = (Object.keys(APPROVAL_KIND_CODES) as ApprovalKind[]).find(
129
+ (kind) => APPROVAL_KIND_CODES[kind] === match[1],
130
+ );
131
+ const decision = (Object.keys(APPROVAL_DECISION_CODES) as ApprovalDecision[]).find(
132
+ (code) => APPROVAL_DECISION_CODES[code] === match[2],
133
+ );
134
+ return approvalKind && decision
135
+ ? { kind: 'approval', approvalKind, decision, approvalId: match[3] }
136
+ : null;
137
+ }
138
+ if (payload.startsWith(QUESTION_PREFIX)) {
139
+ const rest = payload.slice(QUESTION_PREFIX.length);
140
+ const separator = rest.indexOf(':');
141
+ if (separator <= 0 || separator === rest.length - 1) return null;
142
+ return {
143
+ kind: 'question',
144
+ questionId: rest.slice(0, separator),
145
+ optionValue: rest.slice(separator + 1),
146
+ };
147
+ }
148
+ return null;
149
+ }
150
+
151
+ // ── Controls ──
152
+
153
+ type ControlResult = { button: MaxSendButton } | { dropped: string };
154
+
155
+ function clipLabel(label: string): string {
156
+ const chars = Array.from(label.trim());
157
+ return chars.length <= MAX_BUTTON_TEXT_LIMIT
158
+ ? chars.join('')
159
+ : `${chars.slice(0, MAX_BUTTON_TEXT_LIMIT - 1).join('')}…`;
160
+ }
161
+
162
+ function callbackButton(label: string, payload: string): ControlResult {
163
+ return fitsPayload(payload)
164
+ ? { button: { text: clipLabel(label), type: 'callback', payload } }
165
+ : { dropped: label };
166
+ }
167
+
168
+ function linkButton(label: string, url: string): ControlResult {
169
+ const trimmed = url.trim();
170
+ if (!/^https?:\/\//iu.test(trimmed) || trimmed.length > MAX_LINK_URL_LENGTH)
171
+ return { dropped: label };
172
+ return { button: { text: clipLabel(label), type: 'link', url: trimmed } };
173
+ }
174
+
175
+ function commandPayload(command: string): string {
176
+ const trimmed = command.trim();
177
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
178
+ }
179
+
180
+ function toControl(button: MessagePresentationButton): ControlResult {
181
+ const label = button.label;
182
+ const action = resolveMessagePresentationButtonAction(button);
183
+ if (!action || button.disabled) return { dropped: label };
184
+ switch (action.type) {
185
+ case 'url':
186
+ return linkButton(label, action.url);
187
+ case 'web-app':
188
+ // MAX open_app addresses a bot's mini app by name, not by URL.
189
+ return action.url ? linkButton(label, action.url) : { dropped: label };
190
+ case 'command':
191
+ return action.command.trim()
192
+ ? callbackButton(label, commandPayload(action.command))
193
+ : { dropped: label };
194
+ case 'callback':
195
+ return callbackButton(label, `${CALLBACK_PREFIX}${action.value}`);
196
+ case 'approval': {
197
+ const kind = APPROVAL_KIND_CODES[action.approvalKind as ApprovalKind];
198
+ const decision = APPROVAL_DECISION_CODES[action.decision as ApprovalDecision];
199
+ if (!kind || !decision || !action.approvalId) return { dropped: label };
200
+ return callbackButton(label, `${APPROVAL_PREFIX}${kind}:${decision}:${action.approvalId}`);
201
+ }
202
+ case 'question':
203
+ // custom-input needs a free-text composer target MAX cannot address; the
204
+ // producer states the text route in the message, so the control is omitted.
205
+ if ('intent' in action || action.questionId.includes(':')) return { dropped: label };
206
+ return callbackButton(label, `${QUESTION_PREFIX}${action.questionId}:${action.optionValue}`);
207
+ default:
208
+ return { dropped: label };
209
+ }
210
+ }
211
+
212
+ function toOptionControl(option: MessagePresentationOption): ControlResult {
213
+ const action = resolveMessagePresentationOptionAction(option);
214
+ if (!action) return { dropped: option.label };
215
+ return action.type === 'command'
216
+ ? callbackButton(option.label, commandPayload(action.command))
217
+ : callbackButton(option.label, `${CALLBACK_PREFIX}${action.value}`);
218
+ }
219
+
220
+ // ── Text ──
221
+
222
+ const TONE_PREFIX: Record<string, string> = {
223
+ info: 'ℹ️',
224
+ success: '✅',
225
+ warning: '⚠️',
226
+ danger: '⛔',
227
+ };
228
+
229
+ function collapse(value: string | number): string {
230
+ return String(value).replace(/\s+/gu, ' ').replace(/`/gu, "'").trim();
231
+ }
232
+
233
+ function monospaceTable(headers: string[], rows: string[][]): string {
234
+ const widths = headers.map((header, column) =>
235
+ Math.max(Array.from(header).length, ...rows.map((row) => Array.from(row[column] ?? '').length)),
236
+ );
237
+ const pad = (cells: string[]) =>
238
+ cells
239
+ .map(
240
+ (cell, column) => cell + ' '.repeat(Math.max(0, widths[column] - Array.from(cell).length)),
241
+ )
242
+ .join(' | ')
243
+ .trimEnd();
244
+ const lines = [
245
+ pad(headers),
246
+ widths.map((width) => '-'.repeat(Math.max(1, width))).join('-+-'),
247
+ ...rows.map(pad),
248
+ ];
249
+ return ['```', ...lines, '```'].join('\n');
250
+ }
251
+
252
+ function renderDataBlock(
253
+ block: Extract<MessagePresentationBlock, { type: 'table' | 'chart' }>,
254
+ ): string {
255
+ if (block.type === 'table') {
256
+ return [
257
+ `**${collapse(block.caption)}**`,
258
+ monospaceTable(
259
+ block.headers.map(collapse),
260
+ block.rows.map((row) => row.map(collapse)),
261
+ ),
262
+ ].join('\n');
263
+ }
264
+ if (block.chartType === 'pie') {
265
+ const total = block.segments.reduce((sum, segment) => sum + segment.value, 0);
266
+ const rows = block.segments.map((segment) => [
267
+ collapse(segment.label),
268
+ collapse(segment.value),
269
+ total > 0 ? `${((segment.value / total) * 100).toFixed(1)}%` : '',
270
+ ]);
271
+ return [`**${collapse(block.title)}** (pie)`, monospaceTable(['', '', '%'], rows)].join('\n');
272
+ }
273
+ const headers = [
274
+ collapse(block.xLabel ?? ''),
275
+ ...block.series.map((series) => collapse(series.name)),
276
+ ];
277
+ const rows = block.categories.map((category, index) => [
278
+ collapse(category),
279
+ ...block.series.map((series) => collapse(series.values[index] ?? '')),
280
+ ]);
281
+ const axis = block.yLabel ? `, ${collapse(block.yLabel)}` : '';
282
+ return [
283
+ `**${collapse(block.title)}** (${block.chartType}${axis})`,
284
+ monospaceTable(headers, rows),
285
+ ].join('\n');
286
+ }
287
+
288
+ function renderContext(text: string): string {
289
+ const trimmed = text.trim();
290
+ return /[_*\n]/u.test(trimmed) ? trimmed : `_${trimmed}_`;
291
+ }
292
+
293
+ function chunk<T>(items: T[], size: number): T[][] {
294
+ const rows: T[][] = [];
295
+ for (let index = 0; index < items.length; index += size)
296
+ rows.push(items.slice(index, index + size));
297
+ return rows;
298
+ }
299
+
300
+ export type MaxRenderedPresentation = {
301
+ text: string;
302
+ buttons: MaxSendButton[][];
303
+ };
304
+
305
+ /** Pure mapping of an (already adapted) presentation to MAX text + keyboard rows. */
306
+ export function renderMaxPresentationParts(params: {
307
+ presentation: MessagePresentation;
308
+ text?: string | null;
309
+ }): MaxRenderedPresentation {
310
+ const { presentation } = params;
311
+ const baseText = params.text?.trim() ?? '';
312
+ const parts: string[] = [];
313
+
314
+ const title = presentation.title?.trim();
315
+ if (title && !baseText.startsWith(title)) {
316
+ const tone = presentation.tone ? TONE_PREFIX[presentation.tone] : undefined;
317
+ parts.push(tone ? `${tone} **${title}**` : `**${title}**`);
318
+ }
319
+ if (baseText) parts.push(baseText);
320
+
321
+ const rows: MaxSendButton[][] = [];
322
+ const dropped: string[] = [];
323
+ let buttonCount = 0;
324
+ const pushRows = (candidates: MaxSendButton[][]) => {
325
+ for (const row of candidates) {
326
+ const room = Math.min(row.length, MAX_KEYBOARD_BUTTONS - buttonCount);
327
+ if (rows.length >= MAX_KEYBOARD_ROWS || room <= 0) {
328
+ dropped.push(...row.map((button) => button.text));
329
+ continue;
330
+ }
331
+ rows.push(row.slice(0, room));
332
+ dropped.push(...row.slice(room).map((button) => button.text));
333
+ buttonCount += room;
334
+ }
335
+ };
336
+
337
+ for (const block of presentation.blocks) {
338
+ switch (block.type) {
339
+ case 'text':
340
+ if (block.text.trim()) parts.push(block.text.trim());
341
+ break;
342
+ case 'context':
343
+ if (block.text.trim()) parts.push(renderContext(block.text));
344
+ break;
345
+ case 'divider':
346
+ parts.push('———');
347
+ break;
348
+ case 'table':
349
+ case 'chart':
350
+ parts.push(renderDataBlock(block));
351
+ break;
352
+ case 'buttons': {
353
+ const buttons: MaxSendButton[] = [];
354
+ for (const control of block.buttons.map(toControl)) {
355
+ if ('button' in control) buttons.push(control.button);
356
+ else dropped.push(control.dropped);
357
+ }
358
+ pushRows(chunk(buttons, BUTTONS_PER_ROW));
359
+ break;
360
+ }
361
+ case 'select': {
362
+ const options: MaxSendButton[] = [];
363
+ for (const control of block.options.map(toOptionControl)) {
364
+ if ('button' in control) options.push(control.button);
365
+ else dropped.push(control.dropped);
366
+ }
367
+ pushRows(chunk(options, SELECT_OPTIONS_PER_ROW));
368
+ break;
369
+ }
370
+ }
371
+ }
372
+
373
+ if (dropped.length > 0) {
374
+ // Label-only fallback, as core renders controls a channel cannot carry.
375
+ const fallback = renderMessagePresentationFallbackText({
376
+ presentation: {
377
+ blocks: [
378
+ { type: 'buttons', buttons: dropped.map((label) => ({ label, value: 'unavailable' })) },
379
+ ],
380
+ },
381
+ });
382
+ if (fallback) parts.push(fallback);
383
+ }
384
+
385
+ return { text: parts.join('\n\n'), buttons: rows };
386
+ }
387
+
388
+ function readChannelDataMax(channelData: unknown): Record<string, unknown> {
389
+ if (!channelData || typeof channelData !== 'object' || Array.isArray(channelData)) return {};
390
+ const max = (channelData as Record<string, unknown>).max;
391
+ return max && typeof max === 'object' && !Array.isArray(max)
392
+ ? (max as Record<string, unknown>)
393
+ : {};
394
+ }
395
+
396
+ /** `renderPresentation` for the outbound adapter: presentation → channelData.max.buttons. */
397
+ export function renderMaxPresentation(
398
+ payload: ReplyPayload,
399
+ presentation: MessagePresentation,
400
+ ): ReplyPayload {
401
+ const rendered = renderMaxPresentationParts({ presentation, text: payload.text });
402
+ const existingMax = readChannelDataMax(payload.channelData);
403
+ const existingButtons = Array.isArray(existingMax.buttons)
404
+ ? (existingMax.buttons as MaxSendButton[][])
405
+ : [];
406
+ const buttons = [...existingButtons, ...rendered.buttons];
407
+ const rest: ReplyPayload = { ...payload };
408
+ delete rest.presentation;
409
+ return {
410
+ ...rest,
411
+ text: rendered.text,
412
+ ...(buttons.length > 0 || payload.channelData
413
+ ? {
414
+ channelData: {
415
+ ...(payload.channelData ?? {}),
416
+ max: { ...existingMax, ...(buttons.length > 0 ? { buttons } : {}) },
417
+ },
418
+ }
419
+ : {}),
420
+ };
421
+ }
422
+
423
+ /** Same adaptation + render + fallback policy core applies on its outbound path. */
424
+ export async function materializeMaxPresentation(payload: ReplyPayload): Promise<ReplyPayload> {
425
+ if (!payload.presentation) return payload;
426
+ return renderPresentationForDelivery(
427
+ {
428
+ presentationCapabilities: MAX_PRESENTATION_CAPABILITIES,
429
+ renderPresentation: (adapted) => renderMaxPresentation(adapted, adapted.presentation),
430
+ },
431
+ payload,
432
+ );
433
+ }
434
+
435
+ export type MaxDeliveryPin = { enabled: boolean; notify?: boolean; required?: boolean };
436
+
437
+ /** Normalize `delivery.pin` (true | {enabled, notify, required}) or the tool's `pin: true`. */
438
+ export function readMaxDeliveryPin(
439
+ delivery: unknown,
440
+ pinFlag?: unknown,
441
+ ): MaxDeliveryPin | undefined {
442
+ const raw =
443
+ delivery && typeof delivery === 'object' && !Array.isArray(delivery)
444
+ ? (delivery as ReplyPayloadDelivery).pin
445
+ : undefined;
446
+ if (raw === true) return { enabled: true };
447
+ if (raw && typeof raw === 'object' && raw.enabled) {
448
+ return { enabled: true, notify: raw.notify === true, required: raw.required === true };
449
+ }
450
+ if (pinFlag === true) return { enabled: true };
451
+ return undefined;
452
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { OpenClawConfig, PluginRuntime } from 'openclaw/plugin-sdk/core';
2
+
3
+ let runtime: PluginRuntime | null = null;
4
+
5
+ export function setMaxRuntime(next: PluginRuntime) {
6
+ runtime = next;
7
+ }
8
+
9
+ export function getMaxRuntime(): PluginRuntime {
10
+ if (!runtime) {
11
+ throw new Error('MAX runtime not initialized');
12
+ }
13
+ return runtime;
14
+ }
15
+
16
+ /**
17
+ * Legacy runtime config API (OpenClaw < 2026.9.3).
18
+ * Since 2026.9.3 `loadConfig`/`writeConfigFile` are removed from PluginRuntime;
19
+ * the runtime exposes `config.current()` and transactional `replaceConfigFile`/`mutateConfigFile`.
20
+ */
21
+ type LegacyMaxRuntimeConfigApi = {
22
+ loadConfig?: () => Promise<OpenClawConfig> | OpenClawConfig;
23
+ writeConfigFile?: (cfg: OpenClawConfig) => Promise<unknown> | unknown;
24
+ };
25
+
26
+ /** Read the current config snapshot through the plugin runtime, on any supported OpenClaw version. */
27
+ export async function loadMaxConfig(): Promise<OpenClawConfig> {
28
+ const config = getMaxRuntime().config;
29
+ if (typeof config.current === 'function') {
30
+ return config.current() as OpenClawConfig;
31
+ }
32
+ const legacy = config as unknown as LegacyMaxRuntimeConfigApi;
33
+ if (typeof legacy.loadConfig === 'function') {
34
+ return await legacy.loadConfig();
35
+ }
36
+ throw new Error(
37
+ 'MAX runtime config API unavailable: neither config.current() nor config.loadConfig()',
38
+ );
39
+ }
40
+
41
+ /** Persist a full config replacement through the plugin runtime, on any supported OpenClaw version. */
42
+ export async function writeMaxConfig(nextConfig: OpenClawConfig): Promise<void> {
43
+ const config = getMaxRuntime().config;
44
+ if (typeof config.replaceConfigFile === 'function') {
45
+ await config.replaceConfigFile({ nextConfig, afterWrite: { mode: 'auto' } });
46
+ return;
47
+ }
48
+ const legacy = config as unknown as LegacyMaxRuntimeConfigApi;
49
+ if (typeof legacy.writeConfigFile === 'function') {
50
+ await legacy.writeConfigFile(nextConfig);
51
+ return;
52
+ }
53
+ throw new Error(
54
+ 'MAX runtime config API unavailable: neither config.replaceConfigFile() nor config.writeConfigFile()',
55
+ );
56
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Russian Trusted Root/Sub CA (Минцифры РФ) — required for TLS to platform-api2.max.ru.
3
+ *
4
+ * Source: https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
5
+ * https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt
6
+ * Root CA valid until 2032-02-27, Sub CA until 2027-03-06.
7
+ * These are used ONLY for MAX API connections (per-dispatcher ca), never installed process-wide.
8
+ */
9
+
10
+ export const RUSSIAN_TRUSTED_ROOT_CA = `
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
13
+ PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
14
+ ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
15
+ Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
16
+ VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
17
+ YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
18
+ dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
19
+ qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
20
+ XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
21
+ zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
22
+ YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
23
+ Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
24
+ U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
25
+ 4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
26
+ G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
27
+ BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
28
+ ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
29
+ OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
30
+ BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
31
+ BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
32
+ AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
33
+ tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
34
+ W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
35
+ /3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
36
+ AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
37
+ C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
38
+ 4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
39
+ WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
40
+ D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
41
+ EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
42
+ 391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
43
+ -----END CERTIFICATE-----`;
44
+
45
+ export const RUSSIAN_TRUSTED_SUB_CA = `
46
+ -----BEGIN CERTIFICATE-----
47
+ MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
48
+ PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
49
+ ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
50
+ Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
51
+ VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
52
+ YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
53
+ IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
54
+ wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
55
+ HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
56
+ S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
57
+ vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
58
+ BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
59
+ vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
60
+ L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
61
+ 3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
62
+ qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
63
+ Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
64
+ 2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
65
+ Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
66
+ XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
67
+ AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
68
+ b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
69
+ eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
70
+ aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
71
+ gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
72
+ b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
73
+ L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
74
+ LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
75
+ AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
76
+ Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
77
+ anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
78
+ cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
79
+ Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
80
+ Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
81
+ h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
82
+ F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
83
+ E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
84
+ GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
85
+ ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
86
+ -----END CERTIFICATE-----`;