@femtomc/mu-server 26.2.70 → 26.2.71
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/dist/api/activities.d.ts +2 -0
- package/dist/api/activities.js +160 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +45 -0
- package/dist/api/control_plane.d.ts +2 -0
- package/dist/api/control_plane.js +28 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +182 -0
- package/dist/api/heartbeats.d.ts +2 -0
- package/dist/api/heartbeats.js +211 -0
- package/dist/api/identities.d.ts +2 -0
- package/dist/api/identities.js +103 -0
- package/dist/api/runs.d.ts +2 -0
- package/dist/api/runs.js +207 -0
- package/dist/cli.js +58 -3
- package/dist/config.d.ts +4 -21
- package/dist/config.js +24 -75
- package/dist/control_plane.d.ts +4 -2
- package/dist/control_plane.js +226 -25
- package/dist/control_plane_bootstrap_helpers.d.ts +2 -1
- package/dist/control_plane_bootstrap_helpers.js +11 -1
- package/dist/control_plane_contract.d.ts +57 -0
- package/dist/control_plane_contract.js +1 -1
- package/dist/control_plane_reload.d.ts +63 -0
- package/dist/control_plane_reload.js +525 -0
- package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
- package/dist/control_plane_run_queue_coordinator.js +327 -0
- package/dist/control_plane_telegram_generation.js +0 -1
- package/dist/control_plane_wake_delivery.d.ts +50 -0
- package/dist/control_plane_wake_delivery.js +123 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/run_queue.d.ts +95 -0
- package/dist/run_queue.js +817 -0
- package/dist/run_supervisor.d.ts +20 -0
- package/dist/run_supervisor.js +25 -1
- package/dist/server.d.ts +5 -10
- package/dist/server.js +337 -528
- package/dist/server_program_orchestration.js +2 -0
- package/dist/server_routing.d.ts +3 -2
- package/dist/server_routing.js +28 -900
- package/package.json +7 -6
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getControlPlanePaths, IdentityStore, ROLE_SCOPES, } from "@femtomc/mu-control-plane";
|
|
2
|
+
export async function identityRoutes(request, url, deps, headers) {
|
|
3
|
+
const path = url.pathname;
|
|
4
|
+
const cpPaths = getControlPlanePaths(deps.context.repoRoot);
|
|
5
|
+
const identityStore = new IdentityStore(cpPaths.identitiesPath);
|
|
6
|
+
await identityStore.load();
|
|
7
|
+
if (path === "/api/identities") {
|
|
8
|
+
if (request.method !== "GET") {
|
|
9
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
10
|
+
}
|
|
11
|
+
const includeInactive = url.searchParams.get("include_inactive")?.trim().toLowerCase() === "true";
|
|
12
|
+
const bindings = identityStore.listBindings({ includeInactive });
|
|
13
|
+
return Response.json({ count: bindings.length, bindings }, { headers });
|
|
14
|
+
}
|
|
15
|
+
if (path === "/api/identities/link") {
|
|
16
|
+
if (request.method !== "POST") {
|
|
17
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
18
|
+
}
|
|
19
|
+
let body;
|
|
20
|
+
try {
|
|
21
|
+
body = (await request.json());
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
25
|
+
}
|
|
26
|
+
const channel = typeof body.channel === "string" ? body.channel.trim() : "";
|
|
27
|
+
if (!channel || (channel !== "slack" && channel !== "discord" && channel !== "telegram")) {
|
|
28
|
+
return Response.json({ error: "channel is required (slack, discord, telegram)" }, { status: 400, headers });
|
|
29
|
+
}
|
|
30
|
+
const actorId = typeof body.actor_id === "string" ? body.actor_id.trim() : "";
|
|
31
|
+
if (!actorId) {
|
|
32
|
+
return Response.json({ error: "actor_id is required" }, { status: 400, headers });
|
|
33
|
+
}
|
|
34
|
+
const tenantId = typeof body.tenant_id === "string" ? body.tenant_id.trim() : "";
|
|
35
|
+
if (!tenantId) {
|
|
36
|
+
return Response.json({ error: "tenant_id is required" }, { status: 400, headers });
|
|
37
|
+
}
|
|
38
|
+
const roleKey = typeof body.role === "string" ? body.role.trim() : "operator";
|
|
39
|
+
const roleScopes = ROLE_SCOPES[roleKey];
|
|
40
|
+
if (!roleScopes) {
|
|
41
|
+
return Response.json({ error: `invalid role: ${roleKey} (operator, contributor, viewer)` }, { status: 400, headers });
|
|
42
|
+
}
|
|
43
|
+
const bindingId = typeof body.binding_id === "string" && body.binding_id.trim().length > 0
|
|
44
|
+
? body.binding_id.trim()
|
|
45
|
+
: `bind-${crypto.randomUUID()}`;
|
|
46
|
+
const operatorId = typeof body.operator_id === "string" && body.operator_id.trim().length > 0
|
|
47
|
+
? body.operator_id.trim()
|
|
48
|
+
: "default";
|
|
49
|
+
const decision = await identityStore.link({
|
|
50
|
+
bindingId,
|
|
51
|
+
operatorId,
|
|
52
|
+
channel: channel,
|
|
53
|
+
channelTenantId: tenantId,
|
|
54
|
+
channelActorId: actorId,
|
|
55
|
+
scopes: [...roleScopes],
|
|
56
|
+
});
|
|
57
|
+
switch (decision.kind) {
|
|
58
|
+
case "linked":
|
|
59
|
+
return Response.json({ ok: true, kind: "linked", binding: decision.binding }, { status: 201, headers });
|
|
60
|
+
case "binding_exists":
|
|
61
|
+
return Response.json({ ok: false, kind: "binding_exists", binding: decision.binding }, { status: 409, headers });
|
|
62
|
+
case "principal_already_linked":
|
|
63
|
+
return Response.json({ ok: false, kind: "principal_already_linked", binding: decision.binding }, { status: 409, headers });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (path === "/api/identities/unlink") {
|
|
67
|
+
if (request.method !== "POST") {
|
|
68
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
69
|
+
}
|
|
70
|
+
let body;
|
|
71
|
+
try {
|
|
72
|
+
body = (await request.json());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
76
|
+
}
|
|
77
|
+
const bindingId = typeof body.binding_id === "string" ? body.binding_id.trim() : "";
|
|
78
|
+
if (!bindingId) {
|
|
79
|
+
return Response.json({ error: "binding_id is required" }, { status: 400, headers });
|
|
80
|
+
}
|
|
81
|
+
const actorBindingId = typeof body.actor_binding_id === "string" ? body.actor_binding_id.trim() : "";
|
|
82
|
+
if (!actorBindingId) {
|
|
83
|
+
return Response.json({ error: "actor_binding_id is required" }, { status: 400, headers });
|
|
84
|
+
}
|
|
85
|
+
const reason = typeof body.reason === "string" ? body.reason.trim() : null;
|
|
86
|
+
const decision = await identityStore.unlinkSelf({
|
|
87
|
+
bindingId,
|
|
88
|
+
actorBindingId,
|
|
89
|
+
reason: reason || null,
|
|
90
|
+
});
|
|
91
|
+
switch (decision.kind) {
|
|
92
|
+
case "unlinked":
|
|
93
|
+
return Response.json({ ok: true, kind: "unlinked", binding: decision.binding }, { headers });
|
|
94
|
+
case "not_found":
|
|
95
|
+
return Response.json({ ok: false, kind: "not_found" }, { status: 404, headers });
|
|
96
|
+
case "invalid_actor":
|
|
97
|
+
return Response.json({ ok: false, kind: "invalid_actor" }, { status: 403, headers });
|
|
98
|
+
case "already_inactive":
|
|
99
|
+
return Response.json({ ok: false, kind: "already_inactive", binding: decision.binding }, { status: 409, headers });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
103
|
+
}
|
package/dist/api/runs.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { normalizeWakeMode } from "../server_types.js";
|
|
2
|
+
export async function runRoutes(request, url, deps, headers) {
|
|
3
|
+
const path = url.pathname;
|
|
4
|
+
if (path === "/api/runs") {
|
|
5
|
+
if (request.method !== "GET") {
|
|
6
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
7
|
+
}
|
|
8
|
+
const status = url.searchParams.get("status")?.trim() || undefined;
|
|
9
|
+
const limitRaw = url.searchParams.get("limit");
|
|
10
|
+
const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
|
|
11
|
+
const runs = await deps.controlPlaneProxy.listRuns?.({ status, limit });
|
|
12
|
+
return Response.json({ count: runs?.length ?? 0, runs: runs ?? [] }, { headers });
|
|
13
|
+
}
|
|
14
|
+
if (path === "/api/runs/start") {
|
|
15
|
+
if (request.method !== "POST") {
|
|
16
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
17
|
+
}
|
|
18
|
+
let body;
|
|
19
|
+
try {
|
|
20
|
+
body = (await request.json());
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
24
|
+
}
|
|
25
|
+
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
|
26
|
+
if (prompt.length === 0) {
|
|
27
|
+
return Response.json({ error: "prompt is required" }, { status: 400, headers });
|
|
28
|
+
}
|
|
29
|
+
const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
30
|
+
? Math.max(1, Math.trunc(body.max_steps))
|
|
31
|
+
: undefined;
|
|
32
|
+
try {
|
|
33
|
+
const run = await deps.controlPlaneProxy.startRun?.({ prompt, maxSteps });
|
|
34
|
+
if (!run) {
|
|
35
|
+
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
36
|
+
}
|
|
37
|
+
await deps.registerAutoRunHeartbeatProgram(run).catch(async (error) => {
|
|
38
|
+
await deps.context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
39
|
+
source: "mu-server.runs",
|
|
40
|
+
payload: {
|
|
41
|
+
action: "register_failed",
|
|
42
|
+
run_job_id: run.job_id,
|
|
43
|
+
error: deps.describeError(error),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return Response.json({ ok: true, run }, { status: 201, headers });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return Response.json({ error: deps.describeError(err) }, { status: 500, headers });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (path === "/api/runs/resume") {
|
|
54
|
+
if (request.method !== "POST") {
|
|
55
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
56
|
+
}
|
|
57
|
+
let body;
|
|
58
|
+
try {
|
|
59
|
+
body = (await request.json());
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
63
|
+
}
|
|
64
|
+
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
|
|
65
|
+
if (rootIssueId.length === 0) {
|
|
66
|
+
return Response.json({ error: "root_issue_id is required" }, { status: 400, headers });
|
|
67
|
+
}
|
|
68
|
+
const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
|
|
69
|
+
? Math.max(1, Math.trunc(body.max_steps))
|
|
70
|
+
: undefined;
|
|
71
|
+
try {
|
|
72
|
+
const run = await deps.controlPlaneProxy.resumeRun?.({ rootIssueId, maxSteps });
|
|
73
|
+
if (!run) {
|
|
74
|
+
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
75
|
+
}
|
|
76
|
+
await deps.registerAutoRunHeartbeatProgram(run).catch(async (error) => {
|
|
77
|
+
await deps.context.eventLog.emit("run.auto_heartbeat.lifecycle", {
|
|
78
|
+
source: "mu-server.runs",
|
|
79
|
+
payload: {
|
|
80
|
+
action: "register_failed",
|
|
81
|
+
run_job_id: run.job_id,
|
|
82
|
+
error: deps.describeError(error),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
return Response.json({ ok: true, run }, { status: 201, headers });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return Response.json({ error: deps.describeError(err) }, { status: 500, headers });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (path === "/api/runs/interrupt") {
|
|
93
|
+
if (request.method !== "POST") {
|
|
94
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
95
|
+
}
|
|
96
|
+
let body;
|
|
97
|
+
try {
|
|
98
|
+
body = (await request.json());
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
102
|
+
}
|
|
103
|
+
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
|
|
104
|
+
const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
|
|
105
|
+
const result = await deps.controlPlaneProxy.interruptRun?.({
|
|
106
|
+
rootIssueId,
|
|
107
|
+
jobId,
|
|
108
|
+
});
|
|
109
|
+
if (!result) {
|
|
110
|
+
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
111
|
+
}
|
|
112
|
+
if (!result.ok && result.reason === "not_running" && result.run) {
|
|
113
|
+
await deps.disableAutoRunHeartbeatProgram({
|
|
114
|
+
jobId: result.run.job_id,
|
|
115
|
+
status: result.run.status,
|
|
116
|
+
reason: "interrupt_not_running",
|
|
117
|
+
}).catch(() => {
|
|
118
|
+
// best effort cleanup only
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return Response.json(result, { status: result.ok ? 200 : 404, headers });
|
|
122
|
+
}
|
|
123
|
+
if (path === "/api/runs/heartbeat") {
|
|
124
|
+
if (request.method !== "POST") {
|
|
125
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
126
|
+
}
|
|
127
|
+
let body;
|
|
128
|
+
try {
|
|
129
|
+
body = (await request.json());
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return Response.json({ error: "invalid json body" }, { status: 400, headers });
|
|
133
|
+
}
|
|
134
|
+
const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
|
|
135
|
+
const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
|
|
136
|
+
const reason = typeof body.reason === "string" ? body.reason.trim() : null;
|
|
137
|
+
const wakeMode = normalizeWakeMode(body.wake_mode);
|
|
138
|
+
const result = await deps.controlPlaneProxy.heartbeatRun?.({
|
|
139
|
+
rootIssueId,
|
|
140
|
+
jobId,
|
|
141
|
+
reason,
|
|
142
|
+
wakeMode,
|
|
143
|
+
});
|
|
144
|
+
if (!result) {
|
|
145
|
+
return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
|
|
146
|
+
}
|
|
147
|
+
if (!result.ok && result.reason === "not_running" && result.run) {
|
|
148
|
+
await deps.disableAutoRunHeartbeatProgram({
|
|
149
|
+
jobId: result.run.job_id,
|
|
150
|
+
status: result.run.status,
|
|
151
|
+
reason: "run_not_running",
|
|
152
|
+
}).catch(() => {
|
|
153
|
+
// best effort cleanup only
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (result.ok) {
|
|
157
|
+
return Response.json(result, { status: 200, headers });
|
|
158
|
+
}
|
|
159
|
+
if (result.reason === "missing_target") {
|
|
160
|
+
return Response.json(result, { status: 400, headers });
|
|
161
|
+
}
|
|
162
|
+
if (result.reason === "not_running") {
|
|
163
|
+
return Response.json(result, { status: 409, headers });
|
|
164
|
+
}
|
|
165
|
+
return Response.json(result, { status: 404, headers });
|
|
166
|
+
}
|
|
167
|
+
if (path.startsWith("/api/runs/")) {
|
|
168
|
+
const rest = path.slice("/api/runs/".length);
|
|
169
|
+
const [rawId, maybeSub] = rest.split("/");
|
|
170
|
+
const idOrRoot = decodeURIComponent(rawId ?? "").trim();
|
|
171
|
+
if (idOrRoot.length === 0) {
|
|
172
|
+
return Response.json({ error: "missing run id" }, { status: 400, headers });
|
|
173
|
+
}
|
|
174
|
+
if (maybeSub === "trace") {
|
|
175
|
+
if (request.method !== "GET") {
|
|
176
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
177
|
+
}
|
|
178
|
+
const limitRaw = url.searchParams.get("limit");
|
|
179
|
+
const limit = limitRaw && /^\d+$/.test(limitRaw)
|
|
180
|
+
? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
|
|
181
|
+
: undefined;
|
|
182
|
+
const trace = await deps.controlPlaneProxy.traceRun?.({ idOrRoot, limit });
|
|
183
|
+
if (!trace) {
|
|
184
|
+
return Response.json({ error: "run trace not found" }, { status: 404, headers });
|
|
185
|
+
}
|
|
186
|
+
return Response.json(trace, { headers });
|
|
187
|
+
}
|
|
188
|
+
if (request.method !== "GET") {
|
|
189
|
+
return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
|
|
190
|
+
}
|
|
191
|
+
const run = await deps.controlPlaneProxy.getRun?.(idOrRoot);
|
|
192
|
+
if (!run) {
|
|
193
|
+
return Response.json({ error: "run not found" }, { status: 404, headers });
|
|
194
|
+
}
|
|
195
|
+
if (run.status !== "running") {
|
|
196
|
+
await deps.disableAutoRunHeartbeatProgram({
|
|
197
|
+
jobId: run.job_id,
|
|
198
|
+
status: run.status,
|
|
199
|
+
reason: "run_terminal_snapshot",
|
|
200
|
+
}).catch(() => {
|
|
201
|
+
// best effort cleanup only
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
return Response.json(run, { headers });
|
|
205
|
+
}
|
|
206
|
+
return Response.json({ error: "Not Found" }, { status: 404, headers });
|
|
207
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,19 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { findRepoRoot } from "@femtomc/mu-core/node";
|
|
3
5
|
import { composeServerRuntime, createServerFromRuntime } from "./server.js";
|
|
4
|
-
|
|
6
|
+
// Parse CLI flags: --port N, --repo-root PATH
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
let port = parseInt(Bun.env.PORT || "3000", 10);
|
|
9
|
+
let repoRoot = null;
|
|
10
|
+
for (let i = 0; i < argv.length; i++) {
|
|
11
|
+
const arg = argv[i];
|
|
12
|
+
if (arg === "--port" && i + 1 < argv.length) {
|
|
13
|
+
port = parseInt(argv[++i], 10);
|
|
14
|
+
}
|
|
15
|
+
else if (arg.startsWith("--port=")) {
|
|
16
|
+
port = parseInt(arg.slice("--port=".length), 10);
|
|
17
|
+
}
|
|
18
|
+
else if (arg === "--repo-root" && i + 1 < argv.length) {
|
|
19
|
+
repoRoot = argv[++i];
|
|
20
|
+
}
|
|
21
|
+
else if (arg.startsWith("--repo-root=")) {
|
|
22
|
+
repoRoot = arg.slice("--repo-root=".length);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { port, repoRoot };
|
|
26
|
+
}
|
|
27
|
+
const args = parseArgs(process.argv.slice(2));
|
|
5
28
|
let repoRoot;
|
|
6
29
|
try {
|
|
7
|
-
repoRoot = findRepoRoot();
|
|
30
|
+
repoRoot = args.repoRoot ?? findRepoRoot();
|
|
8
31
|
}
|
|
9
32
|
catch {
|
|
10
33
|
console.error("Error: Could not find .mu directory. Run 'mu serve' or 'mu run' once to initialize it.");
|
|
11
34
|
process.exit(1);
|
|
12
35
|
}
|
|
36
|
+
const port = args.port;
|
|
37
|
+
const discoveryPath = join(repoRoot, ".mu", "control-plane", "server.json");
|
|
13
38
|
console.log(`Starting mu-server on port ${port}...`);
|
|
14
39
|
console.log(`Repository root: ${repoRoot}`);
|
|
15
40
|
const runtime = await composeServerRuntime({ repoRoot });
|
|
16
|
-
const
|
|
41
|
+
const initiateShutdown = async () => {
|
|
42
|
+
console.log("Shutdown initiated via API");
|
|
43
|
+
try {
|
|
44
|
+
rmSync(discoveryPath, { force: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// best-effort
|
|
48
|
+
}
|
|
49
|
+
await runtime.controlPlane?.stop();
|
|
50
|
+
server.stop();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
};
|
|
53
|
+
const serverConfig = createServerFromRuntime(runtime, { port, initiateShutdown });
|
|
17
54
|
let server;
|
|
18
55
|
try {
|
|
19
56
|
server = Bun.serve(serverConfig);
|
|
@@ -27,6 +64,18 @@ catch (err) {
|
|
|
27
64
|
}
|
|
28
65
|
throw err;
|
|
29
66
|
}
|
|
67
|
+
// Write discovery file so clients can find this server
|
|
68
|
+
try {
|
|
69
|
+
writeFileSync(discoveryPath, JSON.stringify({
|
|
70
|
+
pid: process.pid,
|
|
71
|
+
port,
|
|
72
|
+
url: `http://localhost:${port}`,
|
|
73
|
+
started_at_ms: Date.now(),
|
|
74
|
+
}) + "\n");
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`Warning: could not write ${discoveryPath}: ${err}`);
|
|
78
|
+
}
|
|
30
79
|
console.log(`Server running at http://localhost:${port}`);
|
|
31
80
|
console.log(`Capabilities: lifecycle=[${runtime.capabilities.session_lifecycle_actions.join(",")}]`);
|
|
32
81
|
if (runtime.controlPlane && runtime.controlPlane.activeAdapters.length > 0) {
|
|
@@ -40,6 +89,12 @@ else {
|
|
|
40
89
|
console.log(`API Status: http://localhost:${port}/api/status`);
|
|
41
90
|
}
|
|
42
91
|
const cleanup = async () => {
|
|
92
|
+
try {
|
|
93
|
+
rmSync(discoveryPath, { force: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// best-effort
|
|
97
|
+
}
|
|
43
98
|
await runtime.controlPlane?.stop();
|
|
44
99
|
server.stop();
|
|
45
100
|
process.exit(0);
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type WakeTurnMode = "off" | "shadow" | "active";
|
|
1
2
|
export type MuConfig = {
|
|
2
3
|
version: 1;
|
|
3
4
|
control_plane: {
|
|
@@ -13,17 +14,11 @@ export type MuConfig = {
|
|
|
13
14
|
bot_token: string | null;
|
|
14
15
|
bot_username: string | null;
|
|
15
16
|
};
|
|
16
|
-
gmail: {
|
|
17
|
-
enabled: boolean;
|
|
18
|
-
webhook_secret: string | null;
|
|
19
|
-
client_id: string | null;
|
|
20
|
-
client_secret: string | null;
|
|
21
|
-
refresh_token: string | null;
|
|
22
|
-
};
|
|
23
17
|
};
|
|
24
18
|
operator: {
|
|
25
19
|
enabled: boolean;
|
|
26
20
|
run_triggers_enabled: boolean;
|
|
21
|
+
wake_turn_mode: WakeTurnMode;
|
|
27
22
|
provider: string | null;
|
|
28
23
|
model: string | null;
|
|
29
24
|
};
|
|
@@ -43,17 +38,11 @@ export type MuConfigPatch = {
|
|
|
43
38
|
bot_token?: string | null;
|
|
44
39
|
bot_username?: string | null;
|
|
45
40
|
};
|
|
46
|
-
gmail?: {
|
|
47
|
-
enabled?: boolean;
|
|
48
|
-
webhook_secret?: string | null;
|
|
49
|
-
client_id?: string | null;
|
|
50
|
-
client_secret?: string | null;
|
|
51
|
-
refresh_token?: string | null;
|
|
52
|
-
};
|
|
53
41
|
};
|
|
54
42
|
operator?: {
|
|
55
43
|
enabled?: boolean;
|
|
56
44
|
run_triggers_enabled?: boolean;
|
|
45
|
+
wake_turn_mode?: WakeTurnMode;
|
|
57
46
|
provider?: string | null;
|
|
58
47
|
model?: string | null;
|
|
59
48
|
};
|
|
@@ -73,17 +62,11 @@ export type MuConfigPresence = {
|
|
|
73
62
|
bot_token: boolean;
|
|
74
63
|
bot_username: boolean;
|
|
75
64
|
};
|
|
76
|
-
gmail: {
|
|
77
|
-
enabled: boolean;
|
|
78
|
-
webhook_secret: boolean;
|
|
79
|
-
client_id: boolean;
|
|
80
|
-
client_secret: boolean;
|
|
81
|
-
refresh_token: boolean;
|
|
82
|
-
};
|
|
83
65
|
};
|
|
84
66
|
operator: {
|
|
85
67
|
enabled: boolean;
|
|
86
68
|
run_triggers_enabled: boolean;
|
|
69
|
+
wake_turn_mode: WakeTurnMode;
|
|
87
70
|
provider: boolean;
|
|
88
71
|
model: boolean;
|
|
89
72
|
};
|
package/dist/config.js
CHANGED
|
@@ -15,17 +15,11 @@ export const DEFAULT_MU_CONFIG = {
|
|
|
15
15
|
bot_token: null,
|
|
16
16
|
bot_username: null,
|
|
17
17
|
},
|
|
18
|
-
gmail: {
|
|
19
|
-
enabled: false,
|
|
20
|
-
webhook_secret: null,
|
|
21
|
-
client_id: null,
|
|
22
|
-
client_secret: null,
|
|
23
|
-
refresh_token: null,
|
|
24
|
-
},
|
|
25
18
|
},
|
|
26
19
|
operator: {
|
|
27
20
|
enabled: true,
|
|
28
21
|
run_triggers_enabled: true,
|
|
22
|
+
wake_turn_mode: "off",
|
|
29
23
|
provider: null,
|
|
30
24
|
model: null,
|
|
31
25
|
},
|
|
@@ -60,6 +54,19 @@ function normalizeBoolean(value, fallback) {
|
|
|
60
54
|
}
|
|
61
55
|
return fallback;
|
|
62
56
|
}
|
|
57
|
+
function normalizeWakeTurnMode(value, fallback) {
|
|
58
|
+
if (typeof value !== "string") {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
const normalized = value.trim().toLowerCase();
|
|
62
|
+
if (normalized === "shadow") {
|
|
63
|
+
return "shadow";
|
|
64
|
+
}
|
|
65
|
+
if (normalized === "active") {
|
|
66
|
+
return "active";
|
|
67
|
+
}
|
|
68
|
+
return "off";
|
|
69
|
+
}
|
|
63
70
|
export function normalizeMuConfig(input) {
|
|
64
71
|
const next = cloneDefault();
|
|
65
72
|
const root = asRecord(input);
|
|
@@ -90,24 +97,6 @@ export function normalizeMuConfig(input) {
|
|
|
90
97
|
next.control_plane.adapters.telegram.bot_username = normalizeNullableString(telegram.bot_username);
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
const gmail = asRecord(adapters.gmail);
|
|
94
|
-
if (gmail) {
|
|
95
|
-
if ("enabled" in gmail) {
|
|
96
|
-
next.control_plane.adapters.gmail.enabled = normalizeBoolean(gmail.enabled, next.control_plane.adapters.gmail.enabled);
|
|
97
|
-
}
|
|
98
|
-
if ("webhook_secret" in gmail) {
|
|
99
|
-
next.control_plane.adapters.gmail.webhook_secret = normalizeNullableString(gmail.webhook_secret);
|
|
100
|
-
}
|
|
101
|
-
if ("client_id" in gmail) {
|
|
102
|
-
next.control_plane.adapters.gmail.client_id = normalizeNullableString(gmail.client_id);
|
|
103
|
-
}
|
|
104
|
-
if ("client_secret" in gmail) {
|
|
105
|
-
next.control_plane.adapters.gmail.client_secret = normalizeNullableString(gmail.client_secret);
|
|
106
|
-
}
|
|
107
|
-
if ("refresh_token" in gmail) {
|
|
108
|
-
next.control_plane.adapters.gmail.refresh_token = normalizeNullableString(gmail.refresh_token);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
100
|
}
|
|
112
101
|
const operator = asRecord(controlPlane.operator);
|
|
113
102
|
if (operator) {
|
|
@@ -117,6 +106,9 @@ export function normalizeMuConfig(input) {
|
|
|
117
106
|
if ("run_triggers_enabled" in operator) {
|
|
118
107
|
next.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, next.control_plane.operator.run_triggers_enabled);
|
|
119
108
|
}
|
|
109
|
+
if ("wake_turn_mode" in operator) {
|
|
110
|
+
next.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, next.control_plane.operator.wake_turn_mode);
|
|
111
|
+
}
|
|
120
112
|
if ("provider" in operator) {
|
|
121
113
|
next.control_plane.operator.provider = normalizeNullableString(operator.provider);
|
|
122
114
|
}
|
|
@@ -166,28 +158,6 @@ function normalizeMuConfigPatch(input) {
|
|
|
166
158
|
patch.control_plane.adapters.telegram = telegramPatch;
|
|
167
159
|
}
|
|
168
160
|
}
|
|
169
|
-
const gmail = asRecord(adapters.gmail);
|
|
170
|
-
if (gmail) {
|
|
171
|
-
const gmailPatch = {};
|
|
172
|
-
if ("enabled" in gmail) {
|
|
173
|
-
gmailPatch.enabled = normalizeBoolean(gmail.enabled, DEFAULT_MU_CONFIG.control_plane.adapters.gmail.enabled);
|
|
174
|
-
}
|
|
175
|
-
if ("webhook_secret" in gmail) {
|
|
176
|
-
gmailPatch.webhook_secret = normalizeNullableString(gmail.webhook_secret);
|
|
177
|
-
}
|
|
178
|
-
if ("client_id" in gmail) {
|
|
179
|
-
gmailPatch.client_id = normalizeNullableString(gmail.client_id);
|
|
180
|
-
}
|
|
181
|
-
if ("client_secret" in gmail) {
|
|
182
|
-
gmailPatch.client_secret = normalizeNullableString(gmail.client_secret);
|
|
183
|
-
}
|
|
184
|
-
if ("refresh_token" in gmail) {
|
|
185
|
-
gmailPatch.refresh_token = normalizeNullableString(gmail.refresh_token);
|
|
186
|
-
}
|
|
187
|
-
if (Object.keys(gmailPatch).length > 0) {
|
|
188
|
-
patch.control_plane.adapters.gmail = gmailPatch;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
161
|
}
|
|
192
162
|
const operator = asRecord(controlPlane.operator);
|
|
193
163
|
if (operator) {
|
|
@@ -198,6 +168,9 @@ function normalizeMuConfigPatch(input) {
|
|
|
198
168
|
if ("run_triggers_enabled" in operator) {
|
|
199
169
|
patch.control_plane.operator.run_triggers_enabled = normalizeBoolean(operator.run_triggers_enabled, DEFAULT_MU_CONFIG.control_plane.operator.run_triggers_enabled);
|
|
200
170
|
}
|
|
171
|
+
if ("wake_turn_mode" in operator) {
|
|
172
|
+
patch.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, DEFAULT_MU_CONFIG.control_plane.operator.wake_turn_mode);
|
|
173
|
+
}
|
|
201
174
|
if ("provider" in operator) {
|
|
202
175
|
patch.control_plane.operator.provider = normalizeNullableString(operator.provider);
|
|
203
176
|
}
|
|
@@ -241,23 +214,6 @@ export function applyMuConfigPatch(base, patchInput) {
|
|
|
241
214
|
next.control_plane.adapters.telegram.bot_username = adapters.telegram.bot_username ?? null;
|
|
242
215
|
}
|
|
243
216
|
}
|
|
244
|
-
if (adapters.gmail) {
|
|
245
|
-
if ("enabled" in adapters.gmail && typeof adapters.gmail.enabled === "boolean") {
|
|
246
|
-
next.control_plane.adapters.gmail.enabled = adapters.gmail.enabled;
|
|
247
|
-
}
|
|
248
|
-
if ("webhook_secret" in adapters.gmail) {
|
|
249
|
-
next.control_plane.adapters.gmail.webhook_secret = adapters.gmail.webhook_secret ?? null;
|
|
250
|
-
}
|
|
251
|
-
if ("client_id" in adapters.gmail) {
|
|
252
|
-
next.control_plane.adapters.gmail.client_id = adapters.gmail.client_id ?? null;
|
|
253
|
-
}
|
|
254
|
-
if ("client_secret" in adapters.gmail) {
|
|
255
|
-
next.control_plane.adapters.gmail.client_secret = adapters.gmail.client_secret ?? null;
|
|
256
|
-
}
|
|
257
|
-
if ("refresh_token" in adapters.gmail) {
|
|
258
|
-
next.control_plane.adapters.gmail.refresh_token = adapters.gmail.refresh_token ?? null;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
217
|
}
|
|
262
218
|
const operator = patch.control_plane.operator;
|
|
263
219
|
if (operator) {
|
|
@@ -267,6 +223,9 @@ export function applyMuConfigPatch(base, patchInput) {
|
|
|
267
223
|
if ("run_triggers_enabled" in operator && typeof operator.run_triggers_enabled === "boolean") {
|
|
268
224
|
next.control_plane.operator.run_triggers_enabled = operator.run_triggers_enabled;
|
|
269
225
|
}
|
|
226
|
+
if ("wake_turn_mode" in operator) {
|
|
227
|
+
next.control_plane.operator.wake_turn_mode = normalizeWakeTurnMode(operator.wake_turn_mode, next.control_plane.operator.wake_turn_mode);
|
|
228
|
+
}
|
|
270
229
|
if ("provider" in operator) {
|
|
271
230
|
next.control_plane.operator.provider = operator.provider ?? null;
|
|
272
231
|
}
|
|
@@ -317,10 +276,6 @@ export function redactMuConfigSecrets(config) {
|
|
|
317
276
|
next.control_plane.adapters.discord.signing_secret = redacted(next.control_plane.adapters.discord.signing_secret);
|
|
318
277
|
next.control_plane.adapters.telegram.webhook_secret = redacted(next.control_plane.adapters.telegram.webhook_secret);
|
|
319
278
|
next.control_plane.adapters.telegram.bot_token = redacted(next.control_plane.adapters.telegram.bot_token);
|
|
320
|
-
next.control_plane.adapters.gmail.webhook_secret = redacted(next.control_plane.adapters.gmail.webhook_secret);
|
|
321
|
-
next.control_plane.adapters.gmail.client_id = redacted(next.control_plane.adapters.gmail.client_id);
|
|
322
|
-
next.control_plane.adapters.gmail.client_secret = redacted(next.control_plane.adapters.gmail.client_secret);
|
|
323
|
-
next.control_plane.adapters.gmail.refresh_token = redacted(next.control_plane.adapters.gmail.refresh_token);
|
|
324
279
|
return next;
|
|
325
280
|
}
|
|
326
281
|
function isPresent(value) {
|
|
@@ -341,17 +296,11 @@ export function muConfigPresence(config) {
|
|
|
341
296
|
bot_token: isPresent(config.control_plane.adapters.telegram.bot_token),
|
|
342
297
|
bot_username: isPresent(config.control_plane.adapters.telegram.bot_username),
|
|
343
298
|
},
|
|
344
|
-
gmail: {
|
|
345
|
-
enabled: config.control_plane.adapters.gmail.enabled,
|
|
346
|
-
webhook_secret: isPresent(config.control_plane.adapters.gmail.webhook_secret),
|
|
347
|
-
client_id: isPresent(config.control_plane.adapters.gmail.client_id),
|
|
348
|
-
client_secret: isPresent(config.control_plane.adapters.gmail.client_secret),
|
|
349
|
-
refresh_token: isPresent(config.control_plane.adapters.gmail.refresh_token),
|
|
350
|
-
},
|
|
351
299
|
},
|
|
352
300
|
operator: {
|
|
353
301
|
enabled: config.control_plane.operator.enabled,
|
|
354
302
|
run_triggers_enabled: config.control_plane.operator.run_triggers_enabled,
|
|
303
|
+
wake_turn_mode: config.control_plane.operator.wake_turn_mode,
|
|
355
304
|
provider: isPresent(config.control_plane.operator.provider),
|
|
356
305
|
model: isPresent(config.control_plane.operator.model),
|
|
357
306
|
},
|
package/dist/control_plane.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { MessagingOperatorBackend, MessagingOperatorRuntime } from "@femtomc/mu-agent";
|
|
2
2
|
import { type GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
|
|
3
|
-
import type
|
|
3
|
+
import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type ControlPlaneHandle, type ControlPlaneSessionLifecycle, type InterRootQueuePolicy, type TelegramGenerationSwapHooks, type WakeDeliveryObserver } from "./control_plane_contract.js";
|
|
4
4
|
import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
5
5
|
import { type ControlPlaneRunSupervisorOpts } from "./run_supervisor.js";
|
|
6
|
-
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, } from "./control_plane_contract.js";
|
|
6
|
+
export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
|
|
7
7
|
type DetectedAdapter = {
|
|
8
8
|
name: "slack";
|
|
9
9
|
signingSecret: string;
|
|
@@ -47,6 +47,8 @@ export type BootstrapControlPlaneOpts = {
|
|
|
47
47
|
generation?: ControlPlaneGenerationContext;
|
|
48
48
|
telemetry?: GenerationTelemetryRecorder | null;
|
|
49
49
|
telegramGenerationHooks?: TelegramGenerationSwapHooks;
|
|
50
|
+
wakeDeliveryObserver?: WakeDeliveryObserver | null;
|
|
50
51
|
terminalEnabled?: boolean;
|
|
52
|
+
interRootQueuePolicy?: InterRootQueuePolicy;
|
|
51
53
|
};
|
|
52
54
|
export declare function bootstrapControlPlane(opts: BootstrapControlPlaneOpts): Promise<ControlPlaneHandle | null>;
|