@femtomc/mu-server 26.2.98 → 26.2.99
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 +32 -49
- package/dist/control_plane.js +6 -166
- package/dist/control_plane_contract.d.ts +3 -2
- package/dist/server.js +21 -13
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# @femtomc/mu-server
|
|
2
2
|
|
|
3
|
-
HTTP API server for mu control-plane infrastructure.
|
|
3
|
+
HTTP API server for mu control-plane infrastructure.
|
|
4
|
+
Powers `mu serve`, messaging frontend transport routes, and
|
|
5
|
+
control-plane/session coordination endpoints.
|
|
4
6
|
|
|
5
|
-
> Scope note: server-routed business query/mutation gateway endpoints have
|
|
7
|
+
> Scope note: server-routed business query/mutation gateway endpoints have
|
|
8
|
+
> been removed. Business reads/writes are CLI-first, while long-lived runtime
|
|
9
|
+
> coordination (runs/heartbeats/cron) stays server-owned.
|
|
6
10
|
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
@@ -141,66 +145,45 @@ Use `mu store paths --pretty` to resolve `<store>` for the active repo/workspace
|
|
|
141
145
|
- `GET /api/control-plane/events`
|
|
142
146
|
- `GET /api/control-plane/events/tail`
|
|
143
147
|
|
|
144
|
-
## Messaging adapter setup (
|
|
148
|
+
## Messaging adapter setup (skills-first)
|
|
145
149
|
|
|
146
|
-
|
|
150
|
+
For first-time channel onboarding, prefer bundled setup skills from `mu`
|
|
151
|
+
(`setup-slack`, `setup-discord`, `setup-telegram`, `setup-neovim`).
|
|
152
|
+
These workflows are agent-first: the agent patches config, reloads control-plane,
|
|
153
|
+
verifies routes/capabilities, collects IDs from audit where possible, and asks users
|
|
154
|
+
only for required external-console steps and secret handoff.
|
|
147
155
|
|
|
148
|
-
|
|
149
|
-
mu store paths --pretty
|
|
150
|
-
mu control status --pretty
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
2) Edit `<store>/config.json` and set adapter secrets:
|
|
154
|
-
|
|
155
|
-
- Slack: `control_plane.adapters.slack.signing_secret`, `bot_token`
|
|
156
|
-
- Discord: `control_plane.adapters.discord.signing_secret`
|
|
157
|
-
- Telegram: `control_plane.adapters.telegram.webhook_secret`, `bot_token`, `bot_username`
|
|
158
|
-
- Neovim: `control_plane.adapters.neovim.shared_secret`
|
|
159
|
-
- Optional operator tuning: `control_plane.operator.timeout_ms` (max wall-time per messaging turn, default `600000`).
|
|
160
|
-
|
|
161
|
-
3) Reload live control-plane runtime:
|
|
156
|
+
Baseline status/verification commands:
|
|
162
157
|
|
|
163
158
|
```bash
|
|
159
|
+
mu control status --pretty
|
|
160
|
+
mu store paths --pretty
|
|
164
161
|
mu control reload
|
|
165
|
-
|
|
162
|
+
curl -s http://localhost:3000/api/control-plane/channels | jq '.channels'
|
|
163
|
+
mu control identities --all --pretty
|
|
166
164
|
```
|
|
167
165
|
|
|
168
|
-
4) Link identities for channel actors (examples):
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
mu control link --channel slack --actor-id U123 --tenant-id T123
|
|
172
|
-
mu control link --channel discord --actor-id <user-id> --tenant-id <guild-id>
|
|
173
|
-
mu control link --channel telegram --actor-id <chat-id> --tenant-id telegram-bot
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
For Neovim, use `:Mu link` in `mu.nvim` after configuring `shared_secret`.
|
|
177
|
-
|
|
178
166
|
## Media support operations checklist
|
|
179
167
|
|
|
180
|
-
When validating attachment support end-to-end
|
|
168
|
+
When validating attachment support end-to-end:
|
|
181
169
|
|
|
182
|
-
1.
|
|
170
|
+
1. Ensure Slack/Telegram bot tokens are configured in `<store>/config.json`.
|
|
183
171
|
2. Reload control-plane (`mu control reload`).
|
|
184
|
-
3. Verify `/api/control-plane/channels` media
|
|
172
|
+
3. Verify `/api/control-plane/channels` media flags:
|
|
185
173
|
- `media.outbound_delivery`
|
|
186
174
|
- `media.inbound_attachment_download`
|
|
187
|
-
4.
|
|
188
|
-
5.
|
|
189
|
-
- Slack media upload
|
|
190
|
-
- Telegram PNG/JPEG/WEBP
|
|
191
|
-
- Telegram SVG/PDF
|
|
192
|
-
|
|
193
|
-
Operational
|
|
194
|
-
|
|
195
|
-
-
|
|
196
|
-
- Telegram text
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
- Awaiting-confirmation envelopes include Telegram inline `Confirm`/`Cancel` callback buttons when interaction metadata provides confirmation actions.
|
|
200
|
-
- Telegram callback payloads are contract-limited to `confirm:<command_id>` and `cancel:<command_id>`; unsupported payloads are explicitly rejected.
|
|
201
|
-
- Callback buttons keep parity with command fallback: `/mu confirm <id>` and `/mu cancel <id>` remain valid.
|
|
202
|
-
- Group/supergroup Telegram freeform text is deterministic no-op with guidance; explicit `/mu ...` commands stay actionable.
|
|
203
|
-
- 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.
|
|
175
|
+
4. Run one text-only turn and verify normal delivery.
|
|
176
|
+
5. Run one attachment-bearing turn and verify channel-specific routing:
|
|
177
|
+
- Slack media upload via `files.upload`
|
|
178
|
+
- Telegram PNG/JPEG/WEBP via `sendPhoto`
|
|
179
|
+
- Telegram SVG/PDF via `sendDocument`
|
|
180
|
+
|
|
181
|
+
Operational fallbacks:
|
|
182
|
+
|
|
183
|
+
- Telegram media upload failure falls back to text `sendMessage`.
|
|
184
|
+
- Telegram long text is deterministically chunked into ordered `sendMessage` calls.
|
|
185
|
+
- `telegram_reply_to_message_id` metadata anchors replies when parseable.
|
|
186
|
+
- Missing Slack/Telegram bot tokens surface capability reason codes (`*_bot_token_missing`) and retry behavior.
|
|
204
187
|
|
|
205
188
|
## Running the Server
|
|
206
189
|
|
package/dist/control_plane.js
CHANGED
|
@@ -161,52 +161,8 @@ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
|
|
|
161
161
|
}
|
|
162
162
|
return chunks;
|
|
163
163
|
}
|
|
164
|
-
function slackBlocksForOutboxRecord(
|
|
165
|
-
|
|
166
|
-
if (!interactionMessage || typeof interactionMessage !== "object") {
|
|
167
|
-
return undefined;
|
|
168
|
-
}
|
|
169
|
-
const rawActions = interactionMessage.actions;
|
|
170
|
-
if (!Array.isArray(rawActions) || rawActions.length === 0) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
const buttons = [];
|
|
174
|
-
for (const action of rawActions) {
|
|
175
|
-
if (!action || typeof action !== "object") {
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
const candidate = action;
|
|
179
|
-
if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
|
|
183
|
-
const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
|
|
184
|
-
if (confirm?.[1]) {
|
|
185
|
-
buttons.push({
|
|
186
|
-
type: "button",
|
|
187
|
-
text: { type: "plain_text", text: candidate.label },
|
|
188
|
-
value: `confirm:${confirm[1]}`,
|
|
189
|
-
action_id: `confirm_${confirm[1]}`,
|
|
190
|
-
});
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
|
|
194
|
-
if (cancel?.[1]) {
|
|
195
|
-
buttons.push({
|
|
196
|
-
type: "button",
|
|
197
|
-
text: { type: "plain_text", text: candidate.label },
|
|
198
|
-
value: `cancel:${cancel[1]}`,
|
|
199
|
-
action_id: `cancel_${cancel[1]}`,
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
if (buttons.length === 0) {
|
|
204
|
-
return undefined;
|
|
205
|
-
}
|
|
206
|
-
return [
|
|
207
|
-
{ type: "section", text: { type: "mrkdwn", text: body } },
|
|
208
|
-
{ type: "actions", elements: buttons },
|
|
209
|
-
];
|
|
164
|
+
function slackBlocksForOutboxRecord(_record, _body) {
|
|
165
|
+
return undefined;
|
|
210
166
|
}
|
|
211
167
|
function slackThreadTsFromMetadata(metadata) {
|
|
212
168
|
const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
|
|
@@ -225,36 +181,8 @@ function slackStatusMessageTsFromMetadata(metadata) {
|
|
|
225
181
|
const trimmed = value.trim();
|
|
226
182
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
227
183
|
}
|
|
228
|
-
function telegramReplyMarkupForOutboxRecord(
|
|
229
|
-
|
|
230
|
-
if (!interactionMessage || typeof interactionMessage !== "object") {
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
const rawActions = interactionMessage.actions;
|
|
234
|
-
if (!Array.isArray(rawActions) || rawActions.length === 0) {
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
const row = [];
|
|
238
|
-
for (const action of rawActions) {
|
|
239
|
-
if (!action || typeof action !== "object") {
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
const candidate = action;
|
|
243
|
-
if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
|
|
247
|
-
const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
|
|
248
|
-
if (confirm?.[1]) {
|
|
249
|
-
row.push({ text: candidate.label, callback_data: `confirm:${confirm[1]}` });
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
|
|
253
|
-
if (cancel?.[1]) {
|
|
254
|
-
row.push({ text: candidate.label, callback_data: `cancel:${cancel[1]}` });
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return row.length > 0 ? { inline_keyboard: [row] } : undefined;
|
|
184
|
+
function telegramReplyMarkupForOutboxRecord(_record) {
|
|
185
|
+
return undefined;
|
|
258
186
|
}
|
|
259
187
|
async function postTelegramMessage(botToken, payload) {
|
|
260
188
|
return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
@@ -710,94 +638,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
710
638
|
pipeline = new ControlPlaneCommandPipeline({
|
|
711
639
|
runtime,
|
|
712
640
|
operator,
|
|
713
|
-
mutationExecutor: async (record) => {
|
|
714
|
-
if (record.target_type === "reload" || record.target_type === "update") {
|
|
715
|
-
if (record.command_args.length > 0) {
|
|
716
|
-
return {
|
|
717
|
-
terminalState: "failed",
|
|
718
|
-
errorCode: "cli_validation_failed",
|
|
719
|
-
trace: {
|
|
720
|
-
cliCommandKind: record.target_type,
|
|
721
|
-
},
|
|
722
|
-
mutatingEvents: [
|
|
723
|
-
{
|
|
724
|
-
eventType: "session.lifecycle.command.failed",
|
|
725
|
-
payload: {
|
|
726
|
-
action: record.target_type,
|
|
727
|
-
reason: "unexpected_args",
|
|
728
|
-
args: record.command_args,
|
|
729
|
-
},
|
|
730
|
-
},
|
|
731
|
-
],
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
const action = record.target_type;
|
|
735
|
-
const executeLifecycleAction = action === "reload" ? opts.sessionLifecycle.reload : opts.sessionLifecycle.update;
|
|
736
|
-
try {
|
|
737
|
-
const lifecycle = await executeLifecycleAction();
|
|
738
|
-
if (!lifecycle.ok) {
|
|
739
|
-
return {
|
|
740
|
-
terminalState: "failed",
|
|
741
|
-
errorCode: "session_lifecycle_failed",
|
|
742
|
-
trace: {
|
|
743
|
-
cliCommandKind: action,
|
|
744
|
-
},
|
|
745
|
-
mutatingEvents: [
|
|
746
|
-
{
|
|
747
|
-
eventType: "session.lifecycle.command.failed",
|
|
748
|
-
payload: {
|
|
749
|
-
action,
|
|
750
|
-
reason: lifecycle.message,
|
|
751
|
-
details: lifecycle.details ?? null,
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
],
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
return {
|
|
758
|
-
terminalState: "completed",
|
|
759
|
-
result: {
|
|
760
|
-
ok: true,
|
|
761
|
-
action,
|
|
762
|
-
message: lifecycle.message,
|
|
763
|
-
details: lifecycle.details ?? null,
|
|
764
|
-
},
|
|
765
|
-
trace: {
|
|
766
|
-
cliCommandKind: action,
|
|
767
|
-
},
|
|
768
|
-
mutatingEvents: [
|
|
769
|
-
{
|
|
770
|
-
eventType: `session.lifecycle.command.${action}`,
|
|
771
|
-
payload: {
|
|
772
|
-
action,
|
|
773
|
-
message: lifecycle.message,
|
|
774
|
-
details: lifecycle.details ?? null,
|
|
775
|
-
},
|
|
776
|
-
},
|
|
777
|
-
],
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
catch (err) {
|
|
781
|
-
return {
|
|
782
|
-
terminalState: "failed",
|
|
783
|
-
errorCode: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
|
|
784
|
-
trace: {
|
|
785
|
-
cliCommandKind: action,
|
|
786
|
-
},
|
|
787
|
-
mutatingEvents: [
|
|
788
|
-
{
|
|
789
|
-
eventType: "session.lifecycle.command.failed",
|
|
790
|
-
payload: {
|
|
791
|
-
action,
|
|
792
|
-
reason: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
|
|
793
|
-
},
|
|
794
|
-
},
|
|
795
|
-
],
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
return null;
|
|
800
|
-
},
|
|
801
641
|
});
|
|
802
642
|
await pipeline.start();
|
|
803
643
|
const telegramManager = new TelegramAdapterGenerationManager({
|
|
@@ -1058,11 +898,11 @@ export async function bootstrapControlPlane(opts) {
|
|
|
1058
898
|
}
|
|
1059
899
|
return result;
|
|
1060
900
|
},
|
|
1061
|
-
async
|
|
901
|
+
async submitAutonomousIngress(autonomousOpts) {
|
|
1062
902
|
if (!pipeline) {
|
|
1063
903
|
throw new Error("control_plane_pipeline_unavailable");
|
|
1064
904
|
}
|
|
1065
|
-
return await pipeline.
|
|
905
|
+
return await pipeline.handleAutonomousIngress(autonomousOpts);
|
|
1066
906
|
},
|
|
1067
907
|
async stop() {
|
|
1068
908
|
wakeDeliveryObserver = null;
|
|
@@ -123,10 +123,11 @@ export type ControlPlaneHandle = {
|
|
|
123
123
|
config: ControlPlaneConfig;
|
|
124
124
|
reason: string;
|
|
125
125
|
}): Promise<TelegramGenerationReloadResult>;
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
submitAutonomousIngress?(opts: {
|
|
127
|
+
text: string;
|
|
128
128
|
repoRoot: string;
|
|
129
129
|
requestId?: string;
|
|
130
|
+
metadata?: Record<string, unknown>;
|
|
130
131
|
}): Promise<CommandPipelineResult>;
|
|
131
132
|
stop(): Promise<void>;
|
|
132
133
|
};
|
package/dist/server.js
CHANGED
|
@@ -74,7 +74,7 @@ function extractWakeTurnReply(turnResult) {
|
|
|
74
74
|
const compact = presented.compact.trim();
|
|
75
75
|
return compact.length > 0 ? compact : null;
|
|
76
76
|
}
|
|
77
|
-
function
|
|
77
|
+
function buildWakeTurnIngressText(opts) {
|
|
78
78
|
const wakeSource = stringField(opts.payload, "wake_source") ?? "unknown";
|
|
79
79
|
const programId = stringField(opts.payload, "program_id") ?? "unknown";
|
|
80
80
|
const title = stringField(opts.payload, "title") ?? "(untitled wake program)";
|
|
@@ -98,7 +98,7 @@ function buildWakeTurnCommandText(opts) {
|
|
|
98
98
|
"",
|
|
99
99
|
`payload=${payloadSnapshot}`,
|
|
100
100
|
"",
|
|
101
|
-
"
|
|
101
|
+
"Respond conversationally with exactly one concise operator message suitable for immediate broadcast.",
|
|
102
102
|
].join("\n");
|
|
103
103
|
}
|
|
104
104
|
export function createContext(repoRoot) {
|
|
@@ -142,7 +142,8 @@ function createServer(options = {}) {
|
|
|
142
142
|
const programId = stringField(opts.payload, "program_id");
|
|
143
143
|
const sourceTsMs = numberField(opts.payload, "source_ts_ms");
|
|
144
144
|
let decision;
|
|
145
|
-
|
|
145
|
+
const autonomousIngress = controlPlaneProxy.submitAutonomousIngress;
|
|
146
|
+
if (typeof autonomousIngress !== "function") {
|
|
146
147
|
decision = {
|
|
147
148
|
outcome: "fallback",
|
|
148
149
|
reason: "control_plane_unavailable",
|
|
@@ -155,14 +156,21 @@ function createServer(options = {}) {
|
|
|
155
156
|
else {
|
|
156
157
|
const turnRequestId = `wake-turn-${wakeId}`;
|
|
157
158
|
try {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
const ingressText = buildWakeTurnIngressText({
|
|
160
|
+
wakeId,
|
|
161
|
+
message: opts.message,
|
|
162
|
+
payload: opts.payload,
|
|
163
|
+
});
|
|
164
|
+
const turnResult = await autonomousIngress({
|
|
165
|
+
text: ingressText,
|
|
164
166
|
repoRoot: context.repoRoot,
|
|
165
167
|
requestId: turnRequestId,
|
|
168
|
+
metadata: {
|
|
169
|
+
wake_id: wakeId,
|
|
170
|
+
wake_source: wakeSource,
|
|
171
|
+
program_id: programId,
|
|
172
|
+
source_ts_ms: sourceTsMs,
|
|
173
|
+
},
|
|
166
174
|
});
|
|
167
175
|
if (turnResult.kind === "noop" || turnResult.kind === "invalid") {
|
|
168
176
|
decision = {
|
|
@@ -404,12 +412,12 @@ function createServer(options = {}) {
|
|
|
404
412
|
const handle = reloadManager.getControlPlaneCurrent();
|
|
405
413
|
handle?.setWakeDeliveryObserver?.(observer ?? null);
|
|
406
414
|
},
|
|
407
|
-
async
|
|
415
|
+
async submitAutonomousIngress(opts) {
|
|
408
416
|
const handle = reloadManager.getControlPlaneCurrent();
|
|
409
|
-
if (
|
|
410
|
-
|
|
417
|
+
if (handle?.submitAutonomousIngress) {
|
|
418
|
+
return await handle.submitAutonomousIngress(opts);
|
|
411
419
|
}
|
|
412
|
-
|
|
420
|
+
throw new Error("control_plane_unavailable");
|
|
413
421
|
},
|
|
414
422
|
async stop() {
|
|
415
423
|
const handle = reloadManager.getControlPlaneCurrent();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@femtomc/mu-server",
|
|
3
|
-
"version": "26.2.
|
|
3
|
+
"version": "26.2.99",
|
|
4
4
|
"description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mu",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"start": "bun run dist/cli.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@femtomc/mu-agent": "26.2.
|
|
34
|
-
"@femtomc/mu-control-plane": "26.2.
|
|
35
|
-
"@femtomc/mu-core": "26.2.
|
|
33
|
+
"@femtomc/mu-agent": "26.2.99",
|
|
34
|
+
"@femtomc/mu-control-plane": "26.2.99",
|
|
35
|
+
"@femtomc/mu-core": "26.2.99"
|
|
36
36
|
}
|
|
37
37
|
}
|