spawnfile 0.1.4 → 0.1.6
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/cli/lifecycleCommands.d.ts +3 -0
- package/dist/cli/lifecycleCommands.js +80 -0
- package/dist/cli/runCli.d.ts +2 -1
- package/dist/cli/runCli.js +4 -48
- package/dist/compiler/buildCompilePlan.js +12 -202
- package/dist/compiler/buildCompilePlanRuntime.d.ts +6 -1
- package/dist/compiler/buildCompilePlanRuntime.js +9 -0
- package/dist/compiler/buildCompilePlanTeams.js +16 -10
- package/dist/compiler/buildCompilePlanTraversal.d.ts +16 -0
- package/dist/compiler/buildCompilePlanTraversal.js +214 -0
- package/dist/compiler/buildCompilePlanTraversalHelpers.d.ts +18 -0
- package/dist/compiler/buildCompilePlanTraversalHelpers.js +22 -0
- package/dist/compiler/compilePlanHelpers.d.ts +3 -1
- package/dist/compiler/compilePlanHelpers.js +37 -1
- package/dist/compiler/compileProject.js +14 -0
- package/dist/compiler/containerArtifacts.js +18 -3
- package/dist/compiler/containerArtifactsPlans.js +32 -0
- package/dist/compiler/containerArtifactsRender.js +86 -3
- package/dist/compiler/containerArtifactsTypes.d.ts +7 -3
- package/dist/compiler/containerEntrypointRender.d.ts +1 -1
- package/dist/compiler/containerEntrypointRender.js +34 -24
- package/dist/compiler/containerTargetResources.d.ts +4 -0
- package/dist/compiler/containerTargetResources.js +54 -0
- package/dist/compiler/containerWorkspaceResourceRender.d.ts +3 -0
- package/dist/compiler/containerWorkspaceResourceRender.js +128 -0
- package/dist/compiler/executionDefaults.js +0 -3
- package/dist/compiler/helpers.d.ts +1 -1
- package/dist/compiler/index.d.ts +1 -0
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/moltnetArtifacts.d.ts +11 -5
- package/dist/compiler/moltnetArtifacts.js +133 -117
- package/dist/compiler/moltnetClientConfig.js +8 -2
- package/dist/compiler/moltnetConfigLowering.d.ts +36 -0
- package/dist/compiler/moltnetConfigLowering.js +125 -0
- package/dist/compiler/moltnetRuntimeConfig.d.ts +2 -0
- package/dist/compiler/moltnetRuntimeConfig.js +69 -0
- package/dist/compiler/surfaces.d.ts +3 -13
- package/dist/compiler/surfaces.js +1 -6
- package/dist/compiler/syncProjectAuth.js +14 -0
- package/dist/compiler/types.d.ts +16 -1
- package/dist/compiler/upProject.d.ts +19 -0
- package/dist/compiler/upProject.js +37 -0
- package/dist/compiler/view/buildOrganizationView.js +22 -2
- package/dist/compiler/view/renderNetworks.js +14 -3
- package/dist/compiler/view/renderTree.js +8 -2
- package/dist/compiler/view/types.d.ts +18 -3
- package/dist/compiler/workspaceResources.d.ts +34 -0
- package/dist/compiler/workspaceResources.js +120 -0
- package/dist/filesystem/paths.js +1 -10
- package/dist/manifest/executionSchemas.d.ts +106 -0
- package/dist/manifest/executionSchemas.js +140 -0
- package/dist/manifest/loadManifest.js +15 -27
- package/dist/manifest/renderSpawnfile.js +44 -52
- package/dist/manifest/renderSpawnfileNetworks.d.ts +2 -0
- package/dist/manifest/renderSpawnfileNetworks.js +63 -0
- package/dist/manifest/renderSpawnfileWorkspace.d.ts +2 -0
- package/dist/manifest/renderSpawnfileWorkspace.js +47 -0
- package/dist/manifest/scaffold.js +12 -6
- package/dist/manifest/scheduleSchemas.d.ts +15 -0
- package/dist/manifest/scheduleSchemas.js +26 -0
- package/dist/manifest/schemas.d.ts +626 -368
- package/dist/manifest/schemas.js +51 -191
- package/dist/manifest/teamNetworkSchemas.d.ts +228 -0
- package/dist/manifest/teamNetworkSchemas.js +295 -0
- package/dist/manifest/workspaceSchemas.d.ts +96 -0
- package/dist/manifest/workspaceSchemas.js +166 -0
- package/dist/report/types.d.ts +10 -0
- package/dist/runtime/common.d.ts +2 -1
- package/dist/runtime/common.js +3 -3
- package/dist/runtime/tinyclaw/adapter.js +9 -2
- package/dist/runtime/tinyclaw/schedules.d.ts +9 -0
- package/dist/runtime/tinyclaw/schedules.js +62 -0
- package/dist/runtime/types.d.ts +1 -0
- package/package.json +2 -1
|
@@ -1,77 +1,13 @@
|
|
|
1
|
-
import { getRuntimeAdapter } from "../runtime/index.js";
|
|
2
1
|
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
import { createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetBaseUrl, resolveMoltnetClientAuth } from "./moltnetConfigLowering.js";
|
|
3
3
|
import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
|
|
4
|
+
import { resolveRuntimeConfig } from "./moltnetRuntimeConfig.js";
|
|
4
5
|
const DEFAULT_MOLTNET_PORT = 8787;
|
|
5
|
-
const DEFAULT_TINYCLAW_PORT = 3777;
|
|
6
6
|
const ROOTFS_PREFIX = "container/rootfs";
|
|
7
|
-
const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
|
|
8
|
-
const CONFIG_FILE_PLACEHOLDER = "<config-file>";
|
|
9
7
|
const createServerKey = (networkId) => networkId;
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const basePort = adapter.container.port;
|
|
14
|
-
if (basePort === undefined) {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
const runtimeAgents = plan.nodes.filter((node) => node.kind === "agent" && node.runtimeName === runtimeName);
|
|
18
|
-
const index = runtimeAgents.findIndex((node) => node.slug === slug);
|
|
19
|
-
if (index < 0) {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
return basePort + (index * (adapter.container.portStride ?? 1));
|
|
23
|
-
};
|
|
24
|
-
const createTinyClawChannel = (networkId, agentId) => `moltnet:${networkId}:${agentId}`;
|
|
25
|
-
const replaceContainerPathTemplate = (template, instanceRoot, configFileName) => template
|
|
26
|
-
.replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
|
|
27
|
-
.replaceAll(CONFIG_FILE_PLACEHOLDER, configFileName);
|
|
28
|
-
const resolveRuntimeInstancePaths = (runtimeName, slug) => {
|
|
29
|
-
const adapter = getRuntimeAdapter(runtimeName);
|
|
30
|
-
const instanceRoot = `/var/lib/spawnfile/instances/${runtimeName}/agent-${slug}`;
|
|
31
|
-
return {
|
|
32
|
-
configPath: replaceContainerPathTemplate(adapter.container.instancePaths.configPathTemplate, instanceRoot, adapter.container.configFileName),
|
|
33
|
-
homePath: adapter.container.instancePaths.homePathTemplate
|
|
34
|
-
? replaceContainerPathTemplate(adapter.container.instancePaths.homePathTemplate, instanceRoot, adapter.container.configFileName)
|
|
35
|
-
: undefined
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
const resolveRuntimeConfig = (plan, agentNode, nodeSlug, networkId, agentId) => {
|
|
39
|
-
switch (agentNode.runtime.name) {
|
|
40
|
-
case "openclaw": {
|
|
41
|
-
const port = resolveSequentialRuntimePort(plan, "openclaw", nodeSlug);
|
|
42
|
-
if (!port) {
|
|
43
|
-
throw new SpawnfileError("compile_error", `Unable to resolve OpenClaw gateway port for Moltnet agent ${agentNode.name}`);
|
|
44
|
-
}
|
|
45
|
-
const instancePaths = resolveRuntimeInstancePaths("openclaw", nodeSlug);
|
|
46
|
-
return {
|
|
47
|
-
gateway_url: `ws://127.0.0.1:${port}`,
|
|
48
|
-
...(instancePaths.homePath ? { home_path: instancePaths.homePath } : {}),
|
|
49
|
-
kind: "openclaw",
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
case "picoclaw": {
|
|
53
|
-
const instancePaths = resolveRuntimeInstancePaths("picoclaw", nodeSlug);
|
|
54
|
-
return {
|
|
55
|
-
command: "/usr/local/bin/picoclaw",
|
|
56
|
-
config_path: instancePaths.configPath,
|
|
57
|
-
...(instancePaths.homePath ? { home_path: instancePaths.homePath } : {}),
|
|
58
|
-
kind: "picoclaw",
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
case "tinyclaw": {
|
|
62
|
-
const channel = createTinyClawChannel(networkId, agentId);
|
|
63
|
-
return {
|
|
64
|
-
ack_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses`,
|
|
65
|
-
channel,
|
|
66
|
-
inbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/message`,
|
|
67
|
-
kind: "tinyclaw",
|
|
68
|
-
outbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses/pending?channel=${encodeURIComponent(channel)}`
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
default:
|
|
72
|
-
throw new SpawnfileError("compile_error", `Moltnet does not know how to attach runtime ${agentNode.runtime.name} directly`);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
8
|
+
const toContainerRootfsPath = (rootfsPath) => `/${rootfsPath.replace(`${ROOTFS_PREFIX}/`, "")}`;
|
|
9
|
+
const isNetworkHttpEnabled = (network) => network.server?.mode === "managed" && network.server.human_ingress === true;
|
|
10
|
+
const resolveNetworkPort = (network, fallbackPort) => network.server?.mode === "managed" ? network.server.listen.port : fallbackPort;
|
|
75
11
|
export const generateMoltnetArtifacts = async (plan) => {
|
|
76
12
|
const teamNodes = plan.nodes
|
|
77
13
|
.filter((node) => node.kind === "team")
|
|
@@ -83,9 +19,16 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
83
19
|
let nextPort = DEFAULT_MOLTNET_PORT;
|
|
84
20
|
for (const teamNode of teamNodes) {
|
|
85
21
|
for (const network of teamNode.value.networks ?? []) {
|
|
22
|
+
const server = network.server;
|
|
23
|
+
if (!server) {
|
|
24
|
+
throw new SpawnfileError("validation_error", `Moltnet network ${network.id} must declare server`);
|
|
25
|
+
}
|
|
86
26
|
const serverKey = createServerKey(network.id);
|
|
87
27
|
const existingPlan = serverPlans.get(serverKey);
|
|
88
28
|
if (existingPlan) {
|
|
29
|
+
if (existingPlan.baseUrl !== resolveMoltnetBaseUrl(server)) {
|
|
30
|
+
throw new SpawnfileError("validation_error", `Duplicate Moltnet network ${network.id} declares conflicting server URL`);
|
|
31
|
+
}
|
|
89
32
|
for (const room of network.rooms) {
|
|
90
33
|
const concreteMembers = listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room);
|
|
91
34
|
const existingRoom = existingPlan.rooms.find((entry) => entry.id === room.id);
|
|
@@ -97,31 +40,64 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
97
40
|
else {
|
|
98
41
|
existingPlan.rooms.push({
|
|
99
42
|
id: room.id,
|
|
100
|
-
members: concreteMembers
|
|
43
|
+
members: concreteMembers,
|
|
44
|
+
...(room.name ? { name: room.name } : {})
|
|
101
45
|
});
|
|
102
46
|
}
|
|
103
47
|
}
|
|
104
48
|
existingPlan.rooms.sort((left, right) => left.id.localeCompare(right.id));
|
|
105
49
|
}
|
|
106
50
|
else {
|
|
51
|
+
const port = server.mode === "managed" ? resolveNetworkPort(network, nextPort) : undefined;
|
|
52
|
+
const serverId = `${teamNode.slug}-${network.id}`;
|
|
107
53
|
serverPlans.set(serverKey, {
|
|
108
|
-
|
|
54
|
+
baseUrl: resolveMoltnetBaseUrl(server),
|
|
55
|
+
...(server.mode === "managed"
|
|
56
|
+
? { configPath: toContainerRootfsPath(createMoltnetServerConfigPath(serverId)) }
|
|
57
|
+
: {}),
|
|
58
|
+
id: serverId,
|
|
59
|
+
mode: server.mode,
|
|
109
60
|
name: network.name,
|
|
110
61
|
networkId: network.id,
|
|
111
|
-
port:
|
|
62
|
+
...(port ? { port } : {}),
|
|
112
63
|
rooms: network.rooms.map((room) => ({
|
|
113
64
|
id: room.id,
|
|
114
|
-
members: listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room)
|
|
65
|
+
members: listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room),
|
|
66
|
+
...(room.name ? { name: room.name } : {})
|
|
115
67
|
})),
|
|
68
|
+
server,
|
|
69
|
+
secretPatches: [],
|
|
116
70
|
teamSource: teamNode.value.source
|
|
117
71
|
});
|
|
118
|
-
|
|
72
|
+
if (port) {
|
|
73
|
+
nextPort = Math.max(nextPort, port + 1);
|
|
74
|
+
}
|
|
119
75
|
}
|
|
120
76
|
}
|
|
121
77
|
}
|
|
122
|
-
const
|
|
123
|
-
const
|
|
78
|
+
const nodePlans = [];
|
|
79
|
+
const nodePlanKeys = new Set();
|
|
124
80
|
const configFiles = [];
|
|
81
|
+
for (const teamNode of teamNodes) {
|
|
82
|
+
for (const network of teamNode.value.networks ?? []) {
|
|
83
|
+
const serverPlan = serverPlans.get(createServerKey(network.id));
|
|
84
|
+
if (!serverPlan || !network.server || network.server.mode !== "managed" || !serverPlan.configPath) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const native = createMoltnetNativeServerConfig({
|
|
88
|
+
networkId: network.id,
|
|
89
|
+
networkName: network.name,
|
|
90
|
+
rooms: serverPlan.rooms,
|
|
91
|
+
server: network.server
|
|
92
|
+
});
|
|
93
|
+
serverPlan.secretPatches = native.secretPatches;
|
|
94
|
+
configFiles.push({
|
|
95
|
+
content: `${JSON.stringify(native.config, null, 2)}\n`,
|
|
96
|
+
mode: 0o600,
|
|
97
|
+
path: createMoltnetServerConfigPath(serverPlan.id)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
125
101
|
for (const node of plan.nodes) {
|
|
126
102
|
if (node.kind !== "agent") {
|
|
127
103
|
continue;
|
|
@@ -142,67 +118,107 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
142
118
|
if (!serverPlan) {
|
|
143
119
|
throw new SpawnfileError("validation_error", `Unable to find Moltnet network ${attachment.network} for ${agentNode.name}`);
|
|
144
120
|
}
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
121
|
+
const network = teamNode.value.networks?.find((entry) => entry.id === attachment.network);
|
|
122
|
+
if (!network) {
|
|
123
|
+
throw new SpawnfileError("validation_error", `Unable to find Moltnet network ${attachment.network} for ${agentNode.name}`);
|
|
124
|
+
}
|
|
125
|
+
if (network.server?.mode === "managed" && network.server.direct_messages === false && attachment.dms) {
|
|
126
|
+
throw new SpawnfileError("validation_error", `Moltnet network ${attachment.network} disables direct messages but ${agentNode.name} declares dms`);
|
|
149
127
|
}
|
|
150
|
-
|
|
128
|
+
if (!network.server) {
|
|
129
|
+
throw new SpawnfileError("validation_error", `Moltnet network ${attachment.network} must declare server`);
|
|
130
|
+
}
|
|
131
|
+
const configPath = createMoltnetNodeConfigPath(teamNode.slug, attachment.network, attachment.memberId);
|
|
132
|
+
const nodePlanKey = `${attachment.network}::${attachment.memberId}`;
|
|
133
|
+
if (nodePlanKeys.has(nodePlanKey)) {
|
|
134
|
+
throw new SpawnfileError("validation_error", `Duplicate Moltnet node attachment for ${attachment.network}/${attachment.memberId}`);
|
|
135
|
+
}
|
|
136
|
+
nodePlanKeys.add(nodePlanKey);
|
|
137
|
+
const clientAuth = resolveMoltnetClientAuth(network.server, attachment.network, attachment.memberId);
|
|
138
|
+
const usesPerAttachmentOpenToken = clientAuth.mode === "open" &&
|
|
139
|
+
clientAuth.staticToken !== true &&
|
|
140
|
+
Boolean(clientAuth.tokenEnv || clientAuth.tokenPath);
|
|
151
141
|
configFiles.push({
|
|
152
142
|
content: `${JSON.stringify({
|
|
153
|
-
version: "moltnet.
|
|
154
|
-
agent: {
|
|
155
|
-
id: attachment.memberId,
|
|
156
|
-
name: agentNode.name
|
|
157
|
-
},
|
|
143
|
+
version: "moltnet.node.v1",
|
|
158
144
|
moltnet: {
|
|
159
|
-
base_url:
|
|
160
|
-
network_id: attachment.network
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.
|
|
168
|
-
id: roomId,
|
|
169
|
-
...(policy.read ? { read: policy.read } : {}),
|
|
170
|
-
...(policy.reply ? { reply: policy.reply } : {})
|
|
171
|
-
}))
|
|
172
|
-
}
|
|
173
|
-
: {}),
|
|
174
|
-
...(attachment.dms
|
|
175
|
-
? {
|
|
176
|
-
dms: {
|
|
177
|
-
enabled: attachment.dms.enabled,
|
|
178
|
-
...(attachment.dms.read ? { read: attachment.dms.read } : {}),
|
|
179
|
-
...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
|
|
145
|
+
base_url: serverPlan.baseUrl,
|
|
146
|
+
network_id: attachment.network,
|
|
147
|
+
auth_mode: clientAuth.mode,
|
|
148
|
+
...(clientAuth.staticToken
|
|
149
|
+
? { static_token: true }
|
|
150
|
+
: {}),
|
|
151
|
+
...(!usesPerAttachmentOpenToken && clientAuth.tokenEnv
|
|
152
|
+
? {
|
|
153
|
+
token_env: clientAuth.tokenEnv
|
|
180
154
|
}
|
|
155
|
+
: {}),
|
|
156
|
+
...(!usesPerAttachmentOpenToken && clientAuth.tokenPath
|
|
157
|
+
? {
|
|
158
|
+
token_path: clientAuth.tokenPath
|
|
159
|
+
}
|
|
160
|
+
: {})
|
|
161
|
+
},
|
|
162
|
+
attachments: [
|
|
163
|
+
{
|
|
164
|
+
agent: {
|
|
165
|
+
id: attachment.memberId,
|
|
166
|
+
name: agentNode.name
|
|
167
|
+
},
|
|
168
|
+
...(usesPerAttachmentOpenToken
|
|
169
|
+
? {
|
|
170
|
+
moltnet: {
|
|
171
|
+
...(clientAuth.tokenEnv ? { token_env: clientAuth.tokenEnv } : {}),
|
|
172
|
+
...(clientAuth.tokenPath ? { token_path: clientAuth.tokenPath } : {})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
176
|
+
runtime: resolveRuntimeConfig(plan, agentNode, node.slug, attachment.network, attachment.memberId),
|
|
177
|
+
...(attachment.rooms
|
|
178
|
+
? {
|
|
179
|
+
rooms: Object.entries(attachment.rooms)
|
|
180
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
181
|
+
.map(([roomId, policy]) => ({
|
|
182
|
+
id: roomId,
|
|
183
|
+
...(policy.read ? { read: policy.read } : {}),
|
|
184
|
+
...(policy.reply ? { reply: policy.reply } : {})
|
|
185
|
+
}))
|
|
186
|
+
}
|
|
187
|
+
: {}),
|
|
188
|
+
...(attachment.dms
|
|
189
|
+
? {
|
|
190
|
+
dms: {
|
|
191
|
+
enabled: attachment.dms.enabled,
|
|
192
|
+
...(attachment.dms.read ? { read: attachment.dms.read } : {}),
|
|
193
|
+
...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
: {})
|
|
181
197
|
}
|
|
182
|
-
|
|
198
|
+
]
|
|
183
199
|
}, null, 2)}\n`,
|
|
184
200
|
mode: 0o600,
|
|
185
201
|
path: configPath
|
|
186
202
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
networkId: attachment.network,
|
|
191
|
-
runtime: agentNode.runtime.name
|
|
203
|
+
nodePlans.push({
|
|
204
|
+
configPath: toContainerRootfsPath(configPath),
|
|
205
|
+
networkId: attachment.network
|
|
192
206
|
});
|
|
193
207
|
}
|
|
194
208
|
}
|
|
209
|
+
const managedServerPlans = [...serverPlans.values()].filter((serverPlan) => serverPlan.mode === "managed");
|
|
195
210
|
return {
|
|
196
|
-
bridgePlans: bridgePlans.sort((left, right) => left.configPath.localeCompare(right.configPath)),
|
|
197
211
|
files: configFiles,
|
|
198
|
-
|
|
212
|
+
nodePlans: nodePlans.sort((left, right) => left.configPath.localeCompare(right.configPath)),
|
|
213
|
+
ports: [...new Set(managedServerPlans.map((serverPlan) => serverPlan.port).filter((port) => port !== undefined))].sort((left, right) => left - right),
|
|
199
214
|
publishedPorts: [
|
|
200
215
|
...new Set(teamNodes
|
|
201
|
-
.flatMap((teamNode) => (teamNode.value.networks ?? []).map((network) => network
|
|
216
|
+
.flatMap((teamNode) => (teamNode.value.networks ?? []).map((network) => isNetworkHttpEnabled(network)
|
|
202
217
|
? serverPlans.get(createServerKey(network.id))?.port
|
|
203
218
|
: undefined))
|
|
204
219
|
.filter((port) => port !== undefined))
|
|
205
220
|
].sort((left, right) => left - right),
|
|
206
|
-
serverPlans: [...serverPlans.values()].sort((left, right) => left.port - right.port)
|
|
221
|
+
serverPlans: [...serverPlans.values()].sort((left, right) => (left.port ?? Number.MAX_SAFE_INTEGER) - (right.port ?? Number.MAX_SAFE_INTEGER)
|
|
222
|
+
|| left.networkId.localeCompare(right.networkId))
|
|
207
223
|
};
|
|
208
224
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
import { resolveMoltnetClientAuth } from "./moltnetConfigLowering.js";
|
|
2
3
|
const GENERATED_SKILL_NAME = "moltnet";
|
|
3
4
|
const GENERATED_CONFIG_PATH = ".moltnet/config.json";
|
|
4
5
|
const createConfigContent = (node, attachments) => `${JSON.stringify({
|
|
@@ -23,10 +24,15 @@ const createAttachmentConfig = (node, artifacts, attachment) => {
|
|
|
23
24
|
throw new SpawnfileError("compile_error", `Moltnet client config requires a resolved member id for ${node.name}`);
|
|
24
25
|
}
|
|
25
26
|
const serverPlan = findServerPlan(artifacts, attachment);
|
|
27
|
+
const auth = resolveMoltnetClientAuth(serverPlan.server, attachment.network, attachment.memberId);
|
|
26
28
|
return {
|
|
27
29
|
agent_name: node.name,
|
|
28
|
-
auth: {
|
|
29
|
-
|
|
30
|
+
auth: {
|
|
31
|
+
mode: auth.mode,
|
|
32
|
+
...(auth.tokenEnv ? { token_env: auth.tokenEnv } : {}),
|
|
33
|
+
...(auth.tokenPath ? { token_path: auth.tokenPath } : {})
|
|
34
|
+
},
|
|
35
|
+
base_url: serverPlan.baseUrl,
|
|
30
36
|
...(attachment.dms
|
|
31
37
|
? {
|
|
32
38
|
dms: {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TeamNetworkServer } from "../manifest/index.js";
|
|
2
|
+
export interface MoltnetSecretPatch {
|
|
3
|
+
envName: string;
|
|
4
|
+
jsonPath: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MoltnetClientAuthPlan {
|
|
7
|
+
mode: "bearer" | "none" | "open";
|
|
8
|
+
staticToken?: boolean;
|
|
9
|
+
tokenEnv?: string;
|
|
10
|
+
tokenPath?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface MoltnetNativeRoomConfig {
|
|
13
|
+
id: string;
|
|
14
|
+
members: string[];
|
|
15
|
+
name?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface MoltnetNativeServerConfigInput {
|
|
18
|
+
networkId: string;
|
|
19
|
+
networkName: string;
|
|
20
|
+
rooms: MoltnetNativeRoomConfig[];
|
|
21
|
+
server: Extract<TeamNetworkServer, {
|
|
22
|
+
mode: "managed";
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export declare const createMoltnetOpenTokenPath: (networkId: string, memberId: string) => string;
|
|
26
|
+
export declare const createMoltnetServerConfigPath: (serverId: string) => string;
|
|
27
|
+
export declare const createMoltnetNodeConfigPath: (teamSlug: string, networkId: string, agentId: string) => string;
|
|
28
|
+
export declare const renderMoltnetListenAddr: (server: Extract<TeamNetworkServer, {
|
|
29
|
+
mode: "managed";
|
|
30
|
+
}>) => string;
|
|
31
|
+
export declare const resolveMoltnetBaseUrl: (server: TeamNetworkServer) => string;
|
|
32
|
+
export declare const resolveMoltnetClientAuth: (server: TeamNetworkServer, networkId: string, memberId: string) => MoltnetClientAuthPlan;
|
|
33
|
+
export declare const createMoltnetNativeServerConfig: ({ networkId, networkName, rooms, server }: MoltnetNativeServerConfigInput) => {
|
|
34
|
+
config: Record<string, unknown>;
|
|
35
|
+
secretPatches: MoltnetSecretPatch[];
|
|
36
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const pathSafeSegment = (value) => value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "item";
|
|
2
|
+
const isIpv6Literal = (value) => value.includes(":");
|
|
3
|
+
export const createMoltnetOpenTokenPath = (networkId, memberId) => `/var/lib/spawnfile/moltnet/tokens/${pathSafeSegment(networkId)}/${pathSafeSegment(memberId)}.token`;
|
|
4
|
+
export const createMoltnetServerConfigPath = (serverId) => `container/rootfs/var/lib/spawnfile/moltnet/servers/${pathSafeSegment(serverId)}/Moltnet.json`;
|
|
5
|
+
export const createMoltnetNodeConfigPath = (teamSlug, networkId, agentId) => `container/rootfs/var/lib/spawnfile/moltnet/nodes/${pathSafeSegment(teamSlug)}-${pathSafeSegment(networkId)}-${pathSafeSegment(agentId)}.json`;
|
|
6
|
+
export const renderMoltnetListenAddr = (server) => {
|
|
7
|
+
const bind = server.listen.bind;
|
|
8
|
+
return `${isIpv6Literal(bind) ? `[${bind}]` : bind}:${server.listen.port}`;
|
|
9
|
+
};
|
|
10
|
+
export const resolveMoltnetBaseUrl = (server) => {
|
|
11
|
+
if (server.mode === "external") {
|
|
12
|
+
return server.url;
|
|
13
|
+
}
|
|
14
|
+
if (server.url && server.url.trim().length > 0) {
|
|
15
|
+
return server.url.trim();
|
|
16
|
+
}
|
|
17
|
+
const bind = server.listen.bind;
|
|
18
|
+
const host = bind === "0.0.0.0" || bind === "::"
|
|
19
|
+
? "127.0.0.1"
|
|
20
|
+
: isIpv6Literal(bind)
|
|
21
|
+
? `[${bind}]`
|
|
22
|
+
: bind;
|
|
23
|
+
return `http://${host}:${server.listen.port}`;
|
|
24
|
+
};
|
|
25
|
+
export const resolveMoltnetClientAuth = (server, networkId, memberId) => {
|
|
26
|
+
if (server.auth.mode === "none") {
|
|
27
|
+
return { mode: "none" };
|
|
28
|
+
}
|
|
29
|
+
const client = server.auth.client;
|
|
30
|
+
if (server.auth.mode === "open" && !client) {
|
|
31
|
+
return {
|
|
32
|
+
mode: "open",
|
|
33
|
+
tokenPath: createMoltnetOpenTokenPath(networkId, memberId)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!client) {
|
|
37
|
+
return { mode: server.auth.mode };
|
|
38
|
+
}
|
|
39
|
+
const tokenEnv = client.token_env
|
|
40
|
+
?? (client.token_id && server.mode === "managed"
|
|
41
|
+
? server.auth.tokens?.find((token) => token.id === client.token_id)?.secret
|
|
42
|
+
: undefined);
|
|
43
|
+
return {
|
|
44
|
+
mode: server.auth.mode,
|
|
45
|
+
...(client.static_token ? { staticToken: true } : {}),
|
|
46
|
+
...(tokenEnv ? { tokenEnv } : {}),
|
|
47
|
+
...(client.token_path ? { tokenPath: client.token_path } : {})
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const storageConfigFor = (store) => {
|
|
51
|
+
switch (store.kind) {
|
|
52
|
+
case "sqlite":
|
|
53
|
+
return { kind: "sqlite", sqlite: { path: store.path } };
|
|
54
|
+
case "json":
|
|
55
|
+
return { kind: "json", json: { path: store.path } };
|
|
56
|
+
case "postgres":
|
|
57
|
+
return { kind: "postgres", postgres: { dsn: "" } };
|
|
58
|
+
case "memory":
|
|
59
|
+
return { kind: "memory" };
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export const createMoltnetNativeServerConfig = ({ networkId, networkName, rooms, server }) => {
|
|
63
|
+
const secretPatches = [];
|
|
64
|
+
const tokens = (server.auth.tokens ?? []).map((token, index) => {
|
|
65
|
+
secretPatches.push({
|
|
66
|
+
envName: token.secret,
|
|
67
|
+
jsonPath: `auth.tokens.${index}.value`
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
id: token.id,
|
|
71
|
+
value: "",
|
|
72
|
+
scopes: token.scopes,
|
|
73
|
+
...(token.agents ? { agents: token.agents } : {})
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
const pairings = (server.pairings ?? []).map((pairing, index) => {
|
|
77
|
+
secretPatches.push({
|
|
78
|
+
envName: pairing.token_secret,
|
|
79
|
+
jsonPath: `pairings.${index}.token`
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
id: pairing.id,
|
|
83
|
+
remote_network_id: pairing.remote_network_id,
|
|
84
|
+
remote_network_name: pairing.remote_network_name,
|
|
85
|
+
remote_base_url: pairing.remote_base_url,
|
|
86
|
+
token: ""
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
if (server.store.kind === "postgres") {
|
|
90
|
+
secretPatches.push({
|
|
91
|
+
envName: server.store.dsn_secret,
|
|
92
|
+
jsonPath: "storage.postgres.dsn"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
config: {
|
|
97
|
+
version: "moltnet.v1",
|
|
98
|
+
network: {
|
|
99
|
+
id: networkId,
|
|
100
|
+
name: networkName
|
|
101
|
+
},
|
|
102
|
+
server: {
|
|
103
|
+
listen_addr: renderMoltnetListenAddr(server),
|
|
104
|
+
...(server.human_ingress !== undefined ? { human_ingress: server.human_ingress } : {}),
|
|
105
|
+
...(server.direct_messages !== undefined ? { direct_messages: server.direct_messages } : {}),
|
|
106
|
+
...(server.trust_forwarded_proto !== undefined
|
|
107
|
+
? { trust_forwarded_proto: server.trust_forwarded_proto }
|
|
108
|
+
: {}),
|
|
109
|
+
...(server.allowed_origins ? { allowed_origins: server.allowed_origins } : {})
|
|
110
|
+
},
|
|
111
|
+
auth: {
|
|
112
|
+
mode: server.auth.mode,
|
|
113
|
+
...(tokens.length > 0 ? { tokens } : {})
|
|
114
|
+
},
|
|
115
|
+
storage: storageConfigFor(server.store),
|
|
116
|
+
rooms: rooms.map((room) => ({
|
|
117
|
+
id: room.id,
|
|
118
|
+
...(room.name ? { name: room.name } : {}),
|
|
119
|
+
members: room.members
|
|
120
|
+
})),
|
|
121
|
+
...(pairings.length > 0 ? { pairings } : {})
|
|
122
|
+
},
|
|
123
|
+
secretPatches
|
|
124
|
+
};
|
|
125
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getRuntimeAdapter } from "../runtime/index.js";
|
|
2
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
3
|
+
const DEFAULT_TINYCLAW_PORT = 3777;
|
|
4
|
+
const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
|
|
5
|
+
const CONFIG_FILE_PLACEHOLDER = "<config-file>";
|
|
6
|
+
const resolveSequentialRuntimePort = (plan, runtimeName, slug) => {
|
|
7
|
+
const adapter = getRuntimeAdapter(runtimeName);
|
|
8
|
+
const basePort = adapter.container.port;
|
|
9
|
+
if (basePort === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
const runtimeAgents = plan.nodes.filter((node) => node.kind === "agent" && node.runtimeName === runtimeName);
|
|
13
|
+
const index = runtimeAgents.findIndex((node) => node.slug === slug);
|
|
14
|
+
if (index < 0) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return basePort + (index * (adapter.container.portStride ?? 1));
|
|
18
|
+
};
|
|
19
|
+
const createTinyClawChannel = (networkId, agentId) => `moltnet:${networkId}:${agentId}`;
|
|
20
|
+
const replaceContainerPathTemplate = (template, instanceRoot, configFileName) => template
|
|
21
|
+
.replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
|
|
22
|
+
.replaceAll(CONFIG_FILE_PLACEHOLDER, configFileName);
|
|
23
|
+
const resolveRuntimeInstancePaths = (runtimeName, slug) => {
|
|
24
|
+
const adapter = getRuntimeAdapter(runtimeName);
|
|
25
|
+
const instanceRoot = `/var/lib/spawnfile/instances/${runtimeName}/agent-${slug}`;
|
|
26
|
+
return {
|
|
27
|
+
configPath: replaceContainerPathTemplate(adapter.container.instancePaths.configPathTemplate, instanceRoot, adapter.container.configFileName),
|
|
28
|
+
homePath: adapter.container.instancePaths.homePathTemplate
|
|
29
|
+
? replaceContainerPathTemplate(adapter.container.instancePaths.homePathTemplate, instanceRoot, adapter.container.configFileName)
|
|
30
|
+
: undefined
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export const resolveRuntimeConfig = (plan, agentNode, nodeSlug, networkId, agentId) => {
|
|
34
|
+
switch (agentNode.runtime.name) {
|
|
35
|
+
case "openclaw": {
|
|
36
|
+
const port = resolveSequentialRuntimePort(plan, "openclaw", nodeSlug);
|
|
37
|
+
if (!port) {
|
|
38
|
+
throw new SpawnfileError("compile_error", `Unable to resolve OpenClaw gateway port for Moltnet agent ${agentNode.name}`);
|
|
39
|
+
}
|
|
40
|
+
const instancePaths = resolveRuntimeInstancePaths("openclaw", nodeSlug);
|
|
41
|
+
return {
|
|
42
|
+
gateway_url: `ws://127.0.0.1:${port}`,
|
|
43
|
+
...(instancePaths.homePath ? { home_path: instancePaths.homePath } : {}),
|
|
44
|
+
kind: "openclaw"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
case "picoclaw": {
|
|
48
|
+
const instancePaths = resolveRuntimeInstancePaths("picoclaw", nodeSlug);
|
|
49
|
+
return {
|
|
50
|
+
command: "/usr/local/bin/picoclaw",
|
|
51
|
+
config_path: instancePaths.configPath,
|
|
52
|
+
...(instancePaths.homePath ? { home_path: instancePaths.homePath } : {}),
|
|
53
|
+
kind: "picoclaw"
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
case "tinyclaw": {
|
|
57
|
+
const channel = createTinyClawChannel(networkId, agentId);
|
|
58
|
+
return {
|
|
59
|
+
ack_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses`,
|
|
60
|
+
channel,
|
|
61
|
+
inbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/message`,
|
|
62
|
+
kind: "tinyclaw",
|
|
63
|
+
outbound_url: `http://127.0.0.1:${DEFAULT_TINYCLAW_PORT}/api/responses/pending?channel=${encodeURIComponent(channel)}`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
throw new SpawnfileError("compile_error", `Moltnet does not know how to attach runtime ${agentNode.runtime.name} directly`);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
-
import { DocsBlock, McpServer, Secret,
|
|
1
|
+
import { DocsBlock, McpServer, Secret, SkillReference } from "../manifest/index.js";
|
|
2
2
|
import { StringMap } from "../shared/index.js";
|
|
3
|
-
import { ResolvedDocument, ResolvedSkill } from "./types.js";
|
|
3
|
+
import { ResolvedDocument, ResolvedPackage, ResolvedSkill } from "./types.js";
|
|
4
4
|
export declare const mergeSkills: (sharedSkills?: SkillReference[], localSkills?: SkillReference[]) => SkillReference[];
|
|
5
5
|
export declare const mergeResolvedSkills: (sharedSkills?: ResolvedSkill[], localSkills?: ResolvedSkill[]) => ResolvedSkill[];
|
|
6
|
+
export declare const mergePackages: (sharedPackages?: ResolvedPackage[], localPackages?: ResolvedPackage[]) => ResolvedPackage[];
|
|
6
7
|
export declare const mergeMcpServers: (sharedServers?: McpServer[], localServers?: McpServer[]) => McpServer[];
|
|
7
8
|
export declare const mergeSecrets: (sharedSecrets?: Secret[], localSecrets?: Secret[]) => Secret[];
|
|
8
9
|
export declare const mergeEnv: (sharedEnv?: StringMap, localEnv?: StringMap) => StringMap;
|
|
9
10
|
export declare const loadResolvedDocuments: (manifestPath: string, docs: DocsBlock | undefined) => Promise<ResolvedDocument[]>;
|
|
10
11
|
export declare const loadResolvedSkills: (manifestPath: string, skills?: SkillReference[]) => Promise<ResolvedSkill[]>;
|
|
11
|
-
export declare const mergeSharedSurface: (shared: SharedSurface | undefined, local: {
|
|
12
|
-
env: StringMap | undefined;
|
|
13
|
-
mcpServers: McpServer[] | undefined;
|
|
14
|
-
secrets: Secret[] | undefined;
|
|
15
|
-
skills: SkillReference[] | undefined;
|
|
16
|
-
}) => {
|
|
17
|
-
env: StringMap;
|
|
18
|
-
mcpServers: McpServer[];
|
|
19
|
-
secrets: Secret[];
|
|
20
|
-
skills: SkillReference[];
|
|
21
|
-
};
|
|
@@ -12,6 +12,7 @@ const mergeByKey = (sharedValues, localValues, getKey) => {
|
|
|
12
12
|
};
|
|
13
13
|
export const mergeSkills = (sharedSkills = [], localSkills = []) => mergeByKey(sharedSkills, localSkills, (skill) => skill.ref);
|
|
14
14
|
export const mergeResolvedSkills = (sharedSkills = [], localSkills = []) => mergeByKey(sharedSkills, localSkills, (skill) => skill.ref);
|
|
15
|
+
export const mergePackages = (sharedPackages = [], localPackages = []) => mergeByKey(sharedPackages, localPackages, (pkg) => `${pkg.manager}::${pkg.name}`);
|
|
15
16
|
export const mergeMcpServers = (sharedServers = [], localServers = []) => mergeByKey(sharedServers, localServers, (server) => server.name);
|
|
16
17
|
export const mergeSecrets = (sharedSecrets = [], localSecrets = []) => mergeByKey(sharedSecrets, localSecrets, (secret) => secret.name);
|
|
17
18
|
export const mergeEnv = (sharedEnv = {}, localEnv = {}) => ({
|
|
@@ -51,9 +52,3 @@ export const loadResolvedSkills = async (manifestPath, skills = []) => Promise.a
|
|
|
51
52
|
sourcePath
|
|
52
53
|
};
|
|
53
54
|
}));
|
|
54
|
-
export const mergeSharedSurface = (shared, local) => ({
|
|
55
|
-
env: mergeEnv(shared?.env, local.env),
|
|
56
|
-
mcpServers: mergeMcpServers(shared?.mcp_servers, local.mcpServers),
|
|
57
|
-
secrets: mergeSecrets(shared?.secrets, local.secrets),
|
|
58
|
-
skills: mergeSkills(shared?.skills, local.skills)
|
|
59
|
-
});
|
|
@@ -3,6 +3,7 @@ import { readUtf8File } from "../filesystem/index.js";
|
|
|
3
3
|
import { SpawnfileError } from "../shared/index.js";
|
|
4
4
|
import { listAgentSurfaceSecretNames } from "./agentSurfaces.js";
|
|
5
5
|
import { buildCompilePlan } from "./buildCompilePlan.js";
|
|
6
|
+
import { listMoltnetNetworkSecretNames } from "./compilePlanHelpers.js";
|
|
6
7
|
import { listExecutionModelSecretNames, resolveExecutionModelAuthMethods } from "./modelEnv.js";
|
|
7
8
|
const resolveAuthRequirements = async (inputPath) => {
|
|
8
9
|
const plan = await buildCompilePlan(inputPath);
|
|
@@ -23,11 +24,24 @@ const resolveAuthRequirements = async (inputPath) => {
|
|
|
23
24
|
for (const secret of node.value.shared.secrets) {
|
|
24
25
|
addProjectSecret(secret);
|
|
25
26
|
}
|
|
27
|
+
for (const server of node.value.shared.mcpServers) {
|
|
28
|
+
if (server.auth?.secret) {
|
|
29
|
+
addProjectSecret({ name: server.auth.secret, required: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const secretName of listMoltnetNetworkSecretNames([node])) {
|
|
33
|
+
addProjectSecret({ name: secretName, required: true });
|
|
34
|
+
}
|
|
26
35
|
continue;
|
|
27
36
|
}
|
|
28
37
|
for (const secret of node.value.secrets) {
|
|
29
38
|
addProjectSecret(secret);
|
|
30
39
|
}
|
|
40
|
+
for (const server of node.value.mcpServers) {
|
|
41
|
+
if (server.auth?.secret) {
|
|
42
|
+
addProjectSecret({ name: server.auth.secret, required: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
31
45
|
for (const method of Object.values(resolveExecutionModelAuthMethods(node.value.execution))) {
|
|
32
46
|
methods.add(method);
|
|
33
47
|
}
|