spawnfile 0.1.12 → 0.1.13

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 (63) hide show
  1. package/README.md +9 -2
  2. package/dist/auth/runtimeCredentials.js +18 -2
  3. package/dist/cli/devCommands.d.ts +3 -0
  4. package/dist/cli/devCommands.js +115 -0
  5. package/dist/cli/lifecycleCommands.js +2 -0
  6. package/dist/cli/runCli.d.ts +6 -0
  7. package/dist/cli/runCli.js +4 -0
  8. package/dist/cli/statusCommand.d.ts +2 -1
  9. package/dist/cli/statusCommand.js +50 -16
  10. package/dist/compiler/buildProject.d.ts +9 -1
  11. package/dist/compiler/buildProject.js +44 -5
  12. package/dist/compiler/compileProject.d.ts +2 -0
  13. package/dist/compiler/compileProject.js +3 -1
  14. package/dist/compiler/containerArtifactsRender.js +25 -69
  15. package/dist/compiler/containerEntrypointRender.js +4 -1
  16. package/dist/compiler/containerPackageRender.d.ts +15 -0
  17. package/dist/compiler/containerPackageRender.js +63 -0
  18. package/dist/compiler/containerTargetResources.js +3 -1
  19. package/dist/compiler/moltnetArtifacts.js +16 -1
  20. package/dist/compiler/moltnetBinaries.d.ts +4 -1
  21. package/dist/compiler/moltnetBinaries.js +24 -3
  22. package/dist/compiler/moltnetClientConfig.js +11 -0
  23. package/dist/compiler/moltnetRuntimeConfig.js +11 -0
  24. package/dist/compiler/runProject.js +7 -1
  25. package/dist/compiler/runProjectDocker.js +157 -12
  26. package/dist/compiler/upProject.js +1 -0
  27. package/dist/deployment/dockerRecover.d.ts +17 -0
  28. package/dist/deployment/dockerRecover.js +129 -0
  29. package/dist/deployment/index.d.ts +1 -0
  30. package/dist/deployment/index.js +1 -0
  31. package/dist/dev/activity.d.ts +14 -0
  32. package/dist/dev/activity.js +48 -0
  33. package/dist/dev/docker.d.ts +16 -0
  34. package/dist/dev/docker.js +71 -0
  35. package/dist/dev/index.d.ts +2 -0
  36. package/dist/dev/index.js +2 -0
  37. package/dist/dev/project.d.ts +35 -0
  38. package/dist/dev/project.js +164 -0
  39. package/dist/runtime/common.d.ts +2 -0
  40. package/dist/runtime/common.js +1 -1
  41. package/dist/runtime/container.d.ts +1 -0
  42. package/dist/runtime/container.js +93 -10
  43. package/dist/runtime/install.d.ts +1 -1
  44. package/dist/runtime/install.js +43 -1
  45. package/dist/runtime/pi/adapter.d.ts +4 -0
  46. package/dist/runtime/pi/adapter.js +198 -0
  47. package/dist/runtime/pi/appActivitySource.d.ts +1 -0
  48. package/dist/runtime/pi/appActivitySource.js +87 -0
  49. package/dist/runtime/pi/appCoreSource.d.ts +1 -0
  50. package/dist/runtime/pi/appCoreSource.js +354 -0
  51. package/dist/runtime/pi/appPreludeSource.d.ts +1 -0
  52. package/dist/runtime/pi/appPreludeSource.js +103 -0
  53. package/dist/runtime/pi/appSource.d.ts +1 -0
  54. package/dist/runtime/pi/appSource.js +8 -0
  55. package/dist/runtime/pi/appTemplate.d.ts +28 -0
  56. package/dist/runtime/pi/appTemplate.js +144 -0
  57. package/dist/runtime/pi/runAuth.d.ts +2 -0
  58. package/dist/runtime/pi/runAuth.js +68 -0
  59. package/dist/runtime/registry.js +3 -0
  60. package/dist/runtime/types.d.ts +1 -0
  61. package/dist/status/deployments.js +5 -12
  62. package/package.json +7 -2
  63. package/runtimes.yaml +27 -11
@@ -4,16 +4,15 @@ import { createRuntimeInstallRecipe } from "../runtime/index.js";
4
4
  import { SpawnfileError } from "../shared/index.js";
5
5
  export { renderEntrypoint } from "./containerEntrypointRender.js";
6
6
  import { MOLTNET_BIN_DIRECTORY, MOLTNET_BINARY_NAMES } from "./moltnetBinaries.js";
7
+ import { collectPackagesByManager, createAptPackageInstallItem, createNpmPackageInstallCommand, createNpmPackageInstallItem, createPackageInstallCommand, createPipxPackageInstallCommand, createPipxPackageInstallItem, getNpmInstallItemName } from "./containerPackageRender.js";
7
8
  const CONTAINER_ROOTFS_ROOT = "container/rootfs";
8
9
  const GATEWAY_PORT_PLACEHOLDER = "<gateway-port>";
9
10
  const MOLTNET_INSTALL_SCRIPT_URL = "https://moltnet.dev/install.sh";
10
11
  const MOLTNET_RELEASE_METADATA_URL = "https://api.github.com/repos/noopolis/moltnet/releases/latest";
11
12
  const WORKSPACE_PLACEHOLDER = "<workspace-path>";
13
+ const RUNTIME_ROOT_PLACEHOLDER = "<runtime-root>";
12
14
  const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
13
15
  const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
14
- const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
15
- const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
16
- const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
17
16
  const createStateOwnershipCommand = (persistentMountPaths = []) => {
18
17
  const mountPaths = [...new Set(persistentMountPaths)].sort();
19
18
  const mkdirPaths = [...new Set(["/var/lib/spawnfile", ...mountPaths])].sort();
@@ -21,7 +20,6 @@ const createStateOwnershipCommand = (persistentMountPaths = []) => {
21
20
  const chownPaths = [
22
21
  ...new Set([
23
22
  "/var/lib/spawnfile",
24
- "/opt/spawnfile",
25
23
  ...mountPaths.filter((mountPath) => !mountPath.startsWith("/var/lib/spawnfile/"))
26
24
  ])
27
25
  ].sort();
@@ -31,70 +29,13 @@ const createStateOwnershipCommand = (persistentMountPaths = []) => {
31
29
  `chown -R spawnfile:spawnfile ${chownPaths.map(shellQuote).join(" ")}`
32
30
  ].join(" && ");
33
31
  };
34
- const dedupePackages = (packages) => {
35
- const seen = new Map();
36
- for (const currentPackage of packages) {
37
- seen.set(`${currentPackage.manager}\u0000${currentPackage.name}\u0000${currentPackage.version ?? ""}\u0000${currentPackage.scope ?? ""}`, currentPackage);
38
- }
39
- return [...seen.values()].sort((left, right) => `${left.manager}\u0000${left.name}\u0000${left.version ?? ""}\u0000${left.scope ?? ""}`.localeCompare(`${right.manager}\u0000${right.name}\u0000${right.version ?? ""}\u0000${right.scope ?? ""}`));
40
- };
41
- const createPackageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
42
- const createPackageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
43
- const assertImagePackageCompatibility = (packages) => {
44
- const byName = new Map();
45
- for (const currentPackage of packages) {
46
- const conflictIdentity = createPackageConflictIdentity(currentPackage);
47
- const existingPackage = byName.get(conflictIdentity);
48
- if (!existingPackage) {
49
- byName.set(conflictIdentity, currentPackage);
50
- continue;
51
- }
52
- if (createPackageIdentity(existingPackage) !== createPackageIdentity(currentPackage)) {
53
- throw new SpawnfileError("validation_error", `Generated container declares conflicting package definitions for ${currentPackage.manager} package ${currentPackage.name}`);
54
- }
55
- }
56
- };
57
- const createAptPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}=${pkg.version}` : pkg.name;
58
- const createNpmPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
59
- const createPipxPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name;
60
- const getNpmInstallItemName = (item) => {
61
- if (!item.startsWith("@")) {
62
- const versionMarker = item.indexOf("@");
63
- return versionMarker === -1 ? item : item.slice(0, versionMarker);
64
- }
65
- const slashIndex = item.indexOf("/");
66
- if (slashIndex === -1) {
67
- return item;
68
- }
69
- const versionMarker = item.indexOf("@", slashIndex);
70
- return versionMarker === -1 ? item : item.slice(0, versionMarker);
71
- };
72
- const collectPackagesByManager = (runtimePlans) => {
73
- const packagesByManager = {
74
- apt: [],
75
- npm: [],
76
- pipx: []
77
- };
78
- const imagePackages = runtimePlans.flatMap((plan) => plan.packages ?? []);
79
- assertImagePackageCompatibility(imagePackages);
80
- const resolvedPackages = dedupePackages(imagePackages);
81
- for (const packageConfig of resolvedPackages) {
82
- if (packageConfig.manager === "apt") {
83
- packagesByManager.apt.push(packageConfig);
84
- continue;
85
- }
86
- if (packageConfig.manager === "npm") {
87
- packagesByManager.npm.push(packageConfig);
88
- continue;
89
- }
90
- if (packageConfig.manager === "pipx") {
91
- packagesByManager.pipx.push(packageConfig);
92
- continue;
93
- }
32
+ const selectBaseImage = (runtimePlans, runtimeRecipes) => {
33
+ const recipeBaseImages = [
34
+ ...new Set(runtimeRecipes.flatMap((recipe) => recipe.baseImage ? [recipe.baseImage] : []))
35
+ ];
36
+ if (recipeBaseImages.length === 1 && runtimePlans.length === runtimeRecipes.length) {
37
+ return recipeBaseImages[0];
94
38
  }
95
- return packagesByManager;
96
- };
97
- const selectBaseImage = (runtimePlans) => {
98
39
  const firstRuntimeMeta = runtimePlans[0]?.meta;
99
40
  if (runtimePlans.length <= 1) {
100
41
  return firstRuntimeMeta?.standaloneBaseImage ?? "debian:bookworm-slim";
@@ -133,12 +74,13 @@ export const renderEnvExample = (variables) => {
133
74
  export const renderDockerfile = async (runtimePlans, options = {}) => {
134
75
  const runtimeNames = [...new Set(runtimePlans.map((plan) => plan.runtimeName))];
135
76
  const runtimeRecipes = await Promise.all(runtimeNames.map((runtimeName) => createRuntimeInstallRecipe(runtimeName)));
136
- const baseImage = selectBaseImage(runtimePlans);
77
+ const recipeByRuntimeName = new Map(runtimeRecipes.map((recipe) => [recipe.runtimeName, recipe]));
78
+ const baseImage = selectBaseImage(runtimePlans, runtimeRecipes);
137
79
  const needsJsonEnvWriter = runtimePlans.some((plan) => (plan.configEnvBindings?.length ?? 0) > 0);
138
80
  const needsGit = runtimePlans.some((plan) => (plan.resources ?? []).some((resource) => resource.kind === "git"));
139
81
  const systemDeps = [
140
82
  ...new Set([
141
- ...runtimePlans.flatMap((plan) => plan.meta.systemDeps),
83
+ ...runtimePlans.flatMap((plan) => recipeByRuntimeName.get(plan.runtimeName)?.baseImage ? [] : plan.meta.systemDeps),
142
84
  ...(needsGit ? ["git"] : []),
143
85
  ...(options.hasMoltnet && !options.hasStagedMoltnetBinaries
144
86
  ? ["ca-certificates", "curl", "tar"]
@@ -195,6 +137,12 @@ export const renderDockerfile = async (runtimePlans, options = {}) => {
195
137
  }
196
138
  lines.push('RUN if ! id -u spawnfile >/dev/null 2>&1; then useradd --create-home --home-dir /home/spawnfile --shell /bin/bash spawnfile; fi', "");
197
139
  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");
140
+ const postRootfsCommands = [
141
+ ...new Set(runtimePlans.flatMap((plan) => plan.meta.postRootfsCommands ?? []))
142
+ ];
143
+ for (const command of postRootfsCommands) {
144
+ lines.push(`RUN ${command}`);
145
+ }
198
146
  if (options.distribution) {
199
147
  lines.push("", `COPY ${options.distribution.reportOutputFile} ${DISTRIBUTION_REPORT_IMAGE_PATH}`, ...Object.entries(options.distribution.labels)
200
148
  .sort(([left], [right]) => left.localeCompare(right))
@@ -213,11 +161,19 @@ export const createRootfsFiles = (runtimePlans) => runtimePlans.flatMap((plan) =
213
161
  return {
214
162
  content: file.content
215
163
  .replaceAll(WORKSPACE_PLACEHOLDER, plan.instancePaths.workspacePath)
164
+ .replaceAll(RUNTIME_ROOT_PLACEHOLDER, plan.runtimeRoot)
216
165
  .replaceAll(`"${GATEWAY_PORT_PLACEHOLDER}"`, plan.port ? String(plan.port) : "0")
217
166
  .replaceAll(GATEWAY_PORT_PLACEHOLDER, plan.port ? String(plan.port) : ""),
218
167
  path: `${CONTAINER_ROOTFS_ROOT}${plan.instancePaths.configPath}`
219
168
  };
220
169
  }
170
+ if (file.path.startsWith("runtime/")) {
171
+ const relativeRuntimePath = file.path.slice("runtime/".length);
172
+ return {
173
+ content: file.content,
174
+ path: `${CONTAINER_ROOTFS_ROOT}${path.posix.join(plan.runtimeRoot, relativeRuntimePath)}`
175
+ };
176
+ }
221
177
  if (file.path.startsWith("workspace/")) {
222
178
  const relativeWorkspacePath = file.path.slice("workspace/".length);
223
179
  return {
@@ -43,11 +43,14 @@ const createMoltnetStorePrepareCommands = (serverPlan) => {
43
43
  };
44
44
  const resolveStartCommand = (plan) => plan.meta.startCommand
45
45
  .map((token) => token
46
+ .replaceAll("<config-path>", plan.instancePaths.configPath)
47
+ .replaceAll("<home-path>", plan.instancePaths.homePath ?? "")
46
48
  .replaceAll("<runtime-root>", plan.runtimeRoot)
49
+ .replaceAll("<workspace-path>", plan.instancePaths.workspacePath)
47
50
  .replaceAll("<port>", plan.port ? String(plan.port) : ""))
48
51
  .filter((token) => token.length > 0);
49
52
  const createRuntimeReadinessWait = (plan) => {
50
- if (plan.runtimeName !== "openclaw" || !plan.port) {
53
+ if (!["openclaw", "pi"].includes(plan.runtimeName) || !plan.port) {
51
54
  return [];
52
55
  }
53
56
  return [
@@ -0,0 +1,15 @@
1
+ import type { RuntimeTargetPlan } from "./containerArtifactsTypes.js";
2
+ import type { ResolvedPackage } from "./types.js";
3
+ export interface PackagesByManager {
4
+ apt: ResolvedPackage[];
5
+ npm: ResolvedPackage[];
6
+ pipx: ResolvedPackage[];
7
+ }
8
+ export declare const createPackageInstallCommand: (packages: string[]) => string;
9
+ export declare const createNpmPackageInstallCommand: (packages: string[]) => string;
10
+ export declare const createPipxPackageInstallCommand: (packages: string[]) => string;
11
+ export declare const createAptPackageInstallItem: (pkg: ResolvedPackage) => string;
12
+ export declare const createNpmPackageInstallItem: (pkg: ResolvedPackage) => string;
13
+ export declare const createPipxPackageInstallItem: (pkg: ResolvedPackage) => string;
14
+ export declare const getNpmInstallItemName: (item: string) => string;
15
+ export declare const collectPackagesByManager: (runtimePlans: RuntimeTargetPlan[]) => PackagesByManager;
@@ -0,0 +1,63 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ export const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
3
+ export const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
4
+ export const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
5
+ const packageIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}\u0000${pkg.version ?? ""}\u0000${pkg.scope ?? ""}`;
6
+ const packageConflictIdentity = (pkg) => `${pkg.manager}\u0000${pkg.name}`;
7
+ const dedupePackages = (packages) => {
8
+ const seen = new Map();
9
+ for (const currentPackage of packages) {
10
+ seen.set(packageIdentity(currentPackage), currentPackage);
11
+ }
12
+ return [...seen.values()].sort((left, right) => packageIdentity(left).localeCompare(packageIdentity(right)));
13
+ };
14
+ const assertImagePackageCompatibility = (packages) => {
15
+ const byName = new Map();
16
+ for (const currentPackage of packages) {
17
+ const conflictIdentity = packageConflictIdentity(currentPackage);
18
+ const existingPackage = byName.get(conflictIdentity);
19
+ if (!existingPackage) {
20
+ byName.set(conflictIdentity, currentPackage);
21
+ continue;
22
+ }
23
+ if (packageIdentity(existingPackage) !== packageIdentity(currentPackage)) {
24
+ throw new SpawnfileError("validation_error", `Generated container declares conflicting package definitions for ${currentPackage.manager} package ${currentPackage.name}`);
25
+ }
26
+ }
27
+ };
28
+ export const createAptPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}=${pkg.version}` : pkg.name;
29
+ export const createNpmPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name;
30
+ export const createPipxPackageInstallItem = (pkg) => pkg.version ? `${pkg.name}==${pkg.version}` : pkg.name;
31
+ export const getNpmInstallItemName = (item) => {
32
+ if (!item.startsWith("@")) {
33
+ const versionMarker = item.indexOf("@");
34
+ return versionMarker === -1 ? item : item.slice(0, versionMarker);
35
+ }
36
+ const slashIndex = item.indexOf("/");
37
+ if (slashIndex === -1) {
38
+ return item;
39
+ }
40
+ const versionMarker = item.indexOf("@", slashIndex);
41
+ return versionMarker === -1 ? item : item.slice(0, versionMarker);
42
+ };
43
+ export const collectPackagesByManager = (runtimePlans) => {
44
+ const packagesByManager = {
45
+ apt: [],
46
+ npm: [],
47
+ pipx: []
48
+ };
49
+ const imagePackages = runtimePlans.flatMap((plan) => plan.packages ?? []);
50
+ assertImagePackageCompatibility(imagePackages);
51
+ for (const packageConfig of dedupePackages(imagePackages)) {
52
+ if (packageConfig.manager === "apt") {
53
+ packagesByManager.apt.push(packageConfig);
54
+ continue;
55
+ }
56
+ if (packageConfig.manager === "npm") {
57
+ packagesByManager.npm.push(packageConfig);
58
+ continue;
59
+ }
60
+ packagesByManager.pipx.push(packageConfig);
61
+ }
62
+ return packagesByManager;
63
+ };
@@ -4,13 +4,15 @@ import { mergeWorkspaceResourcePlans } from "./workspaceResources.js";
4
4
  const CONFIG_FILE_PLACEHOLDER = "<config-file>";
5
5
  const INSTANCE_ROOT_PLACEHOLDER = "<instance-root>";
6
6
  const SOURCE_AGENT_PLACEHOLDER = "<agent-name>";
7
+ const SOURCE_SLUG_PLACEHOLDER = "<source-slug>";
7
8
  const replaceSourceWorkspacePathTemplate = (template, input, instancePaths, meta) => {
8
9
  const agentName = input.value.kind === "agent" ? input.value.name : input.slug;
9
10
  const instanceRoot = instancePaths.instanceRoot ?? path.posix.dirname(instancePaths.workspacePath);
10
11
  return template
11
12
  .replaceAll(INSTANCE_ROOT_PLACEHOLDER, instanceRoot)
12
13
  .replaceAll(CONFIG_FILE_PLACEHOLDER, meta.configFileName)
13
- .replaceAll(SOURCE_AGENT_PLACEHOLDER, agentName);
14
+ .replaceAll(SOURCE_AGENT_PLACEHOLDER, agentName)
15
+ .replaceAll(SOURCE_SLUG_PLACEHOLDER, input.slug);
14
16
  };
15
17
  const resolveSourceWorkspacePath = (input, instancePaths, meta) => meta.instancePaths.sourceWorkspacePathTemplate
16
18
  ? replaceSourceWorkspacePathTemplate(meta.instancePaths.sourceWorkspacePathTemplate, input, instancePaths, meta)
@@ -1,5 +1,5 @@
1
1
  import { SpawnfileError } from "../shared/index.js";
2
- import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetBaseUrl, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
2
+ import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetClientAuth, resolveMoltnetBaseUrl, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
3
3
  import { assertNetworkBindingEnvUniqueness } from "./networkBinding.js";
4
4
  import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
5
5
  import { assertCompatibleMoltnetNetworkName, assertCompatibleMoltnetRoomPolicy, assertCompatibleMoltnetServer } from "./moltnetRoomPolicyCompatibility.js";
@@ -178,6 +178,21 @@ export const generateMoltnetArtifacts = async (plan) => {
178
178
  throw new SpawnfileError("validation_error", `Duplicate Moltnet node attachment for ${attachment.network}/${attachment.memberId}`);
179
179
  }
180
180
  nodePlanKeys.add(nodePlanKey);
181
+ if (agentNode.runtime.name === "pi") {
182
+ const clientAuth = resolveMoltnetClientAuth(serverPlan.server, attachment.network, attachment.memberId, node.slug);
183
+ const usesPerAttachmentOpenToken = clientAuth.mode === "open" &&
184
+ clientAuth.staticToken !== true &&
185
+ Boolean(clientAuth.tokenEnv || clientAuth.tokenPath);
186
+ if (usesPerAttachmentOpenToken && clientAuth.tokenPath) {
187
+ const mountId = `agent-${node.slug}-moltnet-tokens`;
188
+ addPersistentMount({
189
+ id: mountId,
190
+ mountPath: createMoltnetOpenTokenDirectory(node.slug),
191
+ reason: `Moltnet open-mode generated agent tokens for ${agentNode.name}`,
192
+ volumeName: createPersistentVolumeName(plan.root, mountId)
193
+ });
194
+ }
195
+ }
181
196
  const nodeConfig = createMoltnetNodeConfigContent({
182
197
  agentNode,
183
198
  attachment: { ...attachment, memberId: attachment.memberId },
@@ -1,4 +1,7 @@
1
1
  export declare const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
2
2
  export declare const MOLTNET_BINARY_NAMES: readonly ["moltnet"];
3
+ export type MoltnetTargetArchitecture = "amd64" | "arm64";
3
4
  export declare const resolveMoltnetCliCommand: () => Promise<string>;
4
- export declare const stageMoltnetBinaries: (outputDirectory: string) => Promise<boolean>;
5
+ export declare const stageMoltnetBinaries: (outputDirectory: string, options?: {
6
+ architecture?: MoltnetTargetArchitecture;
7
+ }) => Promise<boolean>;
@@ -7,11 +7,32 @@ import { SpawnfileError } from "../shared/index.js";
7
7
  const execFile = promisify(execFileCallback);
8
8
  const MOLTNET_CLI_ENV = "SPAWNFILE_MOLTNET_CLI";
9
9
  const MOLTNET_RELEASE_DIR_ENV = "SPAWNFILE_MOLTNET_RELEASE_DIR";
10
+ const MOLTNET_TARGET_ARCH_ENV = "SPAWNFILE_MOLTNET_TARGET_ARCH";
10
11
  const LOCAL_MOLTNET_CLI_PATH = path.resolve(process.cwd(), "moltnet", "bin", "moltnet");
11
12
  const MOLTNET_TARGET_OS = "linux";
12
13
  export const MOLTNET_BIN_DIRECTORY = "moltnet-bin";
13
14
  export const MOLTNET_BINARY_NAMES = ["moltnet"];
14
- const resolveTargetArchitecture = () => {
15
+ const normalizeTargetArchitecture = (architecture) => {
16
+ switch (architecture) {
17
+ case "amd64":
18
+ case "x86_64":
19
+ case "x64":
20
+ return "amd64";
21
+ case "aarch64":
22
+ case "arm64":
23
+ return "arm64";
24
+ default:
25
+ throw new SpawnfileError("compile_error", `Moltnet container installs do not support target architecture ${architecture}`);
26
+ }
27
+ };
28
+ const resolveTargetArchitecture = (architecture) => {
29
+ if (architecture) {
30
+ return architecture;
31
+ }
32
+ const configuredArchitecture = process.env[MOLTNET_TARGET_ARCH_ENV]?.trim();
33
+ if (configuredArchitecture) {
34
+ return normalizeTargetArchitecture(configuredArchitecture);
35
+ }
15
36
  switch (process.arch) {
16
37
  case "arm64":
17
38
  return "arm64";
@@ -73,12 +94,12 @@ export const resolveMoltnetCliCommand = async () => {
73
94
  }
74
95
  return validateMoltnetCli("moltnet", "PATH");
75
96
  };
76
- export const stageMoltnetBinaries = async (outputDirectory) => {
97
+ export const stageMoltnetBinaries = async (outputDirectory, options = {}) => {
77
98
  const releaseDirectory = await resolveConfiguredReleaseDirectory();
78
99
  if (!releaseDirectory) {
79
100
  return false;
80
101
  }
81
- const architecture = resolveTargetArchitecture();
102
+ const architecture = resolveTargetArchitecture(options.architecture);
82
103
  const releaseAssetPath = path.join(releaseDirectory, createReleaseAssetName(architecture));
83
104
  if (!(await fileExists(releaseAssetPath))) {
84
105
  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}.`);
@@ -63,6 +63,17 @@ const createAttachmentConfig = (node, artifacts, attachment, agentSlug) => {
63
63
  };
64
64
  };
65
65
  export const resolveMoltnetWorkspaceLayout = (runtimeName, _agentName) => {
66
+ if (runtimeName === "daimon" || runtimeName === "pi") {
67
+ return {
68
+ clientConfigPath: `workspace/.moltnet/config.json`,
69
+ cliRuntime: "codex",
70
+ skillPaths: [
71
+ `workspace/.agents/skills/${GENERATED_SKILL_NAME}/SKILL.md`,
72
+ `workspace/.codex/skills/${GENERATED_SKILL_NAME}/SKILL.md`
73
+ ],
74
+ workspaceRootPath: "workspace"
75
+ };
76
+ }
66
77
  if (runtimeName === "openclaw" || runtimeName === "picoclaw") {
67
78
  return {
68
79
  clientConfigPath: `workspace/${GENERATED_CONFIG_PATH}`,
@@ -51,6 +51,17 @@ export const resolveRuntimeConfig = (plan, agentNode, nodeSlug, _networkId, _age
51
51
  kind: "picoclaw"
52
52
  };
53
53
  }
54
+ case "daimon":
55
+ case "pi": {
56
+ const port = getRuntimeAdapter(agentNode.runtime.name).container.port;
57
+ if (!port) {
58
+ throw new SpawnfileError("compile_error", `Unable to resolve ${agentNode.runtime.name} control port for Moltnet agent ${agentNode.name}`);
59
+ }
60
+ return {
61
+ control_url: `http://127.0.0.1:${port}/agents/${nodeSlug}/wake`,
62
+ kind: "pi"
63
+ };
64
+ }
54
65
  default:
55
66
  throw new SpawnfileError("compile_error", `Moltnet does not know how to attach runtime ${agentNode.runtime.name} directly`);
56
67
  }
@@ -6,7 +6,7 @@ import { ensureDirectory, fileExists, removeDirectory, writeUtf8File } from "../
6
6
  import { appendDockerLabelArgs, createDockerDeploymentLabels, createDockerDeploymentUnitId, createDockerProjectLabel, dockerContextNameForTarget, dockerHostValueForTarget, normalizeDeploymentName, readDeploymentRecordFromOutput, resolveDeploymentRecordPath, verifyDockerDeploymentTarget, writeDockerDeploymentRecordForRun } from "../deployment/index.js";
7
7
  import { DEFAULT_OUTPUT_DIRECTORY, SpawnfileError } from "../shared/index.js";
8
8
  import { compileProject } from "./compileProject.js";
9
- import { createDefaultImageTag } from "./buildProject.js";
9
+ import { createDefaultImageTag, resolveDockerBuildArchitecture } from "./buildProject.js";
10
10
  import { slugify } from "./helpers.js";
11
11
  import { runDockerContainer } from "./runProjectDocker.js";
12
12
  import { assertDeclaredModelAuthSatisfied, assertRunEnvironmentSatisfied, prepareRuntimeAuthMounts, readRunEnvFile, renderDockerEnvFile, resolveAuthMountArgs, resolveRunEnvironment } from "./runProjectAuth.js";
@@ -142,8 +142,14 @@ export const runProject = async (inputPath, options = {}) => {
142
142
  imageTag: options.imageTag,
143
143
  targetExecFile: options.targetExecFile
144
144
  });
145
+ const targetArchitecture = options.containerArchitecture ??
146
+ await resolveDockerBuildArchitecture({
147
+ dockerCommand: options.dockerCommand,
148
+ dockerContext: resolvedOptions.dockerContext
149
+ });
145
150
  const compileResult = await compileProject(inputPath, {
146
151
  clean: options.clean,
152
+ containerArchitecture: targetArchitecture,
147
153
  outputDirectory: options.outputDirectory
148
154
  });
149
155
  const imageTag = resolvedOptions.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
@@ -1,7 +1,137 @@
1
1
  import { execFile as execFileCallback, spawn } from "node:child_process";
2
+ import path from "node:path";
2
3
  import { promisify } from "node:util";
3
4
  import { SpawnfileError } from "../shared/index.js";
4
5
  const execFile = promisify(execFileCallback);
6
+ const parseDockerContextHost = (stdout) => {
7
+ const trimmed = stdout.trim();
8
+ if (!trimmed) {
9
+ return null;
10
+ }
11
+ try {
12
+ const parsed = JSON.parse(trimmed);
13
+ return typeof parsed === "string" ? parsed : null;
14
+ }
15
+ catch {
16
+ return trimmed;
17
+ }
18
+ };
19
+ const resolveSshDockerContext = async (invocation) => {
20
+ if (!invocation.dockerContext) {
21
+ return null;
22
+ }
23
+ const { stdout } = await execFile(invocation.command, [
24
+ "context",
25
+ "inspect",
26
+ invocation.dockerContext,
27
+ "--format",
28
+ "{{json .Endpoints.docker.Host}}"
29
+ ], { cwd: invocation.cwd, timeout: 10_000 });
30
+ const host = parseDockerContextHost(stdout);
31
+ if (!host?.startsWith("ssh://")) {
32
+ return null;
33
+ }
34
+ const url = new URL(host);
35
+ const user = decodeURIComponent(url.username);
36
+ const targetHost = url.hostname;
37
+ return {
38
+ host,
39
+ ...(url.port ? { port: url.port } : {}),
40
+ target: user ? `${user}@${targetHost}` : targetHost
41
+ };
42
+ };
43
+ const sshArgs = (context, command) => [
44
+ ...(context.port ? ["-p", context.port] : []),
45
+ context.target,
46
+ command
47
+ ];
48
+ const scpArgs = (context, source, target) => [
49
+ ...(context.port ? ["-P", context.port] : []),
50
+ "-r",
51
+ source,
52
+ `${context.target}:${target}`
53
+ ];
54
+ const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
55
+ const createRemoteDirectory = async (context) => {
56
+ const { stdout } = await execFile("ssh", sshArgs(context, "mktemp -d /tmp/spawnfile-run-XXXXXX"), {
57
+ timeout: 10_000
58
+ });
59
+ return stdout.trim();
60
+ };
61
+ const copyPathToRemote = async (context, sourcePath, remotePath) => {
62
+ await execFile("scp", scpArgs(context, sourcePath, remotePath), { timeout: 120_000 });
63
+ };
64
+ const removeRemoteDirectory = async (context, remoteDirectory) => {
65
+ await execFile("ssh", sshArgs(context, `rm -rf ${shellQuote(remoteDirectory)}`), {
66
+ timeout: 10_000
67
+ });
68
+ };
69
+ const splitVolumeSpec = (spec) => {
70
+ const delimiter = spec.indexOf(":");
71
+ if (delimiter <= 0) {
72
+ return null;
73
+ }
74
+ return {
75
+ rest: spec.slice(delimiter),
76
+ source: spec.slice(0, delimiter)
77
+ };
78
+ };
79
+ const collectBindMountSources = (args) => {
80
+ const sources = new Set();
81
+ for (let index = 0; index < args.length; index += 1) {
82
+ if (args[index] !== "-v" && args[index] !== "--volume") {
83
+ continue;
84
+ }
85
+ const volume = args[index + 1];
86
+ if (!volume) {
87
+ continue;
88
+ }
89
+ const parsed = splitVolumeSpec(volume);
90
+ if (parsed && path.isAbsolute(parsed.source)) {
91
+ sources.add(parsed.source);
92
+ }
93
+ index += 1;
94
+ }
95
+ return [...sources].sort();
96
+ };
97
+ const rewriteBindMountSources = (args, replacements) => args.map((arg, index) => {
98
+ const previous = args[index - 1];
99
+ if (previous !== "-v" && previous !== "--volume") {
100
+ return arg;
101
+ }
102
+ const parsed = splitVolumeSpec(arg);
103
+ if (!parsed) {
104
+ return arg;
105
+ }
106
+ const replacement = replacements.get(parsed.source);
107
+ return replacement ? `${replacement}${parsed.rest}` : arg;
108
+ });
109
+ const prepareRemoteBindMounts = async (invocation) => {
110
+ const context = await resolveSshDockerContext(invocation);
111
+ if (!context) {
112
+ return { cleanup: async () => undefined, invocation };
113
+ }
114
+ const sources = collectBindMountSources(invocation.args);
115
+ if (sources.length === 0) {
116
+ return { cleanup: async () => undefined, invocation };
117
+ }
118
+ const remoteDirectory = await createRemoteDirectory(context);
119
+ const replacements = new Map();
120
+ for (const [index, source] of sources.entries()) {
121
+ const remotePath = `${remoteDirectory}/${index}-${path.basename(source) || "mount"}`;
122
+ await copyPathToRemote(context, source, remotePath);
123
+ replacements.set(source, remotePath);
124
+ }
125
+ return {
126
+ cleanup: invocation.detach
127
+ ? async () => undefined
128
+ : async () => removeRemoteDirectory(context, remoteDirectory),
129
+ invocation: {
130
+ ...invocation,
131
+ args: rewriteBindMountSources(invocation.args, replacements)
132
+ }
133
+ };
134
+ };
5
135
  const inspectImageArgs = (invocation, containerId) => {
6
136
  const base = invocation.dockerContext
7
137
  ? ["--context", invocation.dockerContext, "inspect"]
@@ -23,35 +153,50 @@ const inspectDetachedImageId = async (invocation, containerId) => {
23
153
  throw new SpawnfileError("runtime_error", `Unable to inspect detached container ${containerId} image id: ${message}`);
24
154
  }
25
155
  };
26
- export const runDockerContainer = async (invocation) => new Promise((resolve, reject) => {
156
+ const runPreparedDockerContainer = (prepared) => new Promise((resolve, reject) => {
27
157
  let stdout = "";
28
- const child = spawn(invocation.command, invocation.args, {
29
- cwd: invocation.cwd,
30
- stdio: invocation.detach ? ["ignore", "pipe", "inherit"] : "inherit"
158
+ const child = spawn(prepared.invocation.command, prepared.invocation.args, {
159
+ cwd: prepared.invocation.cwd,
160
+ stdio: prepared.invocation.detach ? ["ignore", "pipe", "inherit"] : "inherit"
31
161
  });
32
- if (invocation.detach && child.stdout) {
162
+ const settle = (finish) => {
163
+ prepared.cleanup().then(finish, reject);
164
+ };
165
+ const rejectAfterCleanup = (error) => {
166
+ prepared.cleanup().then(() => reject(error), reject);
167
+ };
168
+ if (prepared.invocation.detach && child.stdout) {
33
169
  child.stdout.setEncoding("utf8");
34
170
  child.stdout.on("data", (chunk) => {
35
171
  stdout += chunk;
36
172
  });
37
173
  }
38
174
  child.once("error", (error) => {
39
- reject(new SpawnfileError("runtime_error", `Unable to start docker run for ${invocation.imageTag}: ${error.message}`));
175
+ rejectAfterCleanup(new SpawnfileError("runtime_error", `Unable to start docker run for ${prepared.invocation.imageTag}: ${error.message}`));
40
176
  });
41
177
  child.once("exit", (code, signal) => {
42
178
  if (code === 0) {
43
179
  const containerId = stdout.trim().split(/\s+/)[0];
44
- if (!invocation.detach || !containerId) {
45
- resolve(undefined);
180
+ if (!prepared.invocation.detach || !containerId) {
181
+ settle(() => resolve(undefined));
46
182
  return;
47
183
  }
48
- inspectDetachedImageId(invocation, containerId)
184
+ inspectDetachedImageId(prepared.invocation, containerId)
49
185
  .then((imageId) => resolve({ containerId, ...(imageId ? { imageId } : {}) }))
50
186
  .catch(reject);
51
187
  return;
52
188
  }
53
- reject(new SpawnfileError("runtime_error", signal
54
- ? `Docker run for ${invocation.imageTag} exited from signal ${signal}`
55
- : `Docker run for ${invocation.imageTag} failed with exit code ${code ?? "unknown"}`));
189
+ rejectAfterCleanup(new SpawnfileError("runtime_error", signal
190
+ ? `Docker run for ${prepared.invocation.imageTag} exited from signal ${signal}`
191
+ : `Docker run for ${prepared.invocation.imageTag} failed with exit code ${code ?? "unknown"}`));
56
192
  });
57
193
  });
194
+ export const runDockerContainer = (invocation) => {
195
+ if (!invocation.dockerContext || collectBindMountSources(invocation.args).length === 0) {
196
+ return runPreparedDockerContainer({
197
+ cleanup: async () => undefined,
198
+ invocation
199
+ });
200
+ }
201
+ return prepareRemoteBindMounts(invocation).then(runPreparedDockerContainer);
202
+ };
@@ -21,6 +21,7 @@ export const upProject = async (inputPath, options = {}) => {
21
21
  const buildResult = await buildProject(inputPath, {
22
22
  buildRunner: options.buildRunner,
23
23
  clean: options.clean,
24
+ dockerContext: resolvedOptions.dockerContext,
24
25
  dockerCommand: options.dockerCommand,
25
26
  imageTag: resolvedOptions.imageTag,
26
27
  outputDirectory: options.outputDirectory
@@ -0,0 +1,17 @@
1
+ import type { DeploymentRecord } from "./record.js";
2
+ import { type DockerTargetExecFile } from "./target.js";
3
+ export interface RecoverDockerDeploymentRecordsOptions {
4
+ contains?: DeploymentRecord["units"][number]["contains"];
5
+ context: string;
6
+ dockerCommand?: string;
7
+ execFile?: DockerTargetExecFile;
8
+ outputDirectory: string;
9
+ projectLabel: string;
10
+ runtimeInstanceIds?: string[];
11
+ sourceRoot: string;
12
+ timeoutMs?: number;
13
+ }
14
+ export declare const recoverDockerDeploymentRecords: (options: RecoverDockerDeploymentRecordsOptions) => Promise<Array<{
15
+ path: string;
16
+ record: DeploymentRecord;
17
+ }>>;