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.
Files changed (102) hide show
  1. package/README.md +80 -396
  2. package/dist/cli/index.js +0 -0
  3. package/dist/cli/modelCommands.d.ts +3 -0
  4. package/dist/cli/modelCommands.js +68 -0
  5. package/dist/cli/runCli.d.ts +23 -2
  6. package/dist/cli/runCli.js +78 -122
  7. package/dist/cli/runtimeCommands.d.ts +3 -0
  8. package/dist/cli/runtimeCommands.js +20 -0
  9. package/dist/cli/surfaceCommands.d.ts +3 -0
  10. package/dist/cli/surfaceCommands.js +98 -0
  11. package/dist/cli/viewCommand.d.ts +3 -0
  12. package/dist/cli/viewCommand.js +87 -0
  13. package/dist/compiler/agentSurfaces.js +51 -5
  14. package/dist/compiler/buildCompilePlan.js +38 -40
  15. package/dist/compiler/buildCompilePlanRuntime.d.ts +14 -0
  16. package/dist/compiler/buildCompilePlanRuntime.js +39 -0
  17. package/dist/compiler/buildCompilePlanTeams.d.ts +5 -0
  18. package/dist/compiler/buildCompilePlanTeams.js +38 -0
  19. package/dist/compiler/compilePlanHelpers.js +4 -1
  20. package/dist/compiler/compileProject.js +62 -13
  21. package/dist/compiler/compileProjectSupport.d.ts +17 -0
  22. package/dist/compiler/compileProjectSupport.js +136 -0
  23. package/dist/compiler/containerArtifacts.d.ts +6 -1
  24. package/dist/compiler/containerArtifacts.js +26 -4
  25. package/dist/compiler/containerArtifactsPlans.js +16 -1
  26. package/dist/compiler/containerArtifactsRender.d.ts +4 -2
  27. package/dist/compiler/containerArtifactsRender.js +21 -126
  28. package/dist/compiler/containerArtifactsTypes.d.ts +7 -0
  29. package/dist/compiler/containerEntrypointRender.d.ts +12 -0
  30. package/dist/compiler/containerEntrypointRender.js +186 -0
  31. package/dist/compiler/index.d.ts +4 -0
  32. package/dist/compiler/index.js +4 -0
  33. package/dist/compiler/interactiveSurfaceScopes.d.ts +2 -0
  34. package/dist/compiler/interactiveSurfaceScopes.js +21 -0
  35. package/dist/compiler/moltnetArtifacts.d.ts +27 -0
  36. package/dist/compiler/moltnetArtifacts.js +208 -0
  37. package/dist/compiler/moltnetBinaries.d.ts +4 -0
  38. package/dist/compiler/moltnetBinaries.js +103 -0
  39. package/dist/compiler/moltnetClientConfig.d.ts +11 -0
  40. package/dist/compiler/moltnetClientConfig.js +89 -0
  41. package/dist/compiler/moltnetRepresentativeResolution.d.ts +16 -0
  42. package/dist/compiler/moltnetRepresentativeResolution.js +86 -0
  43. package/dist/compiler/moltnetResolution.d.ts +3 -0
  44. package/dist/compiler/moltnetResolution.js +182 -0
  45. package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
  46. package/dist/compiler/moltnetRoomMemberships.js +140 -0
  47. package/dist/compiler/runProject.js +1 -1
  48. package/dist/compiler/surfaceDefinitions.d.ts +55 -0
  49. package/dist/compiler/surfaceDefinitions.js +204 -0
  50. package/dist/compiler/teamContextHelpers.d.ts +18 -0
  51. package/dist/compiler/teamContextHelpers.js +112 -0
  52. package/dist/compiler/teamContextSupport.d.ts +4 -0
  53. package/dist/compiler/teamContextSupport.js +264 -0
  54. package/dist/compiler/teamContextSupport.testHelpers.d.ts +16 -0
  55. package/dist/compiler/teamContextSupport.testHelpers.js +68 -0
  56. package/dist/compiler/teamContextTypes.d.ts +28 -0
  57. package/dist/compiler/teamContextTypes.js +1 -0
  58. package/dist/compiler/teamRoster.d.ts +12 -0
  59. package/dist/compiler/teamRoster.js +48 -0
  60. package/dist/compiler/teamRosterEntries.d.ts +13 -0
  61. package/dist/compiler/teamRosterEntries.js +230 -0
  62. package/dist/compiler/teamRosterTypes.d.ts +45 -0
  63. package/dist/compiler/teamRosterTypes.js +1 -0
  64. package/dist/compiler/types.d.ts +90 -6
  65. package/dist/compiler/updateProjectRuntime.d.ts +9 -0
  66. package/dist/compiler/updateProjectRuntime.js +67 -0
  67. package/dist/compiler/updateProjectSurfaces.d.ts +8 -0
  68. package/dist/compiler/updateProjectSurfaces.js +106 -0
  69. package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
  70. package/dist/compiler/view/buildOrganizationView.js +180 -0
  71. package/dist/compiler/view/index.d.ts +4 -0
  72. package/dist/compiler/view/index.js +4 -0
  73. package/dist/compiler/view/renderNetworks.d.ts +2 -0
  74. package/dist/compiler/view/renderNetworks.js +93 -0
  75. package/dist/compiler/view/renderTree.d.ts +2 -0
  76. package/dist/compiler/view/renderTree.js +59 -0
  77. package/dist/compiler/view/sourcePaths.d.ts +2 -0
  78. package/dist/compiler/view/sourcePaths.js +19 -0
  79. package/dist/compiler/view/types.d.ts +80 -0
  80. package/dist/compiler/view/types.js +1 -0
  81. package/dist/manifest/loadManifest.js +4 -4
  82. package/dist/manifest/renderSpawnfile.js +74 -8
  83. package/dist/manifest/scaffold.js +1 -3
  84. package/dist/manifest/schemas.d.ts +227 -17
  85. package/dist/manifest/schemas.js +62 -20
  86. package/dist/manifest/surfaceSchemas.d.ts +154 -0
  87. package/dist/manifest/surfaceSchemas.js +77 -5
  88. package/dist/runtime/common.js +3 -0
  89. package/dist/runtime/openclaw/adapter.js +38 -5
  90. package/dist/runtime/openclaw/moltnet.d.ts +12 -0
  91. package/dist/runtime/openclaw/moltnet.js +124 -0
  92. package/dist/runtime/openclaw/surfaces.js +3 -0
  93. package/dist/runtime/picoclaw/adapter.js +27 -8
  94. package/dist/runtime/picoclaw/pico.d.ts +2 -0
  95. package/dist/runtime/picoclaw/pico.js +2 -0
  96. package/dist/runtime/picoclaw/surfaces.js +11 -0
  97. package/dist/runtime/tinyclaw/adapter.js +22 -8
  98. package/dist/runtime/tinyclaw/runAuth.js +28 -1
  99. package/dist/runtime/tinyclaw/surfaces.js +8 -0
  100. package/dist/runtime/types.d.ts +11 -0
  101. package/package.json +5 -3
  102. package/runtimes.yaml +4 -4
@@ -1,8 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { createRuntimeInstallRecipe } from "../runtime/index.js";
3
3
  import { SpawnfileError } from "../shared/index.js";
4
+ export { renderEntrypoint } from "./containerEntrypointRender.js";
5
+ import { MOLTNET_BIN_DIRECTORY, MOLTNET_BINARY_NAMES } from "./moltnetBinaries.js";
4
6
  const CONTAINER_ROOTFS_ROOT = "container/rootfs";
5
7
  const GATEWAY_PORT_PLACEHOLDER = "<gateway-port>";
8
+ const MOLTNET_INSTALL_SCRIPT_URL = "https://moltnet.dev/install.sh";
6
9
  const WORKSPACE_PLACEHOLDER = "<workspace-path>";
7
10
  const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
8
11
  const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
@@ -44,7 +47,7 @@ export const renderEnvExample = (variables) => {
44
47
  }
45
48
  return `${lines.join("\n").trimEnd()}\n`;
46
49
  };
47
- export const renderDockerfile = async (runtimePlans) => {
50
+ export const renderDockerfile = async (runtimePlans, options = {}) => {
48
51
  const runtimeNames = [...new Set(runtimePlans.map((plan) => plan.runtimeName))];
49
52
  const runtimeRecipes = await Promise.all(runtimeNames.map((runtimeName) => createRuntimeInstallRecipe(runtimeName)));
50
53
  const baseImage = selectBaseImage(runtimePlans);
@@ -52,20 +55,33 @@ export const renderDockerfile = async (runtimePlans) => {
52
55
  const systemDeps = [
53
56
  ...new Set([
54
57
  ...runtimePlans.flatMap((plan) => plan.meta.systemDeps),
58
+ ...(options.hasMoltnet && !options.hasStagedMoltnetBinaries
59
+ ? ["ca-certificates", "curl", "tar"]
60
+ : []),
55
61
  ...(needsJsonEnvWriter ? ["python3"] : [])
56
62
  ])
57
63
  ].sort();
58
64
  const globalNpmPackages = [
59
65
  ...new Set(runtimePlans.flatMap((plan) => plan.meta.globalNpmPackages ?? []))
60
66
  ].sort();
61
- const exposedPorts = [...new Set(runtimePlans.flatMap((plan) => (plan.port ? [plan.port] : [])))].sort((left, right) => left - right);
62
- const lines = [`FROM ${baseImage}`, "USER root", "", "WORKDIR /opt/spawnfile"];
67
+ const runtimePorts = runtimePlans.flatMap((plan) => plan.publishedPort ? [plan.publishedPort] : []);
68
+ const moltnetPorts = options.moltnetPublishedPorts ?? [];
69
+ const exposedPorts = [...new Set([...runtimePorts, ...moltnetPorts])].sort((left, right) => left - right);
70
+ const lines = [];
71
+ lines.push(`FROM ${baseImage}`);
72
+ lines.push("USER root", "", "WORKDIR /opt/spawnfile");
63
73
  if (systemDeps.length > 0) {
64
74
  lines.push(createPackageInstallCommand(systemDeps), "");
65
75
  }
66
76
  if (globalNpmPackages.length > 0) {
67
77
  lines.push(createNpmPackageInstallCommand(globalNpmPackages), "");
68
78
  }
79
+ if (options.hasMoltnet && options.hasStagedMoltnetBinaries) {
80
+ lines.push(`COPY ${MOLTNET_BIN_DIRECTORY}/ /usr/local/bin/`, `RUN chmod +x ${MOLTNET_BINARY_NAMES.map((binaryName) => `/usr/local/bin/${binaryName}`).join(" ")}`, "");
81
+ }
82
+ else if (options.hasMoltnet) {
83
+ lines.push(`RUN MOLTNET_INSTALL_DIR=/usr/local/bin sh -c ${shellQuote(`curl -fsSL ${MOLTNET_INSTALL_SCRIPT_URL} | sh`)}`, "");
84
+ }
69
85
  for (const recipe of runtimeRecipes) {
70
86
  for (const copyCommand of recipe.copyCommands) {
71
87
  lines.push(copyCommand);
@@ -76,7 +92,8 @@ export const renderDockerfile = async (runtimePlans) => {
76
92
  lines.push("");
77
93
  }
78
94
  lines.push('RUN if ! id -u spawnfile >/dev/null 2>&1; then useradd --create-home --home-dir /home/spawnfile --shell /bin/bash spawnfile; fi', "");
79
- lines.push("COPY container/rootfs/ /", "COPY .env.example /opt/spawnfile/.env.example", 'COPY entrypoint.sh /opt/spawnfile/entrypoint.sh', "RUN chmod +x /opt/spawnfile/entrypoint.sh", "RUN mkdir -p /var/lib/spawnfile && chown -R spawnfile:spawnfile /var/lib/spawnfile /opt/spawnfile");
95
+ lines.push("COPY container/rootfs/ /", "COPY .env.example /opt/spawnfile/.env.example", 'COPY entrypoint.sh /opt/spawnfile/entrypoint.sh', "RUN chmod +x /opt/spawnfile/entrypoint.sh");
96
+ lines.push("RUN mkdir -p /var/lib/spawnfile && chown -R spawnfile:spawnfile /var/lib/spawnfile /opt/spawnfile");
80
97
  if (exposedPorts.length > 0) {
81
98
  lines.push(`EXPOSE ${exposedPorts.join(" ")}`);
82
99
  }
@@ -84,32 +101,6 @@ export const renderDockerfile = async (runtimePlans) => {
84
101
  lines.push('ENTRYPOINT ["/opt/spawnfile/entrypoint.sh"]');
85
102
  return `${lines.join("\n").trimEnd()}\n`;
86
103
  };
87
- const createEnvironmentAssignments = (plan) => {
88
- const envAssignments = [];
89
- if (plan.instancePaths.homePath) {
90
- envAssignments.push(`HOME=${shellQuote(plan.instancePaths.homePath)}`);
91
- }
92
- if (plan.meta.homeEnv && plan.instancePaths.homePath) {
93
- envAssignments.push(`${plan.meta.homeEnv}=${shellQuote(plan.instancePaths.homePath)}`);
94
- }
95
- if (plan.meta.configPathEnv) {
96
- envAssignments.push(`${plan.meta.configPathEnv}=${shellQuote(plan.instancePaths.configPath)}`);
97
- }
98
- if (plan.meta.portEnv && plan.port) {
99
- envAssignments.push(`${plan.meta.portEnv}=${shellQuote(String(plan.port))}`);
100
- }
101
- for (const [name, value] of Object.entries(plan.meta.staticEnv ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
102
- envAssignments.push(`${name}=${shellQuote(value)}`);
103
- }
104
- return envAssignments;
105
- };
106
- const createEnvFileWrites = (plan) => plan.envFiles.map((binding) => `write_env_file ${shellQuote(binding.envName)} ${shellQuote(binding.filePath)}`);
107
- const createConfigEnvWrites = (plan) => (plan.configEnvBindings ?? []).map((binding) => `apply_json_env_value ${shellQuote(plan.instancePaths.configPath)} ${shellQuote(binding.envName)} ${shellQuote(binding.jsonPath)}`);
108
- const resolveStartCommand = (plan) => plan.meta.startCommand
109
- .map((token) => token
110
- .replaceAll("<runtime-root>", plan.runtimeRoot)
111
- .replaceAll("<port>", plan.port ? String(plan.port) : ""))
112
- .filter((token) => token.length > 0);
113
104
  export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) => plan.targetFiles.map((file) => {
114
105
  if (file.path === plan.meta.configFileName) {
115
106
  return {
@@ -139,99 +130,3 @@ export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) =
139
130
  }
140
131
  throw new SpawnfileError("runtime_error", `Container target ${plan.id} for ${plan.runtimeName} emitted unsupported path ${file.path}`);
141
132
  }));
142
- export const renderEntrypoint = (runtimePlans, requiredSecrets) => {
143
- const lines = [
144
- "#!/usr/bin/env bash",
145
- "set -euo pipefail",
146
- "",
147
- "require_env() {",
148
- ' local name=\"$1\"',
149
- ' if [ -z \"${!name:-}\" ]; then',
150
- ' echo \"Missing required env: $name\" >&2',
151
- " exit 1",
152
- " fi",
153
- "}",
154
- "",
155
- "require_file() {",
156
- ' local target=\"$1\"',
157
- ' if [ ! -f \"$target\" ]; then',
158
- ' echo \"Missing required file: $target\" >&2',
159
- " exit 1",
160
- " fi",
161
- "}",
162
- "",
163
- "write_env_file() {",
164
- ' local name=\"$1\"',
165
- ' local target=\"$2\"',
166
- ' if [ -z \"${!name:-}\" ]; then',
167
- " return",
168
- " fi",
169
- ' mkdir -p \"$(dirname \"$target\")\"',
170
- ' printf %s \"${!name:-}\" > \"$target\"',
171
- "}",
172
- "",
173
- "apply_json_env_value() {",
174
- ' local target=\"$1\"',
175
- ' local name=\"$2\"',
176
- ' local json_path=\"$3\"',
177
- ' if [ -z \"${!name:-}\" ]; then',
178
- " return",
179
- " fi",
180
- " python3 - \"$target\" \"$name\" \"$json_path\" <<'PY'",
181
- "import json",
182
- "import os",
183
- "import sys",
184
- "",
185
- "target_path = sys.argv[1]",
186
- "env_name = sys.argv[2]",
187
- "json_path = sys.argv[3].split('.')",
188
- "value = os.environ.get(env_name)",
189
- "if value is None:",
190
- " raise SystemExit(0)",
191
- "",
192
- "with open(target_path, encoding='utf-8') as handle:",
193
- " data = json.load(handle)",
194
- "",
195
- "cursor = data",
196
- "for part in json_path[:-1]:",
197
- " child = cursor.get(part)",
198
- " if not isinstance(child, dict):",
199
- " child = {}",
200
- " cursor[part] = child",
201
- " cursor = child",
202
- "",
203
- "cursor[json_path[-1]] = value",
204
- "",
205
- "with open(target_path, 'w', encoding='utf-8') as handle:",
206
- " json.dump(data, handle, indent=2)",
207
- " handle.write('\\n')",
208
- "PY",
209
- "}",
210
- ""
211
- ];
212
- for (const secretName of requiredSecrets) {
213
- lines.push(`require_env ${shellQuote(secretName)}`);
214
- }
215
- if (requiredSecrets.length > 0) {
216
- lines.push("");
217
- }
218
- if (runtimePlans.length === 1) {
219
- const plan = runtimePlans[0];
220
- const commandTokens = resolveStartCommand(plan);
221
- const envAssignments = createEnvironmentAssignments(plan);
222
- const envFileWrites = createEnvFileWrites(plan);
223
- const configEnvWrites = createConfigEnvWrites(plan);
224
- lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...envFileWrites, ...configEnvWrites, `${envAssignments.join(" ")} exec ${commandTokens.map(shellQuote).join(" ")}`);
225
- return `${lines.join("\n").trimEnd()}\n`;
226
- }
227
- lines.push("PIDS=()", "", "terminate_children() {", ' for pid in "${PIDS[@]:-}"; do', ' kill "$pid" 2>/dev/null || true', " done", "}", "", "trap terminate_children INT TERM EXIT", "");
228
- for (const plan of runtimePlans) {
229
- const commandTokens = resolveStartCommand(plan);
230
- const envAssignments = createEnvironmentAssignments(plan);
231
- const envFileWrites = createEnvFileWrites(plan);
232
- const configEnvWrites = createConfigEnvWrites(plan);
233
- lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...envFileWrites, ...configEnvWrites, `${envAssignments.join(" ")} ${commandTokens.map(shellQuote).join(" ")} &`, 'PIDS+=("$!")', "");
234
- }
235
- lines.push('if [ "${#PIDS[@]}" -eq 0 ]; then', ' echo "No runtime targets were generated for this compile output" >&2', " exit 1", "fi", "", "status=0", 'for pid in "${PIDS[@]}"; do', ' if ! wait "$pid"; then', " status=1", " fi", "done", "", 'exit "$status"');
236
- return `${lines.join("\n").trimEnd()}\n`;
237
- };
@@ -2,6 +2,7 @@ import type { ContainerReport } from "../report/index.js";
2
2
  import type { EmittedFile, RuntimeContainerConfigEnvBinding, RuntimeContainerMeta } from "../runtime/index.js";
3
3
  import type { ModelAuthMethod } from "../shared/index.js";
4
4
  import type { ResolvedAgentNode, ResolvedTeamNode } from "./types.js";
5
+ import type { MoltnetBridgePlan, MoltnetServerPlan } from "./moltnetArtifacts.js";
5
6
  export interface ContainerEnvVariable {
6
7
  categories: Array<"model" | "project" | "runtime" | "surface">;
7
8
  description: string;
@@ -24,8 +25,10 @@ export interface RuntimeTargetPlan {
24
25
  modelAuthMethods: Record<string, ModelAuthMethod>;
25
26
  modelSecretsRequired: string[];
26
27
  port?: number;
28
+ publishedPort?: number;
27
29
  runtimeName: string;
28
30
  runtimeRoot: string;
31
+ targetConfigEnvBindings?: RuntimeContainerConfigEnvBinding[];
29
32
  targetFiles: EmittedFile[];
30
33
  }
31
34
  export interface CompiledNodeArtifact {
@@ -38,5 +41,9 @@ export interface CompiledNodeArtifact {
38
41
  export interface GeneratedContainerArtifacts {
39
42
  executablePaths: string[];
40
43
  files: EmittedFile[];
44
+ moltnet?: {
45
+ bridgePlans: MoltnetBridgePlan[];
46
+ serverPlans: MoltnetServerPlan[];
47
+ };
41
48
  report: ContainerReport;
42
49
  }
@@ -0,0 +1,12 @@
1
+ import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js";
2
+ import type { MoltnetArtifacts } from "./moltnetArtifacts.js";
3
+ export interface EntrypointOptions {
4
+ hasMoltnet?: boolean;
5
+ hasStagedMoltnetBinaries?: boolean;
6
+ moltnet?: {
7
+ bridgePlans: MoltnetArtifacts["bridgePlans"];
8
+ serverPlans: MoltnetArtifacts["serverPlans"];
9
+ };
10
+ moltnetPublishedPorts?: number[];
11
+ }
12
+ export declare const renderEntrypoint: (runtimePlans: RuntimeTargetPlan[], requiredSecrets: string[], options?: EntrypointOptions) => string;
@@ -0,0 +1,186 @@
1
+ import path from "node:path";
2
+ const MOLTNET_SERVER_DATA_DIRECTORY = "/var/lib/spawnfile/moltnet/servers";
3
+ const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
4
+ const pathSafeSegment = (value) => value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "server";
5
+ const createMoltnetServerDataPath = (serverId) => `${MOLTNET_SERVER_DATA_DIRECTORY}/${pathSafeSegment(serverId)}.db`;
6
+ const createEnvironmentAssignments = (plan) => {
7
+ const envAssignments = [];
8
+ if (plan.instancePaths.homePath) {
9
+ envAssignments.push(`HOME=${shellQuote(plan.instancePaths.homePath)}`);
10
+ }
11
+ if (plan.runtimeName === "tinyclaw" &&
12
+ plan.instancePaths.homePath &&
13
+ (plan.modelAuthMethods.openai === "api_key" || plan.modelAuthMethods.openai === "codex")) {
14
+ envAssignments.push(`CODEX_HOME=${shellQuote(path.posix.join(plan.instancePaths.homePath, ".codex"))}`);
15
+ }
16
+ if (plan.meta.homeEnv && plan.instancePaths.homePath) {
17
+ envAssignments.push(`${plan.meta.homeEnv}=${shellQuote(plan.instancePaths.homePath)}`);
18
+ }
19
+ if (plan.meta.configPathEnv) {
20
+ envAssignments.push(`${plan.meta.configPathEnv}=${shellQuote(plan.instancePaths.configPath)}`);
21
+ }
22
+ if (plan.meta.portEnv && plan.port) {
23
+ envAssignments.push(`${plan.meta.portEnv}=${shellQuote(String(plan.port))}`);
24
+ }
25
+ for (const [name, value] of Object.entries(plan.meta.staticEnv ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
26
+ envAssignments.push(`${name}=${shellQuote(value)}`);
27
+ }
28
+ return envAssignments;
29
+ };
30
+ const createEnvFileWrites = (plan) => plan.envFiles.map((binding) => `write_env_file ${shellQuote(binding.envName)} ${shellQuote(binding.filePath)}`);
31
+ const createConfigEnvWrites = (plan) => (plan.configEnvBindings ?? []).map((binding) => `apply_json_env_value ${shellQuote(plan.instancePaths.configPath)} ${shellQuote(binding.envName)} ${shellQuote(binding.jsonPath)}`);
32
+ const createAuthSetupCommands = (plan) => {
33
+ if (plan.runtimeName !== "tinyclaw" ||
34
+ !plan.instancePaths.homePath ||
35
+ plan.modelAuthMethods.openai !== "api_key") {
36
+ return [];
37
+ }
38
+ return [`configure_codex_api_key_auth ${shellQuote(plan.instancePaths.homePath)}`];
39
+ };
40
+ const resolveStartCommand = (plan) => plan.meta.startCommand
41
+ .map((token) => token
42
+ .replaceAll("<runtime-root>", plan.runtimeRoot)
43
+ .replaceAll("<port>", plan.port ? String(plan.port) : ""))
44
+ .filter((token) => token.length > 0);
45
+ const createRuntimeReadinessWait = (plan) => {
46
+ if (plan.runtimeName !== "openclaw" || !plan.port) {
47
+ return [];
48
+ }
49
+ return [
50
+ "attempts=0",
51
+ `until curl -sf ${shellQuote(`http://127.0.0.1:${plan.port}/healthz`)} >/dev/null; do`,
52
+ " attempts=$((attempts + 1))",
53
+ ' if [ "$attempts" -ge 180 ]; then',
54
+ ` echo ${shellQuote(`Timed out waiting for ${plan.runtimeName} on port ${plan.port}`)} >&2`,
55
+ " exit 1",
56
+ " fi",
57
+ " sleep 1",
58
+ "done",
59
+ ""
60
+ ];
61
+ };
62
+ export const renderEntrypoint = (runtimePlans, requiredSecrets, options = {}) => {
63
+ const lines = [
64
+ "#!/usr/bin/env bash",
65
+ "set -euo pipefail",
66
+ "",
67
+ "require_env() {",
68
+ ' local name=\"$1\"',
69
+ ' if [ -z \"${!name:-}\" ]; then',
70
+ ' echo \"Missing required env: $name\" >&2',
71
+ " exit 1",
72
+ " fi",
73
+ "}",
74
+ "",
75
+ "require_file() {",
76
+ ' local target=\"$1\"',
77
+ ' if [ ! -f \"$target\" ]; then',
78
+ ' echo \"Missing required file: $target\" >&2',
79
+ " exit 1",
80
+ " fi",
81
+ "}",
82
+ "",
83
+ "write_env_file() {",
84
+ ' local name=\"$1\"',
85
+ ' local target=\"$2\"',
86
+ ' if [ -z \"${!name:-}\" ]; then',
87
+ " return",
88
+ " fi",
89
+ ' mkdir -p \"$(dirname \"$target\")\"',
90
+ ' printf %s \"${!name:-}\" > \"$target\"',
91
+ "}",
92
+ "",
93
+ "apply_json_env_value() {",
94
+ ' local target=\"$1\"',
95
+ ' local name=\"$2\"',
96
+ ' local json_path=\"$3\"',
97
+ ' if [ -z \"${!name:-}\" ]; then',
98
+ " return",
99
+ " fi",
100
+ " python3 - \"$target\" \"$name\" \"$json_path\" <<'PY'",
101
+ "import json",
102
+ "import os",
103
+ "import sys",
104
+ "",
105
+ "target_path = sys.argv[1]",
106
+ "env_name = sys.argv[2]",
107
+ "json_path = sys.argv[3].split('.')",
108
+ "value = os.environ.get(env_name)",
109
+ "if value is None:",
110
+ " raise SystemExit(0)",
111
+ "",
112
+ "with open(target_path, encoding='utf-8') as handle:",
113
+ " data = json.load(handle)",
114
+ "",
115
+ "cursor = data",
116
+ "for part in json_path[:-1]:",
117
+ " child = cursor.get(part)",
118
+ " if not isinstance(child, dict):",
119
+ " child = {}",
120
+ " cursor[part] = child",
121
+ " cursor = child",
122
+ "",
123
+ "cursor[json_path[-1]] = value",
124
+ "",
125
+ "with open(target_path, 'w', encoding='utf-8') as handle:",
126
+ " json.dump(data, handle, indent=2)",
127
+ " handle.write('\\n')",
128
+ "PY",
129
+ "}",
130
+ "",
131
+ "configure_codex_api_key_auth() {",
132
+ ' local home_path="$1"',
133
+ ' if [ -z "${OPENAI_API_KEY:-}" ]; then',
134
+ " return",
135
+ " fi",
136
+ ' mkdir -p "$home_path/.codex"',
137
+ ' printf "%s\\n" "${OPENAI_API_KEY:-}" | HOME="$home_path" CODEX_HOME="$home_path/.codex" codex login --with-api-key >/dev/null',
138
+ "}",
139
+ ""
140
+ ];
141
+ lines.push('if [ -n "${OPENCLAW_GATEWAY_TOKEN:-}" ] && [ -z "${OPENCLAW_HOOKS_TOKEN:-}" ]; then', ' export OPENCLAW_HOOKS_TOKEN="hooks-${OPENCLAW_GATEWAY_TOKEN}"', "fi", "");
142
+ for (const secretName of requiredSecrets) {
143
+ lines.push(`require_env ${shellQuote(secretName)}`);
144
+ }
145
+ if (requiredSecrets.length > 0) {
146
+ lines.push("");
147
+ }
148
+ const moltnetServerPlans = options.moltnet?.serverPlans ?? [];
149
+ const moltnetBridgePlans = options.moltnet?.bridgePlans ?? [];
150
+ if (runtimePlans.length === 1 &&
151
+ moltnetServerPlans.length === 0 &&
152
+ moltnetBridgePlans.length === 0) {
153
+ const plan = runtimePlans[0];
154
+ const commandTokens = resolveStartCommand(plan);
155
+ const envAssignments = createEnvironmentAssignments(plan);
156
+ lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), ...createAuthSetupCommands(plan), `${envAssignments.join(" ")} exec ${commandTokens.map(shellQuote).join(" ")}`);
157
+ return `${lines.join("\n").trimEnd()}\n`;
158
+ }
159
+ lines.push("PIDS=()", "", "terminate_children() {", ' for pid in "${PIDS[@]:-}"; do', ' kill "$pid" 2>/dev/null || true', " done", "}", "", "trap terminate_children INT TERM EXIT", "");
160
+ if (moltnetServerPlans.length > 0) {
161
+ lines.push(`mkdir -p ${shellQuote(MOLTNET_SERVER_DATA_DIRECTORY)}`, "");
162
+ }
163
+ for (const serverPlan of moltnetServerPlans) {
164
+ lines.push(`MOLTNET_DATA_PATH=${shellQuote(createMoltnetServerDataPath(serverPlan.id))} MOLTNET_LISTEN_ADDR=${shellQuote(`:${serverPlan.port}`)} MOLTNET_NETWORK_ID=${shellQuote(serverPlan.networkId)} MOLTNET_NETWORK_NAME=${shellQuote(serverPlan.name)} /usr/local/bin/moltnet &`, 'PIDS+=("$!")', "");
165
+ }
166
+ for (const plan of runtimePlans) {
167
+ const commandTokens = resolveStartCommand(plan);
168
+ const envAssignments = createEnvironmentAssignments(plan);
169
+ lines.push(`mkdir -p ${shellQuote(plan.instancePaths.workspacePath)}`, `require_file ${shellQuote(plan.instancePaths.configPath)}`, ...createEnvFileWrites(plan), ...createConfigEnvWrites(plan), ...createAuthSetupCommands(plan), `${envAssignments.join(" ")} ${commandTokens.map(shellQuote).join(" ")} &`, 'PIDS+=("$!")', "", ...createRuntimeReadinessWait(plan));
170
+ }
171
+ for (const serverPlan of moltnetServerPlans) {
172
+ lines.push(`until curl -sf ${shellQuote(`http://127.0.0.1:${serverPlan.port}/healthz`)} >/dev/null; do sleep 1; done`);
173
+ for (const room of serverPlan.rooms) {
174
+ lines.push(`curl -sf -X POST -H 'Content-Type: application/json' -d ${shellQuote(JSON.stringify({
175
+ id: room.id,
176
+ members: room.members
177
+ }))} ${shellQuote(`http://127.0.0.1:${serverPlan.port}/v1/rooms`)} >/dev/null || true`);
178
+ }
179
+ lines.push("");
180
+ }
181
+ for (const bridgePlan of moltnetBridgePlans) {
182
+ lines.push(`/usr/local/bin/moltnet bridge ${shellQuote(bridgePlan.configPath)} &`, 'PIDS+=("$!")', "");
183
+ }
184
+ lines.push('if [ "${#PIDS[@]}" -eq 0 ]; then', ' echo "No runtime targets were generated for this compile output" >&2', " exit 1", "fi", "", "status=0", 'for pid in "${PIDS[@]}"; do', ' if ! wait "$pid"; then', " status=1", " fi", "done", "", 'exit "$status"');
185
+ return `${lines.join("\n").trimEnd()}\n`;
186
+ };
@@ -3,7 +3,11 @@ export * from "./buildCompilePlan.js";
3
3
  export * from "./buildProject.js";
4
4
  export * from "./compileProject.js";
5
5
  export * from "./initProject.js";
6
+ export * from "./moltnetRoomMemberships.js";
6
7
  export * from "./runProject.js";
7
8
  export * from "./syncProjectAuth.js";
8
9
  export * from "./types.js";
9
10
  export * from "./updateProjectModels.js";
11
+ export * from "./updateProjectRuntime.js";
12
+ export * from "./updateProjectSurfaces.js";
13
+ export * from "./view/index.js";
@@ -3,7 +3,11 @@ export * from "./buildCompilePlan.js";
3
3
  export * from "./buildProject.js";
4
4
  export * from "./compileProject.js";
5
5
  export * from "./initProject.js";
6
+ export * from "./moltnetRoomMemberships.js";
6
7
  export * from "./runProject.js";
7
8
  export * from "./syncProjectAuth.js";
8
9
  export * from "./types.js";
9
10
  export * from "./updateProjectModels.js";
11
+ export * from "./updateProjectRuntime.js";
12
+ export * from "./updateProjectSurfaces.js";
13
+ export * from "./view/index.js";
@@ -0,0 +1,2 @@
1
+ import type { ResolvedAgentSurfaces } from "./types.js";
2
+ export declare const listInteractiveSurfaceScopes: (surfaces: ResolvedAgentSurfaces | undefined) => string[];
@@ -0,0 +1,21 @@
1
+ const listMoltnetAttachmentScopes = (attachment) => {
2
+ const scopes = Object.keys(attachment.rooms ?? {})
3
+ .sort((left, right) => left.localeCompare(right))
4
+ .map((roomId) => `moltnet:${attachment.network}:room:${roomId}`);
5
+ if (attachment.dms?.enabled) {
6
+ scopes.push(`moltnet:${attachment.network}:dms`);
7
+ }
8
+ return scopes;
9
+ };
10
+ export const listInteractiveSurfaceScopes = (surfaces) => {
11
+ if (!surfaces) {
12
+ return [];
13
+ }
14
+ return [
15
+ ...(surfaces.discord ? ["discord"] : []),
16
+ ...(surfaces.moltnet?.flatMap(listMoltnetAttachmentScopes) ?? []),
17
+ ...(surfaces.slack ? ["slack"] : []),
18
+ ...(surfaces.telegram ? ["telegram"] : []),
19
+ ...(surfaces.whatsapp ? ["whatsapp"] : [])
20
+ ];
21
+ };
@@ -0,0 +1,27 @@
1
+ import type { EmittedFile } from "../runtime/index.js";
2
+ import type { CompilePlan } from "./types.js";
3
+ export interface MoltnetServerPlan {
4
+ id: string;
5
+ name: string;
6
+ networkId: string;
7
+ port: number;
8
+ rooms: Array<{
9
+ id: string;
10
+ members: string[];
11
+ }>;
12
+ teamSource: string;
13
+ }
14
+ export interface MoltnetBridgePlan {
15
+ agentId: string;
16
+ configPath: string;
17
+ networkId: string;
18
+ runtime: string;
19
+ }
20
+ export interface MoltnetArtifacts {
21
+ bridgePlans: MoltnetBridgePlan[];
22
+ files: EmittedFile[];
23
+ ports: number[];
24
+ publishedPorts: number[];
25
+ serverPlans: MoltnetServerPlan[];
26
+ }
27
+ export declare const generateMoltnetArtifacts: (plan: CompilePlan) => Promise<MoltnetArtifacts | null>;