spawnfile 0.1.3 → 0.1.5
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 +2 -0
- 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 +5 -47
- 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 +37 -27
- 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/runProject.d.ts +2 -0
- package/dist/compiler/runProject.js +20 -6
- package/dist/compiler/surfaces.d.ts +3 -13
- package/dist/compiler/surfaces.js +1 -6
- package/dist/compiler/syncProjectAuth.js +67 -19
- 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/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/picoclaw/adapter.js +7 -0
- package/dist/runtime/picoclaw/runAuth.js +5 -41
- 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
|
+
};
|
|
@@ -16,6 +16,7 @@ export interface RunProjectOptions extends CompileProjectOptions {
|
|
|
16
16
|
containerName?: string;
|
|
17
17
|
detach?: boolean;
|
|
18
18
|
dockerCommand?: string;
|
|
19
|
+
envFilePath?: string;
|
|
19
20
|
imageTag?: string;
|
|
20
21
|
runRunner?: DockerRunRunner;
|
|
21
22
|
}
|
|
@@ -29,6 +30,7 @@ export declare const createDockerRunInvocation: (compileResult: CompileProjectRe
|
|
|
29
30
|
containerName?: string;
|
|
30
31
|
detach?: boolean;
|
|
31
32
|
dockerCommand?: string;
|
|
33
|
+
envFilePath?: string;
|
|
32
34
|
}) => Promise<DockerRunInvocation>;
|
|
33
35
|
export declare const runDockerContainer: DockerRunRunner;
|
|
34
36
|
export declare const runProject: (inputPath: string, options?: RunProjectOptions) => Promise<RunProjectResult>;
|
|
@@ -3,8 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { mkdtemp } from "node:fs/promises";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { requireAuthProfile } from "../auth/index.js";
|
|
7
|
-
import { ensureDirectory, fileExists, removeDirectory, writeUtf8File } from "../filesystem/index.js";
|
|
6
|
+
import { parseEnvFile, requireAuthProfile } from "../auth/index.js";
|
|
7
|
+
import { ensureDirectory, fileExists, readUtf8File, removeDirectory, writeUtf8File } from "../filesystem/index.js";
|
|
8
8
|
import { SpawnfileError } from "../shared/index.js";
|
|
9
9
|
import { compileProject } from "./compileProject.js";
|
|
10
10
|
import { createDefaultImageTag } from "./buildProject.js";
|
|
@@ -47,9 +47,10 @@ const collectMissingRequiredSecrets = (containerReport, env, coveredModelSecrets
|
|
|
47
47
|
}
|
|
48
48
|
return [...missing].sort();
|
|
49
49
|
};
|
|
50
|
-
const resolveRunEnvironment = (containerReport, authProfile) => {
|
|
50
|
+
const resolveRunEnvironment = (containerReport, authProfile, envFileEnv = {}) => {
|
|
51
51
|
const env = {
|
|
52
|
-
...(authProfile?.env ?? {})
|
|
52
|
+
...(authProfile?.env ?? {}),
|
|
53
|
+
...envFileEnv
|
|
53
54
|
};
|
|
54
55
|
for (const name of new Set([...Object.keys(env), ...containerReport.secrets_required])) {
|
|
55
56
|
const processValue = process.env[name];
|
|
@@ -83,6 +84,18 @@ const renderDockerEnvFile = (env) => `${Object.entries(env)
|
|
|
83
84
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
84
85
|
.map(([name, value]) => `${name}=${value}`)
|
|
85
86
|
.join("\n")}\n`;
|
|
87
|
+
const readRunEnvFile = async (envFilePath) => {
|
|
88
|
+
if (!envFilePath) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return parseEnvFile(await readUtf8File(envFilePath));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
96
|
+
throw new SpawnfileError("validation_error", `Unable to read env file ${envFilePath}: ${reason}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
86
99
|
const resolveAuthMountArgs = async (containerReport, authProfile) => {
|
|
87
100
|
if (!authProfile || containerReport.runtime_homes.length === 0) {
|
|
88
101
|
return [];
|
|
@@ -110,7 +123,7 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
|
|
|
110
123
|
const envFilePath = path.join(supportDirectory, "run.env");
|
|
111
124
|
try {
|
|
112
125
|
assertDeclaredModelAuthSatisfied(containerReport, options.authProfile ?? null);
|
|
113
|
-
const env = resolveRunEnvironment(containerReport, options.authProfile ?? null);
|
|
126
|
+
const env = resolveRunEnvironment(containerReport, options.authProfile ?? null, await readRunEnvFile(options.envFilePath));
|
|
114
127
|
const preparedRuntimeAuth = await prepareRuntimeAuthMounts(compileResult.outputDirectory, containerReport, options.authProfile ?? null, env, supportDirectory);
|
|
115
128
|
assertRunEnvironmentSatisfied(containerReport, env, preparedRuntimeAuth.coveredModelSecrets);
|
|
116
129
|
await ensureDirectory(supportDirectory);
|
|
@@ -178,7 +191,8 @@ export const runProject = async (inputPath, options = {}) => {
|
|
|
178
191
|
authProfile,
|
|
179
192
|
containerName: options.containerName,
|
|
180
193
|
detach: options.detach,
|
|
181
|
-
dockerCommand: options.dockerCommand
|
|
194
|
+
dockerCommand: options.dockerCommand,
|
|
195
|
+
envFilePath: options.envFilePath
|
|
182
196
|
});
|
|
183
197
|
try {
|
|
184
198
|
await (options.runRunner ?? runDockerContainer)(invocation);
|