@femtomc/mu-server 26.2.90 → 26.2.91

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/README.md CHANGED
@@ -86,7 +86,7 @@ Use `mu store paths --pretty` to resolve `<store>` for the active repo/workspace
86
86
  "patch": {
87
87
  "control_plane": {
88
88
  "adapters": {
89
- "slack": { "signing_secret": "..." }
89
+ "slack": { "signing_secret": "...", "bot_token": "xoxb-..." }
90
90
  },
91
91
  "memory_index": {
92
92
  "enabled": true,
@@ -124,14 +124,7 @@ Use `mu store paths --pretty` to resolve `<store>` for the active repo/workspace
124
124
 
125
125
  ### Control-plane Coordination Endpoints
126
126
 
127
- - Runs:
128
- - `GET /api/control-plane/runs`
129
- - `POST /api/control-plane/runs/start`
130
- - `POST /api/control-plane/runs/resume`
131
- - `POST /api/control-plane/runs/interrupt`
132
- - `GET /api/control-plane/runs/:id`
133
- - `GET /api/control-plane/runs/:id/trace`
134
- - Scheduling + orchestration:
127
+ - Scheduling + coordination:
135
128
  - `GET|POST|PATCH|DELETE /api/heartbeats...`
136
129
  - `GET|POST|PATCH|DELETE /api/cron...`
137
130
  - Heartbeat programs support an optional free-form `prompt` field; when present it becomes the primary wake instruction sent to the operator turn path.
@@ -156,7 +149,7 @@ mu control status --pretty
156
149
 
157
150
  2) Edit `<store>/config.json` and set adapter secrets:
158
151
 
159
- - Slack: `control_plane.adapters.slack.signing_secret`
152
+ - Slack: `control_plane.adapters.slack.signing_secret`, `bot_token`
160
153
  - Discord: `control_plane.adapters.discord.signing_secret`
161
154
  - Telegram: `control_plane.adapters.telegram.webhook_secret`, `bot_token`, `bot_username`
162
155
  - Neovim: `control_plane.adapters.neovim.shared_secret`
@@ -178,6 +171,26 @@ mu control link --channel telegram --actor-id <chat-id> --tenant-id telegram-bot
178
171
 
179
172
  For Neovim, use `:Mu link` in `mu.nvim` after configuring `shared_secret`.
180
173
 
174
+ ## Media support operations checklist
175
+
176
+ When validating attachment support end-to-end, use this sequence:
177
+
178
+ 1. Configure adapter secrets/tokens in `<store>/config.json` (Slack/Telegram need bot tokens for media egress).
179
+ 2. Reload control-plane (`mu control reload`).
180
+ 3. Verify `/api/control-plane/channels` media capability flags:
181
+ - `media.outbound_delivery`
182
+ - `media.inbound_attachment_download`
183
+ 4. Send a text-only control-plane turn and verify normal delivery semantics still hold.
184
+ 5. Send attachment-bearing ingress/outbox payloads and verify channel-specific routing:
185
+ - Slack media upload through `files.upload`
186
+ - Telegram PNG/JPEG/WEBP images through `sendPhoto`
187
+ - Telegram SVG/PDF through `sendDocument`
188
+
189
+ Operational fallback expectations:
190
+
191
+ - If media upload fails, Telegram delivery falls back to text `sendMessage`.
192
+ - If Slack/Telegram bot token is missing, channel capability reason codes should report `*_bot_token_missing` and outbound delivery retries rather than hard-crashing runtime.
193
+
181
194
  ## Running the Server
182
195
 
183
196
  ### With terminal operator session (recommended)
@@ -1,8 +1,7 @@
1
- import { CONTROL_PLANE_CHANNEL_ADAPTER_SPECS } from "@femtomc/mu-control-plane";
1
+ import { CONTROL_PLANE_CHANNEL_ADAPTER_SPECS, ingressModeForValue, summarizeInboundAttachmentPolicy, } from "@femtomc/mu-control-plane";
2
2
  import { configRoutes } from "./config.js";
3
3
  import { eventRoutes } from "./events.js";
4
4
  import { identityRoutes } from "./identities.js";
5
- import { runRoutes } from "./runs.js";
6
5
  import { sessionTurnRoutes } from "./session_turn.js";
7
6
  function asRecord(value) {
8
7
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -35,6 +34,64 @@ function configuredForChannel(config, channel) {
35
34
  return false;
36
35
  }
37
36
  }
37
+ function mediaOutboundCapability(config, channel) {
38
+ switch (channel) {
39
+ case "slack": {
40
+ const configured = typeof config.control_plane.adapters.slack.bot_token === "string";
41
+ return {
42
+ supported: true,
43
+ configured,
44
+ reason: configured ? null : "slack_bot_token_missing",
45
+ };
46
+ }
47
+ case "telegram": {
48
+ const configured = typeof config.control_plane.adapters.telegram.bot_token === "string";
49
+ return {
50
+ supported: true,
51
+ configured,
52
+ reason: configured ? null : "telegram_bot_token_missing",
53
+ };
54
+ }
55
+ default:
56
+ return {
57
+ supported: false,
58
+ configured: false,
59
+ reason: "channel_media_delivery_unsupported",
60
+ };
61
+ }
62
+ }
63
+ function mediaInboundAttachmentCapability(config, channel) {
64
+ if (channel !== "slack" && channel !== "telegram") {
65
+ return {
66
+ supported: false,
67
+ configured: false,
68
+ reason: "channel_attachment_ingress_unsupported",
69
+ };
70
+ }
71
+ const channelModes = summarizeInboundAttachmentPolicy().channel_modes;
72
+ const policyEnabled = channelModes[channel] === "enabled";
73
+ if (!policyEnabled) {
74
+ return {
75
+ supported: true,
76
+ configured: false,
77
+ reason: "inbound_attachment_channel_disabled",
78
+ };
79
+ }
80
+ if (channel === "slack") {
81
+ const hasToken = typeof config.control_plane.adapters.slack.bot_token === "string";
82
+ return {
83
+ supported: true,
84
+ configured: hasToken,
85
+ reason: hasToken ? null : "slack_bot_token_missing",
86
+ };
87
+ }
88
+ const hasToken = typeof config.control_plane.adapters.telegram.bot_token === "string";
89
+ return {
90
+ supported: true,
91
+ configured: hasToken,
92
+ reason: hasToken ? null : "telegram_bot_token_missing",
93
+ };
94
+ }
38
95
  export async function controlPlaneRoutes(request, url, deps, headers) {
39
96
  const path = url.pathname;
40
97
  if (path === "/api/control-plane" || path === "/api/control-plane/status") {
@@ -99,9 +156,14 @@ export async function controlPlaneRoutes(request, url, deps, headers) {
99
156
  ack_format: spec.ack_format,
100
157
  delivery_semantics: spec.delivery_semantics,
101
158
  deferred_delivery: spec.deferred_delivery,
159
+ ingress_mode: ingressModeForValue(spec.channel),
102
160
  configured: configuredForChannel(config, spec.channel),
103
161
  active: activeChannels.has(spec.channel),
104
162
  frontend: spec.channel === "neovim",
163
+ media: {
164
+ outbound_delivery: mediaOutboundCapability(config, spec.channel),
165
+ inbound_attachment_download: mediaInboundAttachmentCapability(config, spec.channel),
166
+ },
105
167
  }));
106
168
  return Response.json({
107
169
  ok: true,
@@ -109,9 +171,6 @@ export async function controlPlaneRoutes(request, url, deps, headers) {
109
171
  channels,
110
172
  }, { headers });
111
173
  }
112
- if (path === "/api/control-plane/runs" || path.startsWith("/api/control-plane/runs/")) {
113
- return runRoutes(request, url, deps, headers);
114
- }
115
174
  if (path === "/api/control-plane/turn") {
116
175
  return sessionTurnRoutes(request, url, deps, headers);
117
176
  }
package/dist/config.d.ts CHANGED
@@ -4,6 +4,7 @@ export type MuConfig = {
4
4
  adapters: {
5
5
  slack: {
6
6
  signing_secret: string | null;
7
+ bot_token: string | null;
7
8
  };
8
9
  discord: {
9
10
  signing_secret: string | null;
@@ -19,7 +20,6 @@ export type MuConfig = {
19
20
  };
20
21
  operator: {
21
22
  enabled: boolean;
22
- run_triggers_enabled: boolean;
23
23
  provider: string | null;
24
24
  model: string | null;
25
25
  thinking: string | null;
@@ -35,6 +35,7 @@ export type MuConfigPatch = {
35
35
  adapters?: {
36
36
  slack?: {
37
37
  signing_secret?: string | null;
38
+ bot_token?: string | null;
38
39
  };
39
40
  discord?: {
40
41
  signing_secret?: string | null;
@@ -50,7 +51,6 @@ export type MuConfigPatch = {
50
51
  };
51
52
  operator?: {
52
53
  enabled?: boolean;
53
- run_triggers_enabled?: boolean;
54
54
  provider?: string | null;
55
55
  model?: string | null;
56
56
  thinking?: string | null;
@@ -66,6 +66,7 @@ export type MuConfigPresence = {
66
66
  adapters: {
67
67
  slack: {
68
68
  signing_secret: boolean;
69
+ bot_token: boolean;
69
70
  };
70
71
  discord: {
71
72
  signing_secret: boolean;
@@ -81,7 +82,6 @@ export type MuConfigPresence = {
81
82
  };
82
83
  operator: {
83
84
  enabled: boolean;
84
- run_triggers_enabled: boolean;
85
85
  provider: boolean;
86
86
  model: boolean;
87
87
  thinking: boolean;
package/dist/config.js CHANGED
@@ -7,6 +7,7 @@ export const DEFAULT_MU_CONFIG = {
7
7
  adapters: {
8
8
  slack: {
9
9
  signing_secret: null,
10
+ bot_token: null,
10
11
  },
11
12
  discord: {
12
13
  signing_secret: null,
@@ -22,7 +23,6 @@ export const DEFAULT_MU_CONFIG = {
22
23
  },
23
24
  operator: {
24
25
  enabled: true,
25
- run_triggers_enabled: true,
26
26
  provider: null,
27
27
  model: null,
28
28
  thinking: null,
@@ -91,6 +91,9 @@ export function normalizeMuConfig(input) {
91
91
  if (slack && "signing_secret" in slack) {
92
92
  next.control_plane.adapters.slack.signing_secret = normalizeNullableString(slack.signing_secret);
93
93
  }
94
+ if (slack && "bot_token" in slack) {
95
+ next.control_plane.adapters.slack.bot_token = normalizeNullableString(slack.bot_token);
96
+ }
94
97
  const discord = asRecord(adapters.discord);
95
98
  if (discord && "signing_secret" in discord) {
96
99
  next.control_plane.adapters.discord.signing_secret = normalizeNullableString(discord.signing_secret);
@@ -117,9 +120,6 @@ export function normalizeMuConfig(input) {
117
120
  if ("enabled" in operator) {
118
121
  next.control_plane.operator.enabled = normalizeBoolean(operator.enabled, next.control_plane.operator.enabled);
119
122
  }
120
- if ("run_triggers_enabled" in operator) {
121
- next.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, next.control_plane.operator.run_triggers_enabled);
122
- }
123
123
  if ("provider" in operator) {
124
124
  next.control_plane.operator.provider = normalizeNullableString(operator.provider);
125
125
  }
@@ -154,10 +154,17 @@ function normalizeMuConfigPatch(input) {
154
154
  if (adapters) {
155
155
  patch.control_plane.adapters = {};
156
156
  const slack = asRecord(adapters.slack);
157
- if (slack && "signing_secret" in slack) {
158
- patch.control_plane.adapters.slack = {
159
- signing_secret: normalizeNullableString(slack.signing_secret),
160
- };
157
+ if (slack) {
158
+ const slackPatch = {};
159
+ if ("signing_secret" in slack) {
160
+ slackPatch.signing_secret = normalizeNullableString(slack.signing_secret);
161
+ }
162
+ if ("bot_token" in slack) {
163
+ slackPatch.bot_token = normalizeNullableString(slack.bot_token);
164
+ }
165
+ if (Object.keys(slackPatch).length > 0) {
166
+ patch.control_plane.adapters.slack = slackPatch;
167
+ }
161
168
  }
162
169
  const discord = asRecord(adapters.discord);
163
170
  if (discord && "signing_secret" in discord) {
@@ -198,9 +205,6 @@ function normalizeMuConfigPatch(input) {
198
205
  if ("enabled" in operator) {
199
206
  patch.control_plane.operator.enabled = normalizeBoolean(operator.enabled, DEFAULT_MU_CONFIG.control_plane.operator.enabled);
200
207
  }
201
- if ("run_triggers_enabled" in operator) {
202
- patch.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, DEFAULT_MU_CONFIG.control_plane.operator.run_triggers_enabled);
203
- }
204
208
  if ("provider" in operator) {
205
209
  patch.control_plane.operator.provider = normalizeNullableString(operator.provider);
206
210
  }
@@ -249,6 +253,9 @@ export function applyMuConfigPatch(base, patchInput) {
249
253
  if (adapters.slack && "signing_secret" in adapters.slack) {
250
254
  next.control_plane.adapters.slack.signing_secret = adapters.slack.signing_secret ?? null;
251
255
  }
256
+ if (adapters.slack && "bot_token" in adapters.slack) {
257
+ next.control_plane.adapters.slack.bot_token = adapters.slack.bot_token ?? null;
258
+ }
252
259
  if (adapters.discord && "signing_secret" in adapters.discord) {
253
260
  next.control_plane.adapters.discord.signing_secret = adapters.discord.signing_secret ?? null;
254
261
  }
@@ -272,9 +279,6 @@ export function applyMuConfigPatch(base, patchInput) {
272
279
  if ("enabled" in operator && typeof operator.enabled === "boolean") {
273
280
  next.control_plane.operator.enabled = operator.enabled;
274
281
  }
275
- if ("run_triggers_enabled" in operator && typeof operator.run_triggers_enabled === "boolean") {
276
- next.control_plane.operator.run_triggers_enabled = operator.run_triggers_enabled;
277
- }
278
282
  if ("provider" in operator) {
279
283
  next.control_plane.operator.provider = operator.provider ?? null;
280
284
  }
@@ -334,6 +338,7 @@ function redacted(value) {
334
338
  export function redactMuConfigSecrets(config) {
335
339
  const next = normalizeMuConfig(config);
336
340
  next.control_plane.adapters.slack.signing_secret = redacted(next.control_plane.adapters.slack.signing_secret);
341
+ next.control_plane.adapters.slack.bot_token = redacted(next.control_plane.adapters.slack.bot_token);
337
342
  next.control_plane.adapters.discord.signing_secret = redacted(next.control_plane.adapters.discord.signing_secret);
338
343
  next.control_plane.adapters.telegram.webhook_secret = redacted(next.control_plane.adapters.telegram.webhook_secret);
339
344
  next.control_plane.adapters.telegram.bot_token = redacted(next.control_plane.adapters.telegram.bot_token);
@@ -349,6 +354,7 @@ export function muConfigPresence(config) {
349
354
  adapters: {
350
355
  slack: {
351
356
  signing_secret: isPresent(config.control_plane.adapters.slack.signing_secret),
357
+ bot_token: isPresent(config.control_plane.adapters.slack.bot_token),
352
358
  },
353
359
  discord: {
354
360
  signing_secret: isPresent(config.control_plane.adapters.discord.signing_secret),
@@ -364,7 +370,6 @@ export function muConfigPresence(config) {
364
370
  },
365
371
  operator: {
366
372
  enabled: config.control_plane.operator.enabled,
367
- run_triggers_enabled: config.control_plane.operator.run_triggers_enabled,
368
373
  provider: isPresent(config.control_plane.operator.provider),
369
374
  model: isPresent(config.control_plane.operator.model),
370
375
  thinking: isPresent(config.control_plane.operator.thinking),
@@ -1,7 +1,6 @@
1
1
  import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
2
- import { type GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
3
- import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type InterRootQueuePolicy, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
4
- import { type ControlPlaneRunSupervisorOpts } from "./run_supervisor.js";
2
+ import { type GenerationTelemetryRecorder, type OutboxDeliveryHandlerResult, type OutboxRecord } from "@femtomc/mu-control-plane";
3
+ import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
5
4
  import { detectAdapters } from "./control_plane_adapter_registry.js";
6
5
  export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
7
6
  export { detectAdapters };
@@ -11,6 +10,16 @@ export type TelegramSendMessagePayload = {
11
10
  parse_mode?: "Markdown";
12
11
  disable_web_page_preview?: boolean;
13
12
  };
13
+ export type TelegramSendPhotoPayload = {
14
+ chat_id: string;
15
+ photo: string;
16
+ caption?: string;
17
+ };
18
+ export type TelegramSendDocumentPayload = {
19
+ chat_id: string;
20
+ document: string;
21
+ caption?: string;
22
+ };
14
23
  /**
15
24
  * Telegram supports a markdown dialect that uses single markers for emphasis.
16
25
  * Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
@@ -23,18 +32,24 @@ export declare function buildTelegramSendMessagePayload(opts: {
23
32
  text: string;
24
33
  richFormatting: boolean;
25
34
  }): TelegramSendMessagePayload;
35
+ export declare function deliverTelegramOutboxRecord(opts: {
36
+ botToken: string;
37
+ record: OutboxRecord;
38
+ }): Promise<OutboxDeliveryHandlerResult>;
39
+ export declare function deliverSlackOutboxRecord(opts: {
40
+ botToken: string;
41
+ record: OutboxRecord;
42
+ }): Promise<OutboxDeliveryHandlerResult>;
26
43
  export type BootstrapControlPlaneOpts = {
27
44
  repoRoot: string;
28
45
  config?: ControlPlaneConfig;
29
46
  operatorRuntime?: MessagingOperatorRuntime | null;
30
47
  operatorBackend?: MessagingOperatorBackend;
31
- runSupervisorSpawnProcess?: ControlPlaneRunSupervisorOpts["spawnProcess"];
32
48
  sessionLifecycle: ControlPlaneSessionLifecycle;
33
49
  generation?: ControlPlaneGenerationContext;
34
50
  telemetry?: GenerationTelemetryRecorder | null;
35
51
  telegramGenerationHooks?: TelegramGenerationSwapHooks;
36
52
  wakeDeliveryObserver?: WakeDeliveryObserver | null;
37
53
  terminalEnabled?: boolean;
38
- interRootQueuePolicy?: InterRootQueuePolicy;
39
54
  };
40
55
  export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;