spawnfile 0.1.0

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +464 -0
  3. package/dist/.env.example +5 -0
  4. package/dist/Dockerfile +21 -0
  5. package/dist/auth/importers.d.ts +8 -0
  6. package/dist/auth/importers.js +93 -0
  7. package/dist/auth/index.d.ts +5 -0
  8. package/dist/auth/index.js +5 -0
  9. package/dist/auth/paths.d.ts +6 -0
  10. package/dist/auth/paths.js +18 -0
  11. package/dist/auth/profileStore.d.ts +10 -0
  12. package/dist/auth/profileStore.js +125 -0
  13. package/dist/auth/runtimeCredentials.d.ts +14 -0
  14. package/dist/auth/runtimeCredentials.js +76 -0
  15. package/dist/auth/types.d.ts +22 -0
  16. package/dist/auth/types.js +1 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +4 -0
  19. package/dist/cli/runCli.d.ts +27 -0
  20. package/dist/cli/runCli.js +314 -0
  21. package/dist/compiler/addProjectNode.d.ts +21 -0
  22. package/dist/compiler/addProjectNode.js +126 -0
  23. package/dist/compiler/agentSurfaces.d.ts +4 -0
  24. package/dist/compiler/agentSurfaces.js +70 -0
  25. package/dist/compiler/buildCompilePlan.d.ts +2 -0
  26. package/dist/compiler/buildCompilePlan.js +258 -0
  27. package/dist/compiler/buildProject.d.ts +20 -0
  28. package/dist/compiler/buildProject.js +52 -0
  29. package/dist/compiler/compilePlanHelpers.d.ts +7 -0
  30. package/dist/compiler/compilePlanHelpers.js +39 -0
  31. package/dist/compiler/compileProject.d.ts +11 -0
  32. package/dist/compiler/compileProject.js +182 -0
  33. package/dist/compiler/containerArtifacts.d.ts +4 -0
  34. package/dist/compiler/containerArtifacts.js +64 -0
  35. package/dist/compiler/containerArtifactsPlans.d.ts +4 -0
  36. package/dist/compiler/containerArtifactsPlans.js +154 -0
  37. package/dist/compiler/containerArtifactsRender.d.ts +6 -0
  38. package/dist/compiler/containerArtifactsRender.js +237 -0
  39. package/dist/compiler/containerArtifactsTypes.d.ts +42 -0
  40. package/dist/compiler/containerArtifactsTypes.js +1 -0
  41. package/dist/compiler/discordSurface.d.ts +4 -0
  42. package/dist/compiler/discordSurface.js +28 -0
  43. package/dist/compiler/executionDefaults.d.ts +2 -0
  44. package/dist/compiler/executionDefaults.js +9 -0
  45. package/dist/compiler/helpers.d.ts +7 -0
  46. package/dist/compiler/helpers.js +35 -0
  47. package/dist/compiler/index.d.ts +9 -0
  48. package/dist/compiler/index.js +9 -0
  49. package/dist/compiler/initProject.d.ts +9 -0
  50. package/dist/compiler/initProject.js +46 -0
  51. package/dist/compiler/modelAuth.d.ts +2 -0
  52. package/dist/compiler/modelAuth.js +17 -0
  53. package/dist/compiler/modelEnv.d.ts +10 -0
  54. package/dist/compiler/modelEnv.js +97 -0
  55. package/dist/compiler/runProject.d.ts +34 -0
  56. package/dist/compiler/runProject.js +197 -0
  57. package/dist/compiler/runProjectAuth.d.ts +9 -0
  58. package/dist/compiler/runProjectAuth.js +59 -0
  59. package/dist/compiler/surfaceSupport.d.ts +2 -0
  60. package/dist/compiler/surfaceSupport.js +13 -0
  61. package/dist/compiler/surfaces.d.ts +21 -0
  62. package/dist/compiler/surfaces.js +59 -0
  63. package/dist/compiler/syncProjectAuth.d.ts +7 -0
  64. package/dist/compiler/syncProjectAuth.js +65 -0
  65. package/dist/compiler/types.d.ts +134 -0
  66. package/dist/compiler/types.js +1 -0
  67. package/dist/compiler/updateProjectModels.d.ts +20 -0
  68. package/dist/compiler/updateProjectModels.js +181 -0
  69. package/dist/container/rootfs/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/config.json +16 -0
  70. package/dist/container/rootfs/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/workspace/AGENTS.md +1 -0
  71. package/dist/e2e/cli.d.ts +1 -0
  72. package/dist/e2e/cli.js +40 -0
  73. package/dist/e2e/dockerAuth.d.ts +18 -0
  74. package/dist/e2e/dockerAuth.js +212 -0
  75. package/dist/e2e/fixtures.d.ts +2 -0
  76. package/dist/e2e/fixtures.js +49 -0
  77. package/dist/e2e/index.d.ts +4 -0
  78. package/dist/e2e/index.js +4 -0
  79. package/dist/e2e/runtimePrompts.d.ts +13 -0
  80. package/dist/e2e/runtimePrompts.js +132 -0
  81. package/dist/e2e/scenarios.d.ts +3 -0
  82. package/dist/e2e/scenarios.js +84 -0
  83. package/dist/e2e/types.d.ts +35 -0
  84. package/dist/e2e/types.js +1 -0
  85. package/dist/entrypoint.sh +71 -0
  86. package/dist/filesystem/index.d.ts +2 -0
  87. package/dist/filesystem/index.js +2 -0
  88. package/dist/filesystem/io.d.ts +11 -0
  89. package/dist/filesystem/io.js +57 -0
  90. package/dist/filesystem/paths.d.ts +6 -0
  91. package/dist/filesystem/paths.js +30 -0
  92. package/dist/manifest/index.d.ts +5 -0
  93. package/dist/manifest/index.js +5 -0
  94. package/dist/manifest/loadManifest.d.ts +12 -0
  95. package/dist/manifest/loadManifest.js +208 -0
  96. package/dist/manifest/renderSpawnfile.d.ts +2 -0
  97. package/dist/manifest/renderSpawnfile.js +211 -0
  98. package/dist/manifest/scaffold.d.ts +16 -0
  99. package/dist/manifest/scaffold.js +41 -0
  100. package/dist/manifest/schemas.d.ts +989 -0
  101. package/dist/manifest/schemas.js +314 -0
  102. package/dist/manifest/skillFrontmatter.d.ts +5 -0
  103. package/dist/manifest/skillFrontmatter.js +32 -0
  104. package/dist/manifest/surfaceSchemas.d.ts +148 -0
  105. package/dist/manifest/surfaceSchemas.js +162 -0
  106. package/dist/report/createDiagnostic.d.ts +2 -0
  107. package/dist/report/createDiagnostic.js +4 -0
  108. package/dist/report/createReport.d.ts +2 -0
  109. package/dist/report/createReport.js +7 -0
  110. package/dist/report/index.d.ts +4 -0
  111. package/dist/report/index.js +4 -0
  112. package/dist/report/types.d.ts +50 -0
  113. package/dist/report/types.js +1 -0
  114. package/dist/report/writeReport.d.ts +2 -0
  115. package/dist/report/writeReport.js +9 -0
  116. package/dist/runtime/common.d.ts +13 -0
  117. package/dist/runtime/common.js +63 -0
  118. package/dist/runtime/container.d.ts +8 -0
  119. package/dist/runtime/container.js +67 -0
  120. package/dist/runtime/index.d.ts +4 -0
  121. package/dist/runtime/index.js +4 -0
  122. package/dist/runtime/install.d.ts +51 -0
  123. package/dist/runtime/install.js +167 -0
  124. package/dist/runtime/openclaw/adapter.d.ts +2 -0
  125. package/dist/runtime/openclaw/adapter.js +194 -0
  126. package/dist/runtime/openclaw/runAuth.d.ts +2 -0
  127. package/dist/runtime/openclaw/runAuth.js +125 -0
  128. package/dist/runtime/openclaw/scaffold-assets/AGENTS.md +120 -0
  129. package/dist/runtime/openclaw/scaffold-assets/CLAUDE.md +5 -0
  130. package/dist/runtime/openclaw/scaffold-assets/IDENTITY.md +23 -0
  131. package/dist/runtime/openclaw/scaffold-assets/SOUL.md +36 -0
  132. package/dist/runtime/openclaw/scaffold.d.ts +2 -0
  133. package/dist/runtime/openclaw/scaffold.js +28 -0
  134. package/dist/runtime/openclaw/surfaces.d.ts +5 -0
  135. package/dist/runtime/openclaw/surfaces.js +253 -0
  136. package/dist/runtime/picoclaw/adapter.d.ts +2 -0
  137. package/dist/runtime/picoclaw/adapter.js +204 -0
  138. package/dist/runtime/picoclaw/runAuth.d.ts +2 -0
  139. package/dist/runtime/picoclaw/runAuth.js +117 -0
  140. package/dist/runtime/picoclaw/scaffold-assets/AGENTS.md +3 -0
  141. package/dist/runtime/picoclaw/scaffold-assets/CLAUDE.md +5 -0
  142. package/dist/runtime/picoclaw/scaffold-assets/IDENTITY.md +3 -0
  143. package/dist/runtime/picoclaw/scaffold-assets/SOUL.md +3 -0
  144. package/dist/runtime/picoclaw/scaffold.d.ts +2 -0
  145. package/dist/runtime/picoclaw/scaffold.js +28 -0
  146. package/dist/runtime/picoclaw/surfaces.d.ts +5 -0
  147. package/dist/runtime/picoclaw/surfaces.js +111 -0
  148. package/dist/runtime/registry.d.ts +41 -0
  149. package/dist/runtime/registry.js +134 -0
  150. package/dist/runtime/scaffoldAssets.d.ts +1 -0
  151. package/dist/runtime/scaffoldAssets.js +2 -0
  152. package/dist/runtime/tinyclaw/adapter.d.ts +2 -0
  153. package/dist/runtime/tinyclaw/adapter.js +263 -0
  154. package/dist/runtime/tinyclaw/runAuth.d.ts +2 -0
  155. package/dist/runtime/tinyclaw/runAuth.js +22 -0
  156. package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +160 -0
  157. package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +5 -0
  158. package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +177 -0
  159. package/dist/runtime/tinyclaw/scaffold.d.ts +2 -0
  160. package/dist/runtime/tinyclaw/scaffold.js +24 -0
  161. package/dist/runtime/tinyclaw/surfaces.d.ts +8 -0
  162. package/dist/runtime/tinyclaw/surfaces.js +86 -0
  163. package/dist/runtime/types.d.ts +87 -0
  164. package/dist/runtime/types.js +1 -0
  165. package/dist/runtimes/picoclaw/agents/assistant/config.json +16 -0
  166. package/dist/runtimes/picoclaw/agents/assistant/workspace/AGENTS.md +1 -0
  167. package/dist/shared/constants.d.ts +7 -0
  168. package/dist/shared/constants.js +7 -0
  169. package/dist/shared/errors.d.ts +6 -0
  170. package/dist/shared/errors.js +9 -0
  171. package/dist/shared/index.d.ts +3 -0
  172. package/dist/shared/index.js +3 -0
  173. package/dist/shared/types.d.ts +9 -0
  174. package/dist/shared/types.js +1 -0
  175. package/dist/spawnfile-report.json +71 -0
  176. package/package.json +41 -0
  177. package/runtimes.yaml +62 -0
@@ -0,0 +1,42 @@
1
+ import type { ContainerReport } from "../report/index.js";
2
+ import type { EmittedFile, RuntimeContainerConfigEnvBinding, RuntimeContainerMeta } from "../runtime/index.js";
3
+ import type { ModelAuthMethod } from "../shared/index.js";
4
+ import type { ResolvedAgentNode, ResolvedTeamNode } from "./types.js";
5
+ export interface ContainerEnvVariable {
6
+ categories: Array<"model" | "project" | "runtime" | "surface">;
7
+ description: string;
8
+ name: string;
9
+ required: boolean;
10
+ }
11
+ export interface RuntimeTargetPlan {
12
+ configEnvBindings?: RuntimeContainerConfigEnvBinding[];
13
+ envFiles: Array<{
14
+ envName: string;
15
+ filePath: string;
16
+ }>;
17
+ id: string;
18
+ instancePaths: {
19
+ configPath: string;
20
+ homePath?: string;
21
+ workspacePath: string;
22
+ };
23
+ meta: RuntimeContainerMeta;
24
+ modelAuthMethods: Record<string, ModelAuthMethod>;
25
+ modelSecretsRequired: string[];
26
+ port?: number;
27
+ runtimeName: string;
28
+ runtimeRoot: string;
29
+ targetFiles: EmittedFile[];
30
+ }
31
+ export interface CompiledNodeArtifact {
32
+ emittedFiles: EmittedFile[];
33
+ kind: "agent" | "team";
34
+ runtimeName: string | null;
35
+ slug: string;
36
+ value: ResolvedAgentNode | ResolvedTeamNode;
37
+ }
38
+ export interface GeneratedContainerArtifacts {
39
+ executablePaths: string[];
40
+ files: EmittedFile[];
41
+ report: ContainerReport;
42
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import { SurfacesBlock } from "../manifest/index.js";
2
+ import type { ResolvedAgentSurfaces } from "./types.js";
3
+ export declare const resolveAgentSurfaces: (surfaces: SurfacesBlock | undefined) => ResolvedAgentSurfaces | undefined;
4
+ export declare const listAgentSurfaceSecretNames: (surfaces: ResolvedAgentSurfaces | undefined) => string[];
@@ -0,0 +1,28 @@
1
+ import { DEFAULT_DISCORD_BOT_TOKEN_SECRET } from "../shared/index.js";
2
+ export const resolveAgentSurfaces = (surfaces) => {
3
+ if (!surfaces?.discord) {
4
+ return undefined;
5
+ }
6
+ return {
7
+ discord: {
8
+ ...(surfaces.discord.access
9
+ ? {
10
+ access: {
11
+ channels: [...(surfaces.discord.access.channels ?? [])],
12
+ guilds: [...(surfaces.discord.access.guilds ?? [])],
13
+ mode: surfaces.discord.access.mode ??
14
+ "allowlist",
15
+ users: [...(surfaces.discord.access.users ?? [])]
16
+ }
17
+ }
18
+ : {}),
19
+ botTokenSecret: surfaces.discord.bot_token_secret ?? DEFAULT_DISCORD_BOT_TOKEN_SECRET
20
+ }
21
+ };
22
+ };
23
+ export const listAgentSurfaceSecretNames = (surfaces) => {
24
+ if (!surfaces?.discord) {
25
+ return [];
26
+ }
27
+ return [surfaces.discord.botTokenSecret];
28
+ };
@@ -0,0 +1,2 @@
1
+ import type { ExecutionBlock } from "../manifest/index.js";
2
+ export declare const applyExecutionDefaults: (execution: ExecutionBlock | undefined) => ExecutionBlock;
@@ -0,0 +1,9 @@
1
+ export const applyExecutionDefaults = (execution) => ({
2
+ model: execution?.model,
3
+ sandbox: execution?.sandbox ?? {
4
+ mode: "workspace"
5
+ },
6
+ workspace: execution?.workspace ?? {
7
+ isolation: "isolated"
8
+ }
9
+ });
@@ -0,0 +1,7 @@
1
+ import { CompilePlanNode } from "./types.js";
2
+ export declare const createShortHash: (value: string) => string;
3
+ export declare const slugify: (value: string) => string;
4
+ export declare const stableStringify: (value: unknown) => string;
5
+ export declare const assignStableNodeIds: (nodes: Array<Omit<CompilePlanNode, "id"> & {
6
+ source: string;
7
+ }>) => CompilePlanNode[];
@@ -0,0 +1,35 @@
1
+ import { createHash } from "node:crypto";
2
+ export const createShortHash = (value) => createHash("sha1").update(value).digest("hex").slice(0, 8);
3
+ export const slugify = (value) => value
4
+ .toLowerCase()
5
+ .replace(/[^a-z0-9]+/g, "-")
6
+ .replace(/^-+|-+$/g, "");
7
+ export const stableStringify = (value) => {
8
+ if (Array.isArray(value)) {
9
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
10
+ }
11
+ if (value && typeof value === "object") {
12
+ return `{${Object.entries(value)
13
+ .sort(([left], [right]) => left.localeCompare(right))
14
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
15
+ .join(",")}}`;
16
+ }
17
+ return JSON.stringify(value);
18
+ };
19
+ export const assignStableNodeIds = (nodes) => {
20
+ const counts = new Map();
21
+ return nodes.map((node) => {
22
+ const baseId = `${node.kind}:${node.value.name}`;
23
+ const seen = counts.get(baseId) ?? 0;
24
+ counts.set(baseId, seen + 1);
25
+ const id = seen === 0 ? baseId : `${baseId}#${createShortHash(node.source)}`;
26
+ const slug = seen === 0 ? slugify(node.value.name) : `${slugify(node.value.name)}-${createShortHash(node.source)}`;
27
+ return {
28
+ id,
29
+ kind: node.kind,
30
+ runtimeName: node.runtimeName,
31
+ slug,
32
+ value: node.value
33
+ };
34
+ });
35
+ };
@@ -0,0 +1,9 @@
1
+ export * from "./addProjectNode.js";
2
+ export * from "./buildCompilePlan.js";
3
+ export * from "./buildProject.js";
4
+ export * from "./compileProject.js";
5
+ export * from "./initProject.js";
6
+ export * from "./runProject.js";
7
+ export * from "./syncProjectAuth.js";
8
+ export * from "./types.js";
9
+ export * from "./updateProjectModels.js";
@@ -0,0 +1,9 @@
1
+ export * from "./addProjectNode.js";
2
+ export * from "./buildCompilePlan.js";
3
+ export * from "./buildProject.js";
4
+ export * from "./compileProject.js";
5
+ export * from "./initProject.js";
6
+ export * from "./runProject.js";
7
+ export * from "./syncProjectAuth.js";
8
+ export * from "./types.js";
9
+ export * from "./updateProjectModels.js";
@@ -0,0 +1,9 @@
1
+ export interface InitProjectOptions {
2
+ directory?: string;
3
+ runtime?: string;
4
+ team?: boolean;
5
+ }
6
+ export declare const initProject: (options?: InitProjectOptions) => Promise<{
7
+ createdFiles: string[];
8
+ directory: string;
9
+ }>;
@@ -0,0 +1,46 @@
1
+ import path from "node:path";
2
+ import { ensureDirectory, ensureGitignoreEntry, fileExists, writeUtf8File } from "../filesystem/index.js";
3
+ import { createTeamScaffoldManifest, renderSpawnfile } from "../manifest/index.js";
4
+ import { getRuntimeAdapter } from "../runtime/index.js";
5
+ import { DEFAULT_OUTPUT_DIRECTORY, SpawnfileError } from "../shared/index.js";
6
+ const DEFAULT_AGENT_RUNTIME = "openclaw";
7
+ export const initProject = async (options = {}) => {
8
+ const directory = path.resolve(options.directory ?? process.cwd());
9
+ const manifestPath = path.join(directory, "Spawnfile");
10
+ const runtimeName = options.runtime ?? DEFAULT_AGENT_RUNTIME;
11
+ if (await fileExists(manifestPath)) {
12
+ throw new SpawnfileError("io_error", `Refusing to overwrite existing Spawnfile at ${manifestPath}`);
13
+ }
14
+ if (options.team && options.runtime) {
15
+ throw new SpawnfileError("validation_error", "Team scaffolds do not accept --runtime");
16
+ }
17
+ await ensureDirectory(directory);
18
+ const createdFiles = [manifestPath];
19
+ const gitignorePath = path.join(directory, ".gitignore");
20
+ const hadGitignore = await fileExists(gitignorePath);
21
+ if ((await ensureGitignoreEntry(directory, `${DEFAULT_OUTPUT_DIRECTORY}/`)) && !hadGitignore) {
22
+ createdFiles.push(path.join(directory, ".gitignore"));
23
+ }
24
+ if (options.team) {
25
+ const teamDocPath = path.join(directory, "TEAM.md");
26
+ await writeUtf8File(manifestPath, renderSpawnfile(createTeamScaffoldManifest()));
27
+ await writeUtf8File(teamDocPath, "# Team Instructions\n");
28
+ createdFiles.push(teamDocPath);
29
+ }
30
+ else {
31
+ const scaffold = getRuntimeAdapter(runtimeName).scaffoldAgentProject?.();
32
+ if (!scaffold) {
33
+ throw new SpawnfileError("runtime_error", `Runtime ${runtimeName} does not provide an init scaffold`);
34
+ }
35
+ await writeUtf8File(manifestPath, renderSpawnfile(scaffold.manifest));
36
+ for (const file of scaffold.files) {
37
+ const targetPath = path.join(directory, file.path);
38
+ await writeUtf8File(targetPath, file.content);
39
+ createdFiles.push(targetPath);
40
+ }
41
+ }
42
+ return {
43
+ createdFiles,
44
+ directory
45
+ };
46
+ };
@@ -0,0 +1,2 @@
1
+ import type { ExecutionBlock } from "../manifest/index.js";
2
+ export declare const assertRuntimeSupportsExecutionModelAuth: (runtimeName: string, execution: ExecutionBlock | undefined, nodeName: string) => void;
@@ -0,0 +1,17 @@
1
+ import { getRuntimeAdapter } from "../runtime/index.js";
2
+ import { listEffectiveExecutionModelTargets } from "./modelEnv.js";
3
+ export const assertRuntimeSupportsExecutionModelAuth = (runtimeName, execution, nodeName) => {
4
+ const adapter = getRuntimeAdapter(runtimeName);
5
+ const modelTargets = listEffectiveExecutionModelTargets(execution);
6
+ for (const target of modelTargets) {
7
+ try {
8
+ adapter.assertSupportedModelTarget(target);
9
+ }
10
+ catch (error) {
11
+ if (error instanceof Error) {
12
+ error.message = `${error.message} on ${nodeName}`;
13
+ }
14
+ throw error;
15
+ }
16
+ }
17
+ };
@@ -0,0 +1,10 @@
1
+ import type { ExecutionBlock, ModelTarget } from "../manifest/index.js";
2
+ import { type ModelAuthMethod } from "../shared/index.js";
3
+ import type { EffectiveModelTarget } from "./types.js";
4
+ export declare const resolveModelProviderEnvName: (provider: string) => string;
5
+ export declare const listExecutionModelTargets: (execution: ExecutionBlock | undefined) => ModelTarget[];
6
+ export declare const resolveEffectiveModelTarget: (target: ModelTarget, execution: ExecutionBlock | undefined) => EffectiveModelTarget;
7
+ export declare const listEffectiveExecutionModelTargets: (execution: ExecutionBlock | undefined) => EffectiveModelTarget[];
8
+ export declare const listExecutionModelProviders: (execution: ExecutionBlock | undefined) => string[];
9
+ export declare const resolveExecutionModelAuthMethods: (execution: ExecutionBlock | undefined) => Record<string, ModelAuthMethod>;
10
+ export declare const listExecutionModelSecretNames: (execution: ExecutionBlock | undefined) => string[];
@@ -0,0 +1,97 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ const MODEL_PROVIDER_ENV_VARS = new Map([
3
+ ["anthropic", "ANTHROPIC_API_KEY"],
4
+ ["google", "GOOGLE_API_KEY"],
5
+ ["groq", "GROQ_API_KEY"],
6
+ ["mistral", "MISTRAL_API_KEY"],
7
+ ["openai", "OPENAI_API_KEY"],
8
+ ["openrouter", "OPENROUTER_API_KEY"],
9
+ ["xai", "XAI_API_KEY"]
10
+ ]);
11
+ const resolveLegacyModelAuthMethod = (execution, provider) => {
12
+ const auth = execution?.model?.auth;
13
+ if (!auth) {
14
+ return undefined;
15
+ }
16
+ if (auth.method) {
17
+ return auth.method;
18
+ }
19
+ return auth.methods?.[provider];
20
+ };
21
+ export const resolveModelProviderEnvName = (provider) => MODEL_PROVIDER_ENV_VARS.get(provider) ??
22
+ `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
23
+ export const listExecutionModelTargets = (execution) => {
24
+ if (!execution?.model?.primary) {
25
+ return [];
26
+ }
27
+ return [execution.model.primary, ...(execution.model.fallback ?? [])];
28
+ };
29
+ const resolveDefaultModelAuthMethod = (target) => {
30
+ if (target.provider === "local") {
31
+ return "none";
32
+ }
33
+ if (target.provider === "custom") {
34
+ return undefined;
35
+ }
36
+ return "api_key";
37
+ };
38
+ export const resolveEffectiveModelTarget = (target, execution) => {
39
+ const method = target.auth?.method ??
40
+ resolveLegacyModelAuthMethod(execution, target.provider) ??
41
+ resolveDefaultModelAuthMethod(target);
42
+ if (!method) {
43
+ throw new SpawnfileError("validation_error", `Model ${target.provider}/${target.name} must declare auth.method`);
44
+ }
45
+ if (target.auth?.key && method !== "api_key") {
46
+ throw new SpawnfileError("validation_error", `Model ${target.provider}/${target.name} can only declare auth.key with api_key auth`);
47
+ }
48
+ if ((target.provider === "custom" || target.provider === "local") &&
49
+ !target.endpoint) {
50
+ throw new SpawnfileError("validation_error", `Model ${target.provider}/${target.name} must declare endpoint`);
51
+ }
52
+ if (target.endpoint &&
53
+ target.provider !== "custom" &&
54
+ target.provider !== "local") {
55
+ throw new SpawnfileError("validation_error", `Model ${target.provider}/${target.name} cannot declare endpoint`);
56
+ }
57
+ if (method === "api_key" &&
58
+ (target.provider === "custom" || target.provider === "local") &&
59
+ !target.auth?.key) {
60
+ throw new SpawnfileError("validation_error", `Model ${target.provider}/${target.name} must declare auth.key for api_key auth`);
61
+ }
62
+ return {
63
+ auth: {
64
+ ...(target.auth?.key ? { key: target.auth.key } : {}),
65
+ method
66
+ },
67
+ ...(target.endpoint ? { endpoint: target.endpoint } : {}),
68
+ name: target.name,
69
+ provider: target.provider
70
+ };
71
+ };
72
+ export const listEffectiveExecutionModelTargets = (execution) => listExecutionModelTargets(execution).map((target) => resolveEffectiveModelTarget(target, execution));
73
+ export const listExecutionModelProviders = (execution) => {
74
+ const providers = listEffectiveExecutionModelTargets(execution).map((target) => target.provider);
75
+ return providers.filter((provider, index) => providers.indexOf(provider) === index).sort();
76
+ };
77
+ export const resolveExecutionModelAuthMethods = (execution) => {
78
+ const methods = new Map();
79
+ for (const target of listEffectiveExecutionModelTargets(execution)) {
80
+ const existingMethod = methods.get(target.provider);
81
+ if (existingMethod && existingMethod !== target.auth.method) {
82
+ throw new SpawnfileError("validation_error", `Execution model declares conflicting auth methods for provider ${target.provider}`);
83
+ }
84
+ methods.set(target.provider, target.auth.method);
85
+ }
86
+ return Object.fromEntries([...methods.entries()].sort(([left], [right]) => left.localeCompare(right)));
87
+ };
88
+ export const listExecutionModelSecretNames = (execution) => {
89
+ const secretNames = new Set();
90
+ for (const target of listEffectiveExecutionModelTargets(execution)) {
91
+ if (target.auth.method !== "api_key") {
92
+ continue;
93
+ }
94
+ secretNames.add(target.auth.key ?? resolveModelProviderEnvName(target.provider));
95
+ }
96
+ return [...secretNames].sort();
97
+ };
@@ -0,0 +1,34 @@
1
+ import { type ResolvedAuthProfile } from "../auth/index.js";
2
+ import { type CompileProjectOptions, type CompileProjectResult } from "./compileProject.js";
3
+ export interface DockerRunInvocation {
4
+ args: string[];
5
+ command: string;
6
+ containerName: string | null;
7
+ cwd: string;
8
+ detach: boolean;
9
+ envFilePath: string;
10
+ imageTag: string;
11
+ supportDirectory: string;
12
+ }
13
+ export type DockerRunRunner = (invocation: DockerRunInvocation) => Promise<void>;
14
+ export interface RunProjectOptions extends CompileProjectOptions {
15
+ authProfile?: string;
16
+ containerName?: string;
17
+ detach?: boolean;
18
+ dockerCommand?: string;
19
+ imageTag?: string;
20
+ runRunner?: DockerRunRunner;
21
+ }
22
+ export interface RunProjectResult extends CompileProjectResult {
23
+ authProfileName: string | null;
24
+ containerName: string | null;
25
+ imageTag: string;
26
+ }
27
+ export declare const createDockerRunInvocation: (compileResult: CompileProjectResult, imageTag: string, options?: {
28
+ authProfile?: ResolvedAuthProfile | null;
29
+ containerName?: string;
30
+ detach?: boolean;
31
+ dockerCommand?: string;
32
+ }) => Promise<DockerRunInvocation>;
33
+ export declare const runDockerContainer: DockerRunRunner;
34
+ export declare const runProject: (inputPath: string, options?: RunProjectOptions) => Promise<RunProjectResult>;
@@ -0,0 +1,197 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { mkdtemp } from "node:fs/promises";
4
+ import { randomBytes } from "node:crypto";
5
+ import { spawn } from "node:child_process";
6
+ import { requireAuthProfile } from "../auth/index.js";
7
+ import { ensureDirectory, fileExists, removeDirectory, writeUtf8File } from "../filesystem/index.js";
8
+ import { SpawnfileError } from "../shared/index.js";
9
+ import { compileProject } from "./compileProject.js";
10
+ import { createDefaultImageTag } from "./buildProject.js";
11
+ import { slugify } from "./helpers.js";
12
+ import { assertDeclaredModelAuthSatisfied, prepareRuntimeAuthMounts } from "./runProjectAuth.js";
13
+ const resolveImageTagRoot = (inputPath) => {
14
+ const resolvedPath = path.resolve(inputPath);
15
+ return path.basename(resolvedPath).toLowerCase() === "spawnfile"
16
+ ? path.dirname(resolvedPath)
17
+ : resolvedPath;
18
+ };
19
+ const createDefaultContainerName = (imageTag) => {
20
+ const containerName = slugify(imageTag.replaceAll("/", "-").replaceAll(":", "-"));
21
+ return containerName || "spawnfile-run";
22
+ };
23
+ const getImportMountTargetName = (kind) => kind === "claude-code" ? ".claude" : ".codex";
24
+ const createGeneratedRuntimeSecret = (secretName) => {
25
+ if (secretName === "OPENCLAW_GATEWAY_TOKEN") {
26
+ return randomBytes(24).toString("hex");
27
+ }
28
+ return null;
29
+ };
30
+ const hasEnvValue = (env, name) => typeof env[name] === "string" && env[name].length > 0;
31
+ const collectMissingRequiredSecrets = (containerReport, env, coveredModelSecrets) => {
32
+ const missing = new Set();
33
+ for (const secretName of containerReport.secrets_required) {
34
+ if (hasEnvValue(env, secretName)) {
35
+ continue;
36
+ }
37
+ if (!containerReport.model_secrets_required.includes(secretName)) {
38
+ missing.add(secretName);
39
+ continue;
40
+ }
41
+ const requiredInstances = (containerReport.runtime_instances ?? []).filter((instance) => instance.model_secrets_required.includes(secretName));
42
+ const isCoveredEverywhere = requiredInstances.length > 0 &&
43
+ requiredInstances.every((instance) => coveredModelSecrets.has(`${instance.id}:${secretName}`));
44
+ if (!isCoveredEverywhere) {
45
+ missing.add(secretName);
46
+ }
47
+ }
48
+ return [...missing].sort();
49
+ };
50
+ const resolveRunEnvironment = (containerReport, authProfile) => {
51
+ const env = {
52
+ ...(authProfile?.env ?? {})
53
+ };
54
+ for (const name of new Set([...Object.keys(env), ...containerReport.secrets_required])) {
55
+ const processValue = process.env[name];
56
+ if (typeof processValue === "string" && processValue.length > 0) {
57
+ env[name] = processValue;
58
+ }
59
+ }
60
+ for (const name of containerReport.runtime_secrets_required) {
61
+ if (hasEnvValue(env, name)) {
62
+ continue;
63
+ }
64
+ const generatedValue = createGeneratedRuntimeSecret(name);
65
+ if (generatedValue) {
66
+ env[name] = generatedValue;
67
+ }
68
+ }
69
+ for (const [name, value] of Object.entries(env)) {
70
+ if (value.includes("\n")) {
71
+ throw new SpawnfileError("validation_error", `Env value for ${name} contains a newline and cannot be written to a Docker env file`);
72
+ }
73
+ }
74
+ return env;
75
+ };
76
+ const assertRunEnvironmentSatisfied = (containerReport, env, coveredModelSecrets) => {
77
+ const missing = collectMissingRequiredSecrets(containerReport, env, coveredModelSecrets);
78
+ if (missing.length > 0) {
79
+ throw new SpawnfileError("validation_error", `Missing required runtime env: ${missing.sort().join(", ")}`);
80
+ }
81
+ };
82
+ const renderDockerEnvFile = (env) => `${Object.entries(env)
83
+ .sort(([left], [right]) => left.localeCompare(right))
84
+ .map(([name, value]) => `${name}=${value}`)
85
+ .join("\n")}\n`;
86
+ const resolveAuthMountArgs = async (containerReport, authProfile) => {
87
+ if (!authProfile || containerReport.runtime_homes.length === 0) {
88
+ return [];
89
+ }
90
+ const args = [];
91
+ for (const [kind, entry] of Object.entries(authProfile.imports)) {
92
+ if (!entry) {
93
+ continue;
94
+ }
95
+ if (!(await fileExists(entry.path))) {
96
+ throw new SpawnfileError("validation_error", `Imported auth path for ${kind} does not exist: ${entry.path}`);
97
+ }
98
+ for (const runtimeHome of containerReport.runtime_homes) {
99
+ args.push("-v", `${entry.path}:${path.posix.join(runtimeHome, getImportMountTargetName(kind))}`);
100
+ }
101
+ }
102
+ return args;
103
+ };
104
+ export const createDockerRunInvocation = async (compileResult, imageTag, options = {}) => {
105
+ const containerReport = compileResult.report.container;
106
+ if (!containerReport) {
107
+ throw new SpawnfileError("runtime_error", "Compile output did not include container metadata");
108
+ }
109
+ const supportDirectory = await mkdtemp(path.join(os.tmpdir(), "spawnfile-run-"));
110
+ const envFilePath = path.join(supportDirectory, "run.env");
111
+ try {
112
+ assertDeclaredModelAuthSatisfied(containerReport, options.authProfile ?? null);
113
+ const env = resolveRunEnvironment(containerReport, options.authProfile ?? null);
114
+ const preparedRuntimeAuth = await prepareRuntimeAuthMounts(compileResult.outputDirectory, containerReport, options.authProfile ?? null, env, supportDirectory);
115
+ assertRunEnvironmentSatisfied(containerReport, env, preparedRuntimeAuth.coveredModelSecrets);
116
+ await ensureDirectory(supportDirectory);
117
+ await writeUtf8File(envFilePath, renderDockerEnvFile(env));
118
+ const containerName = options.containerName ?? createDefaultContainerName(imageTag);
119
+ const args = ["run"];
120
+ if (options.detach) {
121
+ args.push("-d");
122
+ }
123
+ else {
124
+ args.push("--rm");
125
+ }
126
+ args.push("--name", containerName);
127
+ for (const port of containerReport.ports) {
128
+ args.push("-p", `${port}:${port}`);
129
+ }
130
+ args.push("--env-file", envFilePath);
131
+ args.push(...(await resolveAuthMountArgs(containerReport, options.authProfile ?? null)));
132
+ args.push(...preparedRuntimeAuth.mountArgs);
133
+ args.push(imageTag);
134
+ return {
135
+ args,
136
+ command: options.dockerCommand ?? "docker",
137
+ containerName,
138
+ cwd: compileResult.outputDirectory,
139
+ detach: options.detach ?? false,
140
+ envFilePath,
141
+ imageTag,
142
+ supportDirectory
143
+ };
144
+ }
145
+ catch (error) {
146
+ await removeDirectory(supportDirectory);
147
+ throw error;
148
+ }
149
+ };
150
+ export const runDockerContainer = async (invocation) => new Promise((resolve, reject) => {
151
+ const child = spawn(invocation.command, invocation.args, {
152
+ cwd: invocation.cwd,
153
+ stdio: "inherit"
154
+ });
155
+ child.once("error", (error) => {
156
+ reject(new SpawnfileError("runtime_error", `Unable to start docker run for ${invocation.imageTag}: ${error.message}`));
157
+ });
158
+ child.once("exit", (code, signal) => {
159
+ if (code === 0) {
160
+ resolve();
161
+ return;
162
+ }
163
+ reject(new SpawnfileError("runtime_error", signal
164
+ ? `Docker run for ${invocation.imageTag} exited from signal ${signal}`
165
+ : `Docker run for ${invocation.imageTag} failed with exit code ${code ?? "unknown"}`));
166
+ });
167
+ });
168
+ export const runProject = async (inputPath, options = {}) => {
169
+ const compileResult = await compileProject(inputPath, {
170
+ clean: options.clean,
171
+ outputDirectory: options.outputDirectory
172
+ });
173
+ const imageTag = options.imageTag ?? createDefaultImageTag(resolveImageTagRoot(inputPath));
174
+ const authProfile = options.authProfile
175
+ ? await requireAuthProfile(options.authProfile)
176
+ : null;
177
+ const invocation = await createDockerRunInvocation(compileResult, imageTag, {
178
+ authProfile,
179
+ containerName: options.containerName,
180
+ detach: options.detach,
181
+ dockerCommand: options.dockerCommand
182
+ });
183
+ try {
184
+ await (options.runRunner ?? runDockerContainer)(invocation);
185
+ }
186
+ finally {
187
+ if (!invocation.detach) {
188
+ await removeDirectory(invocation.supportDirectory);
189
+ }
190
+ }
191
+ return {
192
+ ...compileResult,
193
+ authProfileName: authProfile?.name ?? null,
194
+ containerName: invocation.containerName,
195
+ imageTag
196
+ };
197
+ };
@@ -0,0 +1,9 @@
1
+ import type { ResolvedAuthProfile } from "../auth/index.js";
2
+ import type { ContainerReport } from "../report/index.js";
3
+ interface PreparedRunAuth {
4
+ coveredModelSecrets: Set<string>;
5
+ mountArgs: string[];
6
+ }
7
+ export declare const prepareRuntimeAuthMounts: (outputDirectory: string, containerReport: ContainerReport, authProfile: ResolvedAuthProfile | null, env: Record<string, string>, tempRoot: string) => Promise<PreparedRunAuth>;
8
+ export declare const assertDeclaredModelAuthSatisfied: (containerReport: ContainerReport, authProfile: ResolvedAuthProfile | null) => void;
9
+ export {};
@@ -0,0 +1,59 @@
1
+ import { SpawnfileError } from "../shared/index.js";
2
+ import { getRuntimeAdapter } from "../runtime/index.js";
3
+ const MODEL_AUTH_IMPORT_KINDS = {
4
+ api_key: null,
5
+ "claude-code": "claude-code",
6
+ codex: "codex",
7
+ none: null
8
+ };
9
+ const addCoveredModelSecrets = (coveredModelSecrets, instanceId, secretNames) => {
10
+ for (const secretName of secretNames) {
11
+ coveredModelSecrets.add(`${instanceId}:${secretName}`);
12
+ }
13
+ };
14
+ export const prepareRuntimeAuthMounts = async (outputDirectory, containerReport, authProfile, env, tempRoot) => {
15
+ if (!authProfile) {
16
+ return { coveredModelSecrets: new Set(), mountArgs: [] };
17
+ }
18
+ const coveredModelSecrets = new Set();
19
+ const mountArgs = [];
20
+ for (const instance of containerReport.runtime_instances) {
21
+ const adapter = getRuntimeAdapter(instance.runtime);
22
+ if (!adapter.prepareRuntimeAuth) {
23
+ continue;
24
+ }
25
+ const prepared = await adapter.prepareRuntimeAuth({
26
+ authProfile,
27
+ env,
28
+ instance,
29
+ outputDirectory,
30
+ tempRoot
31
+ });
32
+ addCoveredModelSecrets(coveredModelSecrets, instance.id, prepared.coveredModelSecrets);
33
+ mountArgs.push(...prepared.mountArgs);
34
+ }
35
+ return { coveredModelSecrets, mountArgs };
36
+ };
37
+ export const assertDeclaredModelAuthSatisfied = (containerReport, authProfile) => {
38
+ const requiredImportKinds = new Set();
39
+ for (const instance of containerReport.runtime_instances) {
40
+ for (const method of Object.values(instance.model_auth_methods)) {
41
+ const importKind = MODEL_AUTH_IMPORT_KINDS[method];
42
+ if (importKind) {
43
+ requiredImportKinds.add(importKind);
44
+ }
45
+ }
46
+ }
47
+ if (requiredImportKinds.size === 0) {
48
+ return;
49
+ }
50
+ if (!authProfile) {
51
+ throw new SpawnfileError("validation_error", `Auth profile is required for declared model auth methods: ${[...requiredImportKinds].sort().join(", ")}`);
52
+ }
53
+ const missingImportKinds = [...requiredImportKinds]
54
+ .filter((kind) => !authProfile.imports[kind])
55
+ .sort();
56
+ if (missingImportKinds.length > 0) {
57
+ throw new SpawnfileError("validation_error", `Auth profile ${authProfile.name} is missing required auth imports: ${missingImportKinds.join(", ")}`);
58
+ }
59
+ };
@@ -0,0 +1,2 @@
1
+ import type { ResolvedAgentSurfaces } from "./types.js";
2
+ export declare const assertRuntimeSupportsAgentSurfaces: (runtimeName: string, surfaces: ResolvedAgentSurfaces | undefined, nodeName: string) => void;