spawnfile 0.1.4 → 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.
Files changed (73) hide show
  1. package/dist/cli/lifecycleCommands.d.ts +3 -0
  2. package/dist/cli/lifecycleCommands.js +80 -0
  3. package/dist/cli/runCli.d.ts +2 -1
  4. package/dist/cli/runCli.js +4 -48
  5. package/dist/compiler/buildCompilePlan.js +12 -202
  6. package/dist/compiler/buildCompilePlanRuntime.d.ts +6 -1
  7. package/dist/compiler/buildCompilePlanRuntime.js +9 -0
  8. package/dist/compiler/buildCompilePlanTeams.js +16 -10
  9. package/dist/compiler/buildCompilePlanTraversal.d.ts +16 -0
  10. package/dist/compiler/buildCompilePlanTraversal.js +214 -0
  11. package/dist/compiler/buildCompilePlanTraversalHelpers.d.ts +18 -0
  12. package/dist/compiler/buildCompilePlanTraversalHelpers.js +22 -0
  13. package/dist/compiler/compilePlanHelpers.d.ts +3 -1
  14. package/dist/compiler/compilePlanHelpers.js +37 -1
  15. package/dist/compiler/compileProject.js +14 -0
  16. package/dist/compiler/containerArtifacts.js +18 -3
  17. package/dist/compiler/containerArtifactsPlans.js +32 -0
  18. package/dist/compiler/containerArtifactsRender.js +86 -3
  19. package/dist/compiler/containerArtifactsTypes.d.ts +7 -3
  20. package/dist/compiler/containerEntrypointRender.d.ts +1 -1
  21. package/dist/compiler/containerEntrypointRender.js +34 -24
  22. package/dist/compiler/containerTargetResources.d.ts +4 -0
  23. package/dist/compiler/containerTargetResources.js +54 -0
  24. package/dist/compiler/containerWorkspaceResourceRender.d.ts +3 -0
  25. package/dist/compiler/containerWorkspaceResourceRender.js +128 -0
  26. package/dist/compiler/executionDefaults.js +0 -3
  27. package/dist/compiler/helpers.d.ts +1 -1
  28. package/dist/compiler/index.d.ts +1 -0
  29. package/dist/compiler/index.js +1 -0
  30. package/dist/compiler/moltnetArtifacts.d.ts +11 -5
  31. package/dist/compiler/moltnetArtifacts.js +133 -117
  32. package/dist/compiler/moltnetClientConfig.js +8 -2
  33. package/dist/compiler/moltnetConfigLowering.d.ts +36 -0
  34. package/dist/compiler/moltnetConfigLowering.js +125 -0
  35. package/dist/compiler/moltnetRuntimeConfig.d.ts +2 -0
  36. package/dist/compiler/moltnetRuntimeConfig.js +69 -0
  37. package/dist/compiler/surfaces.d.ts +3 -13
  38. package/dist/compiler/surfaces.js +1 -6
  39. package/dist/compiler/syncProjectAuth.js +14 -0
  40. package/dist/compiler/types.d.ts +16 -1
  41. package/dist/compiler/upProject.d.ts +19 -0
  42. package/dist/compiler/upProject.js +37 -0
  43. package/dist/compiler/view/buildOrganizationView.js +22 -2
  44. package/dist/compiler/view/renderNetworks.js +14 -3
  45. package/dist/compiler/view/renderTree.js +8 -2
  46. package/dist/compiler/view/types.d.ts +18 -3
  47. package/dist/compiler/workspaceResources.d.ts +34 -0
  48. package/dist/compiler/workspaceResources.js +120 -0
  49. package/dist/manifest/executionSchemas.d.ts +106 -0
  50. package/dist/manifest/executionSchemas.js +140 -0
  51. package/dist/manifest/loadManifest.js +15 -27
  52. package/dist/manifest/renderSpawnfile.js +44 -52
  53. package/dist/manifest/renderSpawnfileNetworks.d.ts +2 -0
  54. package/dist/manifest/renderSpawnfileNetworks.js +63 -0
  55. package/dist/manifest/renderSpawnfileWorkspace.d.ts +2 -0
  56. package/dist/manifest/renderSpawnfileWorkspace.js +47 -0
  57. package/dist/manifest/scaffold.js +12 -6
  58. package/dist/manifest/scheduleSchemas.d.ts +15 -0
  59. package/dist/manifest/scheduleSchemas.js +26 -0
  60. package/dist/manifest/schemas.d.ts +626 -368
  61. package/dist/manifest/schemas.js +51 -191
  62. package/dist/manifest/teamNetworkSchemas.d.ts +228 -0
  63. package/dist/manifest/teamNetworkSchemas.js +295 -0
  64. package/dist/manifest/workspaceSchemas.d.ts +96 -0
  65. package/dist/manifest/workspaceSchemas.js +166 -0
  66. package/dist/report/types.d.ts +10 -0
  67. package/dist/runtime/common.d.ts +2 -1
  68. package/dist/runtime/common.js +3 -3
  69. package/dist/runtime/tinyclaw/adapter.js +9 -2
  70. package/dist/runtime/tinyclaw/schedules.d.ts +9 -0
  71. package/dist/runtime/tinyclaw/schedules.js +62 -0
  72. package/dist/runtime/types.d.ts +1 -0
  73. 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 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
- };
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
- id: `${teamNode.slug}-${network.id}`,
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: nextPort,
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
- nextPort += 1;
72
+ if (port) {
73
+ nextPort = Math.max(nextPort, port + 1);
74
+ }
119
75
  }
120
76
  }
121
77
  }
122
- const bridgePlans = [];
123
- const bridgePlanKeys = new Set();
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 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}`);
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
- bridgePlanKeys.add(bridgePlanKey);
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.bridge.v1",
154
- agent: {
155
- id: attachment.memberId,
156
- name: agentNode.name
157
- },
143
+ version: "moltnet.node.v1",
158
144
  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 } : {})
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
- bridgePlans.push({
188
- agentId: attachment.memberId,
189
- configPath: `/${configPath.replace(`${ROOTFS_PREFIX}/`, "")}`,
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
- ports: [...new Set([...serverPlans.values()].map((plan) => plan.port))].sort((left, right) => left - right),
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.expose
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: { mode: "none" },
29
- base_url: `http://127.0.0.1:${serverPlan.port}`,
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,2 @@
1
+ import type { CompilePlan, ResolvedAgentNode } from "./types.js";
2
+ export declare const resolveRuntimeConfig: (plan: CompilePlan, agentNode: ResolvedAgentNode, nodeSlug: string, networkId: string, agentId: string) => Record<string, string>;
@@ -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, SharedSurface, SkillReference } from "../manifest/index.js";
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
  }