spawnfile 0.1.1 → 0.1.3
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 +80 -396
- package/dist/cli/index.js +0 -0
- package/dist/cli/modelCommands.d.ts +3 -0
- package/dist/cli/modelCommands.js +68 -0
- package/dist/cli/runCli.d.ts +23 -2
- package/dist/cli/runCli.js +78 -122
- package/dist/cli/runtimeCommands.d.ts +3 -0
- package/dist/cli/runtimeCommands.js +20 -0
- package/dist/cli/surfaceCommands.d.ts +3 -0
- package/dist/cli/surfaceCommands.js +98 -0
- package/dist/cli/viewCommand.d.ts +3 -0
- package/dist/cli/viewCommand.js +87 -0
- package/dist/compiler/agentSurfaces.js +51 -5
- package/dist/compiler/buildCompilePlan.js +38 -40
- package/dist/compiler/buildCompilePlanRuntime.d.ts +14 -0
- package/dist/compiler/buildCompilePlanRuntime.js +39 -0
- package/dist/compiler/buildCompilePlanTeams.d.ts +5 -0
- package/dist/compiler/buildCompilePlanTeams.js +38 -0
- package/dist/compiler/compilePlanHelpers.js +4 -1
- package/dist/compiler/compileProject.js +62 -13
- package/dist/compiler/compileProjectSupport.d.ts +17 -0
- package/dist/compiler/compileProjectSupport.js +136 -0
- package/dist/compiler/containerArtifacts.d.ts +6 -1
- package/dist/compiler/containerArtifacts.js +26 -4
- package/dist/compiler/containerArtifactsPlans.js +16 -1
- package/dist/compiler/containerArtifactsRender.d.ts +4 -2
- package/dist/compiler/containerArtifactsRender.js +21 -126
- package/dist/compiler/containerArtifactsTypes.d.ts +7 -0
- package/dist/compiler/containerEntrypointRender.d.ts +12 -0
- package/dist/compiler/containerEntrypointRender.js +186 -0
- package/dist/compiler/index.d.ts +4 -0
- package/dist/compiler/index.js +4 -0
- package/dist/compiler/interactiveSurfaceScopes.d.ts +2 -0
- package/dist/compiler/interactiveSurfaceScopes.js +21 -0
- package/dist/compiler/moltnetArtifacts.d.ts +27 -0
- package/dist/compiler/moltnetArtifacts.js +208 -0
- package/dist/compiler/moltnetBinaries.d.ts +4 -0
- package/dist/compiler/moltnetBinaries.js +103 -0
- package/dist/compiler/moltnetClientConfig.d.ts +11 -0
- package/dist/compiler/moltnetClientConfig.js +89 -0
- package/dist/compiler/moltnetRepresentativeResolution.d.ts +16 -0
- package/dist/compiler/moltnetRepresentativeResolution.js +86 -0
- package/dist/compiler/moltnetResolution.d.ts +3 -0
- package/dist/compiler/moltnetResolution.js +182 -0
- package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
- package/dist/compiler/moltnetRoomMemberships.js +140 -0
- package/dist/compiler/runProject.js +1 -1
- package/dist/compiler/surfaceDefinitions.d.ts +55 -0
- package/dist/compiler/surfaceDefinitions.js +204 -0
- package/dist/compiler/teamContextHelpers.d.ts +18 -0
- package/dist/compiler/teamContextHelpers.js +112 -0
- package/dist/compiler/teamContextSupport.d.ts +4 -0
- package/dist/compiler/teamContextSupport.js +264 -0
- package/dist/compiler/teamContextSupport.testHelpers.d.ts +16 -0
- package/dist/compiler/teamContextSupport.testHelpers.js +68 -0
- package/dist/compiler/teamContextTypes.d.ts +28 -0
- package/dist/compiler/teamContextTypes.js +1 -0
- package/dist/compiler/teamRoster.d.ts +12 -0
- package/dist/compiler/teamRoster.js +48 -0
- package/dist/compiler/teamRosterEntries.d.ts +13 -0
- package/dist/compiler/teamRosterEntries.js +230 -0
- package/dist/compiler/teamRosterTypes.d.ts +45 -0
- package/dist/compiler/teamRosterTypes.js +1 -0
- package/dist/compiler/types.d.ts +90 -6
- package/dist/compiler/updateProjectRuntime.d.ts +9 -0
- package/dist/compiler/updateProjectRuntime.js +67 -0
- package/dist/compiler/updateProjectSurfaces.d.ts +8 -0
- package/dist/compiler/updateProjectSurfaces.js +106 -0
- package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
- package/dist/compiler/view/buildOrganizationView.js +180 -0
- package/dist/compiler/view/index.d.ts +4 -0
- package/dist/compiler/view/index.js +4 -0
- package/dist/compiler/view/renderNetworks.d.ts +2 -0
- package/dist/compiler/view/renderNetworks.js +93 -0
- package/dist/compiler/view/renderTree.d.ts +2 -0
- package/dist/compiler/view/renderTree.js +59 -0
- package/dist/compiler/view/sourcePaths.d.ts +2 -0
- package/dist/compiler/view/sourcePaths.js +19 -0
- package/dist/compiler/view/types.d.ts +80 -0
- package/dist/compiler/view/types.js +1 -0
- package/dist/manifest/loadManifest.js +4 -4
- package/dist/manifest/renderSpawnfile.js +74 -8
- package/dist/manifest/scaffold.js +1 -3
- package/dist/manifest/schemas.d.ts +227 -17
- package/dist/manifest/schemas.js +62 -20
- package/dist/manifest/surfaceSchemas.d.ts +154 -0
- package/dist/manifest/surfaceSchemas.js +77 -5
- package/dist/runtime/common.js +3 -0
- package/dist/runtime/openclaw/adapter.js +38 -5
- package/dist/runtime/openclaw/moltnet.d.ts +12 -0
- package/dist/runtime/openclaw/moltnet.js +124 -0
- package/dist/runtime/openclaw/surfaces.js +3 -0
- package/dist/runtime/picoclaw/adapter.js +27 -8
- package/dist/runtime/picoclaw/pico.d.ts +2 -0
- package/dist/runtime/picoclaw/pico.js +2 -0
- package/dist/runtime/picoclaw/surfaces.js +11 -0
- package/dist/runtime/tinyclaw/adapter.js +22 -8
- package/dist/runtime/tinyclaw/runAuth.js +28 -1
- package/dist/runtime/tinyclaw/surfaces.js +8 -0
- package/dist/runtime/types.d.ts +11 -0
- package/package.json +5 -3
- package/runtimes.yaml +4 -4
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { getRuntimeAdapter } from "../runtime/index.js";
|
|
2
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
3
|
+
import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
|
|
4
|
+
const DEFAULT_MOLTNET_PORT = 8787;
|
|
5
|
+
const DEFAULT_TINYCLAW_PORT = 3777;
|
|
6
|
+
const ROOTFS_PREFIX = "container/rootfs";
|
|
7
|
+
const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
|
|
8
|
+
const CONFIG_FILE_PLACEHOLDER = "<config-file>";
|
|
9
|
+
const createServerKey = (networkId) => networkId;
|
|
10
|
+
const createBridgeConfigPath = (teamSlug, networkId, agentId) => `${ROOTFS_PREFIX}/var/lib/spawnfile/moltnet/bridges/${teamSlug}-${networkId}-${agentId}.json`;
|
|
11
|
+
const resolveSequentialRuntimePort = (plan, runtimeName, slug) => {
|
|
12
|
+
const adapter = getRuntimeAdapter(runtimeName);
|
|
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
|
+
};
|
|
75
|
+
export const generateMoltnetArtifacts = async (plan) => {
|
|
76
|
+
const teamNodes = plan.nodes
|
|
77
|
+
.filter((node) => node.kind === "team")
|
|
78
|
+
.filter((node) => (node.value.networks?.length ?? 0) > 0);
|
|
79
|
+
if (teamNodes.length === 0) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const serverPlans = new Map();
|
|
83
|
+
let nextPort = DEFAULT_MOLTNET_PORT;
|
|
84
|
+
for (const teamNode of teamNodes) {
|
|
85
|
+
for (const network of teamNode.value.networks ?? []) {
|
|
86
|
+
const serverKey = createServerKey(network.id);
|
|
87
|
+
const existingPlan = serverPlans.get(serverKey);
|
|
88
|
+
if (existingPlan) {
|
|
89
|
+
for (const room of network.rooms) {
|
|
90
|
+
const concreteMembers = listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room);
|
|
91
|
+
const existingRoom = existingPlan.rooms.find((entry) => entry.id === room.id);
|
|
92
|
+
if (existingRoom) {
|
|
93
|
+
existingRoom.members = [
|
|
94
|
+
...new Set([...existingRoom.members, ...concreteMembers])
|
|
95
|
+
].sort();
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
existingPlan.rooms.push({
|
|
99
|
+
id: room.id,
|
|
100
|
+
members: concreteMembers
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
existingPlan.rooms.sort((left, right) => left.id.localeCompare(right.id));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
serverPlans.set(serverKey, {
|
|
108
|
+
id: `${teamNode.slug}-${network.id}`,
|
|
109
|
+
name: network.name,
|
|
110
|
+
networkId: network.id,
|
|
111
|
+
port: nextPort,
|
|
112
|
+
rooms: network.rooms.map((room) => ({
|
|
113
|
+
id: room.id,
|
|
114
|
+
members: listConcreteMoltnetRoomMemberIds(plan, teamNode.value, network.id, room)
|
|
115
|
+
})),
|
|
116
|
+
teamSource: teamNode.value.source
|
|
117
|
+
});
|
|
118
|
+
nextPort += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const bridgePlans = [];
|
|
123
|
+
const bridgePlanKeys = new Set();
|
|
124
|
+
const configFiles = [];
|
|
125
|
+
for (const node of plan.nodes) {
|
|
126
|
+
if (node.kind !== "agent") {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const agentNode = node.value;
|
|
130
|
+
if (!agentNode.surfaces?.moltnet || agentNode.surfaces.moltnet.length === 0) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
for (const attachment of agentNode.surfaces.moltnet) {
|
|
134
|
+
if (!attachment.teamSource || !attachment.memberId) {
|
|
135
|
+
throw new SpawnfileError("validation_error", `Agent ${agentNode.name} Moltnet attachments require a team-bound network context`);
|
|
136
|
+
}
|
|
137
|
+
const teamNode = teamNodes.find((team) => team.value.source === attachment.teamSource);
|
|
138
|
+
if (!teamNode) {
|
|
139
|
+
throw new SpawnfileError("validation_error", `Unable to find team context for Moltnet attachment ${attachment.network} on ${agentNode.name}`);
|
|
140
|
+
}
|
|
141
|
+
const serverPlan = serverPlans.get(createServerKey(attachment.network));
|
|
142
|
+
if (!serverPlan) {
|
|
143
|
+
throw new SpawnfileError("validation_error", `Unable to find Moltnet network ${attachment.network} for ${agentNode.name}`);
|
|
144
|
+
}
|
|
145
|
+
const configPath = createBridgeConfigPath(teamNode.slug, attachment.network, attachment.memberId);
|
|
146
|
+
const bridgePlanKey = `${attachment.network}::${attachment.memberId}`;
|
|
147
|
+
if (bridgePlanKeys.has(bridgePlanKey)) {
|
|
148
|
+
throw new SpawnfileError("validation_error", `Duplicate Moltnet bridge attachment for ${attachment.network}/${attachment.memberId}`);
|
|
149
|
+
}
|
|
150
|
+
bridgePlanKeys.add(bridgePlanKey);
|
|
151
|
+
configFiles.push({
|
|
152
|
+
content: `${JSON.stringify({
|
|
153
|
+
version: "moltnet.bridge.v1",
|
|
154
|
+
agent: {
|
|
155
|
+
id: attachment.memberId,
|
|
156
|
+
name: agentNode.name
|
|
157
|
+
},
|
|
158
|
+
moltnet: {
|
|
159
|
+
base_url: `http://127.0.0.1:${serverPlan.port}`,
|
|
160
|
+
network_id: attachment.network
|
|
161
|
+
},
|
|
162
|
+
runtime: resolveRuntimeConfig(plan, agentNode, node.slug, attachment.network, attachment.memberId),
|
|
163
|
+
...(attachment.rooms
|
|
164
|
+
? {
|
|
165
|
+
rooms: Object.entries(attachment.rooms)
|
|
166
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
167
|
+
.map(([roomId, policy]) => ({
|
|
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 } : {})
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
: {})
|
|
183
|
+
}, null, 2)}\n`,
|
|
184
|
+
mode: 0o600,
|
|
185
|
+
path: configPath
|
|
186
|
+
});
|
|
187
|
+
bridgePlans.push({
|
|
188
|
+
agentId: attachment.memberId,
|
|
189
|
+
configPath: `/${configPath.replace(`${ROOTFS_PREFIX}/`, "")}`,
|
|
190
|
+
networkId: attachment.network,
|
|
191
|
+
runtime: agentNode.runtime.name
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
bridgePlans: bridgePlans.sort((left, right) => left.configPath.localeCompare(right.configPath)),
|
|
197
|
+
files: configFiles,
|
|
198
|
+
ports: [...new Set([...serverPlans.values()].map((plan) => plan.port))].sort((left, right) => left - right),
|
|
199
|
+
publishedPorts: [
|
|
200
|
+
...new Set(teamNodes
|
|
201
|
+
.flatMap((teamNode) => (teamNode.value.networks ?? []).map((network) => network.expose
|
|
202
|
+
? serverPlans.get(createServerKey(network.id))?.port
|
|
203
|
+
: undefined))
|
|
204
|
+
.filter((port) => port !== undefined))
|
|
205
|
+
].sort((left, right) => left - right),
|
|
206
|
+
serverPlans: [...serverPlans.values()].sort((left, right) => left.port - right.port)
|
|
207
|
+
};
|
|
208
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
|
|
2
|
+
export declare const MOLTNET_BINARY_NAMES: readonly ["moltnet"];
|
|
3
|
+
export declare const resolveMoltnetCliCommand: () => Promise<string>;
|
|
4
|
+
export declare const stageMoltnetBinaries: (outputDirectory: string) => Promise<boolean>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { chmod } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { ensureDirectory, fileExists } from "../filesystem/index.js";
|
|
6
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
8
|
+
const MOLTNET_CLI_ENV = "SPAWNFILE_MOLTNET_CLI";
|
|
9
|
+
const MOLTNET_RELEASE_DIR_ENV = "SPAWNFILE_MOLTNET_RELEASE_DIR";
|
|
10
|
+
const LOCAL_MOLTNET_CLI_PATH = path.resolve(process.cwd(), "moltnet", "bin", "moltnet");
|
|
11
|
+
const MOLTNET_TARGET_OS = "linux";
|
|
12
|
+
export const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
|
|
13
|
+
export const MOLTNET_BINARY_NAMES = ["moltnet"];
|
|
14
|
+
const resolveTargetArchitecture = () => {
|
|
15
|
+
switch (process.arch) {
|
|
16
|
+
case "arm64":
|
|
17
|
+
return "arm64";
|
|
18
|
+
case "x64":
|
|
19
|
+
return "amd64";
|
|
20
|
+
default:
|
|
21
|
+
throw new SpawnfileError("compile_error", `Moltnet container installs do not support host architecture ${process.arch}`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const createReleaseAssetName = (architecture) => `moltnet_${MOLTNET_TARGET_OS}_${architecture}.tar.gz`;
|
|
25
|
+
const isCommandNotFoundError = (error) => typeof error === "object" &&
|
|
26
|
+
error !== null &&
|
|
27
|
+
"code" in error &&
|
|
28
|
+
error.code === "ENOENT";
|
|
29
|
+
const validateMoltnetCli = async (command, sourceLabel) => {
|
|
30
|
+
try {
|
|
31
|
+
await execFile(command, ["version"]);
|
|
32
|
+
return command;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
36
|
+
throw new SpawnfileError("compile_error", `Unable to execute compiled Moltnet CLI from ${sourceLabel}: ${reason}. Install Moltnet with \`curl -fsSL https://moltnet.dev/install.sh | sh\` or set ${MOLTNET_CLI_ENV}.`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const resolveConfiguredReleaseDirectory = async () => {
|
|
40
|
+
const configuredDirectory = process.env[MOLTNET_RELEASE_DIR_ENV]?.trim();
|
|
41
|
+
if (!configuredDirectory) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (!(await fileExists(configuredDirectory))) {
|
|
45
|
+
throw new SpawnfileError("compile_error", `Moltnet release directory ${configuredDirectory} does not exist`);
|
|
46
|
+
}
|
|
47
|
+
return configuredDirectory;
|
|
48
|
+
};
|
|
49
|
+
const findPathMoltnetCli = async () => {
|
|
50
|
+
try {
|
|
51
|
+
await execFile("moltnet", ["version"]);
|
|
52
|
+
return "moltnet";
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isCommandNotFoundError(error)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
59
|
+
throw new SpawnfileError("compile_error", `Unable to execute compiled Moltnet CLI from PATH: ${reason}. Install Moltnet with \`curl -fsSL https://moltnet.dev/install.sh | sh\` or set ${MOLTNET_CLI_ENV}.`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
export const resolveMoltnetCliCommand = async () => {
|
|
63
|
+
const configuredCli = process.env[MOLTNET_CLI_ENV]?.trim();
|
|
64
|
+
if (configuredCli) {
|
|
65
|
+
return validateMoltnetCli(configuredCli, configuredCli);
|
|
66
|
+
}
|
|
67
|
+
const pathCli = await findPathMoltnetCli();
|
|
68
|
+
if (pathCli) {
|
|
69
|
+
return pathCli;
|
|
70
|
+
}
|
|
71
|
+
if (await fileExists(LOCAL_MOLTNET_CLI_PATH)) {
|
|
72
|
+
return validateMoltnetCli(LOCAL_MOLTNET_CLI_PATH, LOCAL_MOLTNET_CLI_PATH);
|
|
73
|
+
}
|
|
74
|
+
return validateMoltnetCli("moltnet", "PATH");
|
|
75
|
+
};
|
|
76
|
+
export const stageMoltnetBinaries = async (outputDirectory) => {
|
|
77
|
+
const releaseDirectory = await resolveConfiguredReleaseDirectory();
|
|
78
|
+
if (!releaseDirectory) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const architecture = resolveTargetArchitecture();
|
|
82
|
+
const releaseAssetPath = path.join(releaseDirectory, createReleaseAssetName(architecture));
|
|
83
|
+
if (!(await fileExists(releaseAssetPath))) {
|
|
84
|
+
throw new SpawnfileError("compile_error", `Moltnet release asset ${releaseAssetPath} does not exist. Build it with \`cd moltnet && make release-assets\` or set ${MOLTNET_RELEASE_DIR_ENV}.`);
|
|
85
|
+
}
|
|
86
|
+
const installDirectory = path.join(outputDirectory, MOLTNET_BIN_DIRECTORY);
|
|
87
|
+
await ensureDirectory(installDirectory);
|
|
88
|
+
try {
|
|
89
|
+
await execFile("tar", ["-C", installDirectory, "-xzf", releaseAssetPath]);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
93
|
+
throw new SpawnfileError("compile_error", `Unable to extract Moltnet release asset ${releaseAssetPath}: ${reason}`);
|
|
94
|
+
}
|
|
95
|
+
for (const binaryName of MOLTNET_BINARY_NAMES) {
|
|
96
|
+
const binaryPath = path.join(installDirectory, binaryName);
|
|
97
|
+
if (!(await fileExists(binaryPath))) {
|
|
98
|
+
throw new SpawnfileError("compile_error", `Moltnet release asset ${releaseAssetPath} did not contain ${binaryName}`);
|
|
99
|
+
}
|
|
100
|
+
await chmod(binaryPath, 0o755);
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EmittedFile } from "../runtime/index.js";
|
|
2
|
+
import type { MoltnetArtifacts } from "./moltnetArtifacts.js";
|
|
3
|
+
import type { ResolvedAgentNode } from "./types.js";
|
|
4
|
+
export interface MoltnetWorkspaceLayout {
|
|
5
|
+
clientConfigPath: string;
|
|
6
|
+
cliRuntime: string;
|
|
7
|
+
skillPaths: string[];
|
|
8
|
+
workspaceRootPath: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const resolveMoltnetWorkspaceLayout: (runtimeName: string, agentName: string) => MoltnetWorkspaceLayout;
|
|
11
|
+
export declare const createMoltnetClientConfigFiles: (node: ResolvedAgentNode, artifacts: MoltnetArtifacts) => EmittedFile[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
const GENERATED_SKILL_NAME = "moltnet";
|
|
3
|
+
const GENERATED_CONFIG_PATH = ".moltnet/config.json";
|
|
4
|
+
const createConfigContent = (node, attachments) => `${JSON.stringify({
|
|
5
|
+
version: "moltnet.client.v1",
|
|
6
|
+
agent: {
|
|
7
|
+
name: node.name,
|
|
8
|
+
runtime: node.runtime.name
|
|
9
|
+
},
|
|
10
|
+
attachments
|
|
11
|
+
}, null, 2)}\n`;
|
|
12
|
+
const findServerPlan = (artifacts, attachment) => {
|
|
13
|
+
const exactPlan = artifacts.serverPlans.find((serverPlan) => serverPlan.networkId === attachment.network &&
|
|
14
|
+
serverPlan.teamSource === attachment.teamSource);
|
|
15
|
+
const plan = exactPlan ?? artifacts.serverPlans.find((serverPlan) => serverPlan.networkId === attachment.network);
|
|
16
|
+
if (!plan) {
|
|
17
|
+
throw new SpawnfileError("compile_error", `Unable to resolve Moltnet server plan for ${attachment.network} on ${attachment.memberId ?? "unknown-agent"}`);
|
|
18
|
+
}
|
|
19
|
+
return plan;
|
|
20
|
+
};
|
|
21
|
+
const createAttachmentConfig = (node, artifacts, attachment) => {
|
|
22
|
+
if (!attachment.memberId) {
|
|
23
|
+
throw new SpawnfileError("compile_error", `Moltnet client config requires a resolved member id for ${node.name}`);
|
|
24
|
+
}
|
|
25
|
+
const serverPlan = findServerPlan(artifacts, attachment);
|
|
26
|
+
return {
|
|
27
|
+
agent_name: node.name,
|
|
28
|
+
auth: { mode: "none" },
|
|
29
|
+
base_url: `http://127.0.0.1:${serverPlan.port}`,
|
|
30
|
+
...(attachment.dms
|
|
31
|
+
? {
|
|
32
|
+
dms: {
|
|
33
|
+
enabled: attachment.dms.enabled,
|
|
34
|
+
...(attachment.dms.read ? { read: attachment.dms.read } : {}),
|
|
35
|
+
...(attachment.dms.reply ? { reply: attachment.dms.reply } : {})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
: {}),
|
|
39
|
+
member_id: attachment.memberId,
|
|
40
|
+
network_id: attachment.network,
|
|
41
|
+
runtime: node.runtime.name,
|
|
42
|
+
...(attachment.rooms
|
|
43
|
+
? {
|
|
44
|
+
rooms: Object.entries(attachment.rooms)
|
|
45
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
46
|
+
.map(([roomId, policy]) => ({
|
|
47
|
+
id: roomId,
|
|
48
|
+
...(policy.read ? { read: policy.read } : {}),
|
|
49
|
+
...(policy.reply ? { reply: policy.reply } : {})
|
|
50
|
+
}))
|
|
51
|
+
}
|
|
52
|
+
: {})
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export const resolveMoltnetWorkspaceLayout = (runtimeName, agentName) => {
|
|
56
|
+
if (runtimeName === "openclaw" || runtimeName === "picoclaw") {
|
|
57
|
+
return {
|
|
58
|
+
clientConfigPath: `workspace/${GENERATED_CONFIG_PATH}`,
|
|
59
|
+
cliRuntime: runtimeName,
|
|
60
|
+
skillPaths: [`workspace/skills/${GENERATED_SKILL_NAME}/SKILL.md`],
|
|
61
|
+
workspaceRootPath: "workspace"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (runtimeName === "tinyclaw") {
|
|
65
|
+
return {
|
|
66
|
+
clientConfigPath: `workspace/${agentName}/${GENERATED_CONFIG_PATH}`,
|
|
67
|
+
cliRuntime: runtimeName,
|
|
68
|
+
skillPaths: [
|
|
69
|
+
`workspace/${agentName}/.agents/skills/${GENERATED_SKILL_NAME}/SKILL.md`,
|
|
70
|
+
`workspace/${agentName}/.claude/skills/${GENERATED_SKILL_NAME}/SKILL.md`
|
|
71
|
+
],
|
|
72
|
+
workspaceRootPath: `workspace/${agentName}`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
throw new SpawnfileError("compile_error", `Moltnet client config does not know how to emit files for runtime ${runtimeName}`);
|
|
76
|
+
};
|
|
77
|
+
export const createMoltnetClientConfigFiles = (node, artifacts) => {
|
|
78
|
+
const attachments = node.surfaces?.moltnet;
|
|
79
|
+
if (!attachments || attachments.length === 0) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const layout = resolveMoltnetWorkspaceLayout(node.runtime.name, node.name);
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
content: createConfigContent(node, attachments.map((attachment) => createAttachmentConfig(node, artifacts, attachment))),
|
|
86
|
+
path: layout.clientConfigPath
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CompilePlan, ResolvedMoltnetAttachment, ResolvedTeamNetwork, ResolvedTeamNode } from "./types.js";
|
|
2
|
+
export interface MoltnetTeamContext {
|
|
3
|
+
memberId: string;
|
|
4
|
+
teamName: string;
|
|
5
|
+
teamSource: string;
|
|
6
|
+
networks: ResolvedTeamNetwork[];
|
|
7
|
+
}
|
|
8
|
+
export interface TeamRepresentativeResolution {
|
|
9
|
+
agentSource: string;
|
|
10
|
+
memberId: string;
|
|
11
|
+
path: string[];
|
|
12
|
+
sourceTeamName: string;
|
|
13
|
+
sourceTeamSource: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const resolveTeamRepresentatives: (plan: CompilePlan, teamNode: ResolvedTeamNode, seen?: string[]) => TeamRepresentativeResolution[];
|
|
16
|
+
export declare const resolveMoltnetAttachments: (attachments: ResolvedMoltnetAttachment[] | undefined, context: MoltnetTeamContext | undefined, nodeName: string) => ResolvedMoltnetAttachment[] | undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
const cloneAttachment = (attachment, context) => ({
|
|
3
|
+
...(attachment.dms ? { dms: { ...attachment.dms } } : {}),
|
|
4
|
+
memberId: context.memberId,
|
|
5
|
+
network: attachment.network,
|
|
6
|
+
...(attachment.rooms
|
|
7
|
+
? {
|
|
8
|
+
rooms: Object.fromEntries(Object.entries(attachment.rooms).map(([roomId, policy]) => [
|
|
9
|
+
roomId,
|
|
10
|
+
{ ...policy }
|
|
11
|
+
]))
|
|
12
|
+
}
|
|
13
|
+
: {}),
|
|
14
|
+
teamSource: context.teamSource
|
|
15
|
+
});
|
|
16
|
+
const findTeamBySource = (plan, source) => {
|
|
17
|
+
const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
|
|
18
|
+
if (!node || node.value.kind !== "team") {
|
|
19
|
+
throw new SpawnfileError("compile_error", `Unable to find team node at ${source}`);
|
|
20
|
+
}
|
|
21
|
+
return node.value;
|
|
22
|
+
};
|
|
23
|
+
export const resolveTeamRepresentatives = (plan, teamNode, seen = []) => {
|
|
24
|
+
if (seen.includes(teamNode.source)) {
|
|
25
|
+
throw new SpawnfileError("compile_error", `Cycle detected while resolving representatives for ${teamNode.name}`);
|
|
26
|
+
}
|
|
27
|
+
const selectedMemberIds = teamNode.externalExplicit
|
|
28
|
+
? teamNode.external
|
|
29
|
+
: teamNode.mode === "hierarchical" && teamNode.lead
|
|
30
|
+
? [teamNode.lead]
|
|
31
|
+
: teamNode.members.map((member) => member.id);
|
|
32
|
+
const nextSeen = [...seen, teamNode.source];
|
|
33
|
+
const representatives = [];
|
|
34
|
+
for (const memberId of selectedMemberIds) {
|
|
35
|
+
const member = teamNode.members.find((entry) => entry.id === memberId);
|
|
36
|
+
if (!member) {
|
|
37
|
+
throw new SpawnfileError("validation_error", `Team ${teamNode.name} representative interface references unknown member ${memberId}`);
|
|
38
|
+
}
|
|
39
|
+
if (member.kind === "agent") {
|
|
40
|
+
representatives.push({
|
|
41
|
+
agentSource: member.nodeSource,
|
|
42
|
+
memberId: member.id,
|
|
43
|
+
path: [member.id],
|
|
44
|
+
sourceTeamName: teamNode.name,
|
|
45
|
+
sourceTeamSource: teamNode.source
|
|
46
|
+
});
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const childTeam = findTeamBySource(plan, member.nodeSource);
|
|
50
|
+
const childRepresentatives = resolveTeamRepresentatives(plan, childTeam, nextSeen);
|
|
51
|
+
if (childRepresentatives.length === 0) {
|
|
52
|
+
throw new SpawnfileError("validation_error", `Team ${childTeam.name} does not resolve to a concrete representative for ${teamNode.name}`);
|
|
53
|
+
}
|
|
54
|
+
for (const representative of childRepresentatives) {
|
|
55
|
+
representatives.push({
|
|
56
|
+
...representative,
|
|
57
|
+
path: [member.id, ...representative.path]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return representatives;
|
|
62
|
+
};
|
|
63
|
+
export const resolveMoltnetAttachments = (attachments, context, nodeName) => {
|
|
64
|
+
if (!attachments || attachments.length === 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (!context) {
|
|
68
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} declares Moltnet surfaces but is not attached to a team network`);
|
|
69
|
+
}
|
|
70
|
+
return attachments.map((attachment) => {
|
|
71
|
+
const network = context.networks.find((entry) => entry.id === attachment.network);
|
|
72
|
+
if (!network) {
|
|
73
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} references unknown Moltnet network ${attachment.network} on team ${context.teamName}`);
|
|
74
|
+
}
|
|
75
|
+
for (const roomId of Object.keys(attachment.rooms ?? {})) {
|
|
76
|
+
const room = network.rooms.find((entry) => entry.id === roomId);
|
|
77
|
+
if (!room) {
|
|
78
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} references unknown Moltnet room ${roomId} on network ${network.id}`);
|
|
79
|
+
}
|
|
80
|
+
if (!room.members.includes(context.memberId)) {
|
|
81
|
+
throw new SpawnfileError("validation_error", `Agent ${nodeName} cannot attach to Moltnet room ${roomId} because member ${context.memberId} is not in that room`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return cloneAttachment(attachment, context);
|
|
85
|
+
});
|
|
86
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CompilePlan } from "./types.js";
|
|
2
|
+
export { resolveMoltnetAttachments, resolveTeamRepresentatives, type MoltnetTeamContext, type TeamRepresentativeResolution } from "./moltnetRepresentativeResolution.js";
|
|
3
|
+
export declare const resolvePlanMoltnetAttachments: (plan: CompilePlan) => void;
|