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.
- package/LICENSE +21 -0
- package/README.md +464 -0
- package/dist/.env.example +5 -0
- package/dist/Dockerfile +21 -0
- package/dist/auth/importers.d.ts +8 -0
- package/dist/auth/importers.js +93 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/paths.d.ts +6 -0
- package/dist/auth/paths.js +18 -0
- package/dist/auth/profileStore.d.ts +10 -0
- package/dist/auth/profileStore.js +125 -0
- package/dist/auth/runtimeCredentials.d.ts +14 -0
- package/dist/auth/runtimeCredentials.js +76 -0
- package/dist/auth/types.d.ts +22 -0
- package/dist/auth/types.js +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/runCli.d.ts +27 -0
- package/dist/cli/runCli.js +314 -0
- package/dist/compiler/addProjectNode.d.ts +21 -0
- package/dist/compiler/addProjectNode.js +126 -0
- package/dist/compiler/agentSurfaces.d.ts +4 -0
- package/dist/compiler/agentSurfaces.js +70 -0
- package/dist/compiler/buildCompilePlan.d.ts +2 -0
- package/dist/compiler/buildCompilePlan.js +258 -0
- package/dist/compiler/buildProject.d.ts +20 -0
- package/dist/compiler/buildProject.js +52 -0
- package/dist/compiler/compilePlanHelpers.d.ts +7 -0
- package/dist/compiler/compilePlanHelpers.js +39 -0
- package/dist/compiler/compileProject.d.ts +11 -0
- package/dist/compiler/compileProject.js +182 -0
- package/dist/compiler/containerArtifacts.d.ts +4 -0
- package/dist/compiler/containerArtifacts.js +64 -0
- package/dist/compiler/containerArtifactsPlans.d.ts +4 -0
- package/dist/compiler/containerArtifactsPlans.js +154 -0
- package/dist/compiler/containerArtifactsRender.d.ts +6 -0
- package/dist/compiler/containerArtifactsRender.js +237 -0
- package/dist/compiler/containerArtifactsTypes.d.ts +42 -0
- package/dist/compiler/containerArtifactsTypes.js +1 -0
- package/dist/compiler/discordSurface.d.ts +4 -0
- package/dist/compiler/discordSurface.js +28 -0
- package/dist/compiler/executionDefaults.d.ts +2 -0
- package/dist/compiler/executionDefaults.js +9 -0
- package/dist/compiler/helpers.d.ts +7 -0
- package/dist/compiler/helpers.js +35 -0
- package/dist/compiler/index.d.ts +9 -0
- package/dist/compiler/index.js +9 -0
- package/dist/compiler/initProject.d.ts +9 -0
- package/dist/compiler/initProject.js +46 -0
- package/dist/compiler/modelAuth.d.ts +2 -0
- package/dist/compiler/modelAuth.js +17 -0
- package/dist/compiler/modelEnv.d.ts +10 -0
- package/dist/compiler/modelEnv.js +97 -0
- package/dist/compiler/runProject.d.ts +34 -0
- package/dist/compiler/runProject.js +197 -0
- package/dist/compiler/runProjectAuth.d.ts +9 -0
- package/dist/compiler/runProjectAuth.js +59 -0
- package/dist/compiler/surfaceSupport.d.ts +2 -0
- package/dist/compiler/surfaceSupport.js +13 -0
- package/dist/compiler/surfaces.d.ts +21 -0
- package/dist/compiler/surfaces.js +59 -0
- package/dist/compiler/syncProjectAuth.d.ts +7 -0
- package/dist/compiler/syncProjectAuth.js +65 -0
- package/dist/compiler/types.d.ts +134 -0
- package/dist/compiler/types.js +1 -0
- package/dist/compiler/updateProjectModels.d.ts +20 -0
- package/dist/compiler/updateProjectModels.js +181 -0
- package/dist/container/rootfs/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/config.json +16 -0
- package/dist/container/rootfs/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/workspace/AGENTS.md +1 -0
- package/dist/e2e/cli.d.ts +1 -0
- package/dist/e2e/cli.js +40 -0
- package/dist/e2e/dockerAuth.d.ts +18 -0
- package/dist/e2e/dockerAuth.js +212 -0
- package/dist/e2e/fixtures.d.ts +2 -0
- package/dist/e2e/fixtures.js +49 -0
- package/dist/e2e/index.d.ts +4 -0
- package/dist/e2e/index.js +4 -0
- package/dist/e2e/runtimePrompts.d.ts +13 -0
- package/dist/e2e/runtimePrompts.js +132 -0
- package/dist/e2e/scenarios.d.ts +3 -0
- package/dist/e2e/scenarios.js +84 -0
- package/dist/e2e/types.d.ts +35 -0
- package/dist/e2e/types.js +1 -0
- package/dist/entrypoint.sh +71 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +2 -0
- package/dist/filesystem/io.d.ts +11 -0
- package/dist/filesystem/io.js +57 -0
- package/dist/filesystem/paths.d.ts +6 -0
- package/dist/filesystem/paths.js +30 -0
- package/dist/manifest/index.d.ts +5 -0
- package/dist/manifest/index.js +5 -0
- package/dist/manifest/loadManifest.d.ts +12 -0
- package/dist/manifest/loadManifest.js +208 -0
- package/dist/manifest/renderSpawnfile.d.ts +2 -0
- package/dist/manifest/renderSpawnfile.js +211 -0
- package/dist/manifest/scaffold.d.ts +16 -0
- package/dist/manifest/scaffold.js +41 -0
- package/dist/manifest/schemas.d.ts +989 -0
- package/dist/manifest/schemas.js +314 -0
- package/dist/manifest/skillFrontmatter.d.ts +5 -0
- package/dist/manifest/skillFrontmatter.js +32 -0
- package/dist/manifest/surfaceSchemas.d.ts +148 -0
- package/dist/manifest/surfaceSchemas.js +162 -0
- package/dist/report/createDiagnostic.d.ts +2 -0
- package/dist/report/createDiagnostic.js +4 -0
- package/dist/report/createReport.d.ts +2 -0
- package/dist/report/createReport.js +7 -0
- package/dist/report/index.d.ts +4 -0
- package/dist/report/index.js +4 -0
- package/dist/report/types.d.ts +50 -0
- package/dist/report/types.js +1 -0
- package/dist/report/writeReport.d.ts +2 -0
- package/dist/report/writeReport.js +9 -0
- package/dist/runtime/common.d.ts +13 -0
- package/dist/runtime/common.js +63 -0
- package/dist/runtime/container.d.ts +8 -0
- package/dist/runtime/container.js +67 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/install.d.ts +51 -0
- package/dist/runtime/install.js +167 -0
- package/dist/runtime/openclaw/adapter.d.ts +2 -0
- package/dist/runtime/openclaw/adapter.js +194 -0
- package/dist/runtime/openclaw/runAuth.d.ts +2 -0
- package/dist/runtime/openclaw/runAuth.js +125 -0
- package/dist/runtime/openclaw/scaffold-assets/AGENTS.md +120 -0
- package/dist/runtime/openclaw/scaffold-assets/CLAUDE.md +5 -0
- package/dist/runtime/openclaw/scaffold-assets/IDENTITY.md +23 -0
- package/dist/runtime/openclaw/scaffold-assets/SOUL.md +36 -0
- package/dist/runtime/openclaw/scaffold.d.ts +2 -0
- package/dist/runtime/openclaw/scaffold.js +28 -0
- package/dist/runtime/openclaw/surfaces.d.ts +5 -0
- package/dist/runtime/openclaw/surfaces.js +253 -0
- package/dist/runtime/picoclaw/adapter.d.ts +2 -0
- package/dist/runtime/picoclaw/adapter.js +204 -0
- package/dist/runtime/picoclaw/runAuth.d.ts +2 -0
- package/dist/runtime/picoclaw/runAuth.js +117 -0
- package/dist/runtime/picoclaw/scaffold-assets/AGENTS.md +3 -0
- package/dist/runtime/picoclaw/scaffold-assets/CLAUDE.md +5 -0
- package/dist/runtime/picoclaw/scaffold-assets/IDENTITY.md +3 -0
- package/dist/runtime/picoclaw/scaffold-assets/SOUL.md +3 -0
- package/dist/runtime/picoclaw/scaffold.d.ts +2 -0
- package/dist/runtime/picoclaw/scaffold.js +28 -0
- package/dist/runtime/picoclaw/surfaces.d.ts +5 -0
- package/dist/runtime/picoclaw/surfaces.js +111 -0
- package/dist/runtime/registry.d.ts +41 -0
- package/dist/runtime/registry.js +134 -0
- package/dist/runtime/scaffoldAssets.d.ts +1 -0
- package/dist/runtime/scaffoldAssets.js +2 -0
- package/dist/runtime/tinyclaw/adapter.d.ts +2 -0
- package/dist/runtime/tinyclaw/adapter.js +263 -0
- package/dist/runtime/tinyclaw/runAuth.d.ts +2 -0
- package/dist/runtime/tinyclaw/runAuth.js +22 -0
- package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +160 -0
- package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +5 -0
- package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +177 -0
- package/dist/runtime/tinyclaw/scaffold.d.ts +2 -0
- package/dist/runtime/tinyclaw/scaffold.js +24 -0
- package/dist/runtime/tinyclaw/surfaces.d.ts +8 -0
- package/dist/runtime/tinyclaw/surfaces.js +86 -0
- package/dist/runtime/types.d.ts +87 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtimes/picoclaw/agents/assistant/config.json +16 -0
- package/dist/runtimes/picoclaw/agents/assistant/workspace/AGENTS.md +1 -0
- package/dist/shared/constants.d.ts +7 -0
- package/dist/shared/constants.js +7 -0
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/shared/index.d.ts +3 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/types.d.ts +9 -0
- package/dist/shared/types.js +1 -0
- package/dist/spawnfile-report.json +71 -0
- package/package.json +41 -0
- package/runtimes.yaml +62 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { cp, mkdir, lstat, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
export const copyDirectory = async (sourcePath, destinationPath, options = {}) => {
|
|
4
|
+
await cp(sourcePath, destinationPath, {
|
|
5
|
+
filter: options.filter,
|
|
6
|
+
force: true,
|
|
7
|
+
recursive: true
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
export const ensureDirectory = async (directoryPath) => {
|
|
11
|
+
await mkdir(directoryPath, { recursive: true });
|
|
12
|
+
};
|
|
13
|
+
export const fileExists = async (filePath) => {
|
|
14
|
+
try {
|
|
15
|
+
await stat(filePath);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const isSymlink = async (filePath) => {
|
|
23
|
+
try {
|
|
24
|
+
const stats = await lstat(filePath);
|
|
25
|
+
return stats.isSymbolicLink();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export const readUtf8File = async (filePath) => readFile(filePath, "utf8");
|
|
32
|
+
export const ensureGitignoreEntry = async (directoryPath, entry) => {
|
|
33
|
+
const gitignorePath = path.join(directoryPath, ".gitignore");
|
|
34
|
+
const normalizedEntry = entry.trim();
|
|
35
|
+
if (normalizedEntry.length === 0) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const existingSource = (await fileExists(gitignorePath)) ? await readUtf8File(gitignorePath) : "";
|
|
39
|
+
const existingEntries = new Set(existingSource
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.map((line) => line.trim())
|
|
42
|
+
.filter((line) => line.length > 0));
|
|
43
|
+
if (existingEntries.has(normalizedEntry)) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const nextSource = existingSource.length === 0
|
|
47
|
+
? `${normalizedEntry}\n`
|
|
48
|
+
: `${existingSource}${existingSource.endsWith("\n") ? "" : "\n"}${normalizedEntry}\n`;
|
|
49
|
+
await writeUtf8File(gitignorePath, nextSource);
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
export const removeDirectory = async (directoryPath) => {
|
|
53
|
+
await rm(directoryPath, { force: true, recursive: true });
|
|
54
|
+
};
|
|
55
|
+
export const writeUtf8File = async (filePath, content) => {
|
|
56
|
+
await writeFile(filePath, content, "utf8");
|
|
57
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const assertPortableRelativePath: (inputPath: string) => void;
|
|
2
|
+
export declare const getCanonicalManifestPath: (filePath: string) => string;
|
|
3
|
+
export declare const getManifestPath: (inputPath: string) => string;
|
|
4
|
+
export declare const getProjectRoot: (manifestPath: string) => string;
|
|
5
|
+
export declare const resolveProjectPath: (manifestPath: string, relativePath: string) => string;
|
|
6
|
+
export declare const toPosixPath: (value: string) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
3
|
+
const INVALID_PATH_SEGMENT = "..";
|
|
4
|
+
export const assertPortableRelativePath = (inputPath) => {
|
|
5
|
+
if (inputPath.includes("\\")) {
|
|
6
|
+
throw new SpawnfileError("validation_error", `Paths must use forward slashes: ${inputPath}`);
|
|
7
|
+
}
|
|
8
|
+
if (path.isAbsolute(inputPath)) {
|
|
9
|
+
throw new SpawnfileError("validation_error", `Absolute paths are not allowed: ${inputPath}`);
|
|
10
|
+
}
|
|
11
|
+
const segments = inputPath.split("/");
|
|
12
|
+
if (segments.includes(INVALID_PATH_SEGMENT)) {
|
|
13
|
+
throw new SpawnfileError("validation_error", `Path traversal is not allowed: ${inputPath}`);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const getCanonicalManifestPath = (filePath) => path.resolve(filePath);
|
|
17
|
+
export const getManifestPath = (inputPath) => path.basename(inputPath) === "Spawnfile"
|
|
18
|
+
? path.resolve(inputPath)
|
|
19
|
+
: path.resolve(inputPath, "Spawnfile");
|
|
20
|
+
export const getProjectRoot = (manifestPath) => path.dirname(manifestPath);
|
|
21
|
+
export const resolveProjectPath = (manifestPath, relativePath) => {
|
|
22
|
+
assertPortableRelativePath(relativePath);
|
|
23
|
+
const projectRoot = getProjectRoot(manifestPath);
|
|
24
|
+
const resolved = path.resolve(projectRoot, relativePath);
|
|
25
|
+
if (!resolved.startsWith(projectRoot + path.sep) && resolved !== projectRoot) {
|
|
26
|
+
throw new SpawnfileError("validation_error", `Path escapes project root: ${relativePath}`);
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
};
|
|
30
|
+
export const toPosixPath = (value) => value.split(path.sep).join("/");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ExecutionBlock, Manifest, RuntimeBinding } from "./schemas.js";
|
|
2
|
+
export interface LoadedManifest<TManifest extends Manifest = Manifest> {
|
|
3
|
+
manifest: TManifest;
|
|
4
|
+
manifestPath: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const loadManifest: (manifestPath: string) => Promise<LoadedManifest>;
|
|
7
|
+
export interface NormalizedRuntimeBinding {
|
|
8
|
+
name: string;
|
|
9
|
+
options: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare const normalizeRuntimeBinding: (runtime: RuntimeBinding | undefined) => NormalizedRuntimeBinding | undefined;
|
|
12
|
+
export declare const mergeExecution: (parentExecution: ExecutionBlock | undefined, childExecution: ExecutionBlock | undefined) => ExecutionBlock | undefined;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parse as parseYaml } from "yaml";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { fileExists, isSymlink, readUtf8File, resolveProjectPath } from "../filesystem/index.js";
|
|
5
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
6
|
+
import { isAgentManifest, isTeamManifest, manifestSchema } from "./schemas.js";
|
|
7
|
+
import { parseSkillFrontmatter } from "./skillFrontmatter.js";
|
|
8
|
+
const ensureUniqueNames = (names, label) => {
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
for (const name of names) {
|
|
11
|
+
if (seen.has(name)) {
|
|
12
|
+
throw new SpawnfileError("validation_error", `Duplicate ${label}: ${name}`);
|
|
13
|
+
}
|
|
14
|
+
seen.add(name);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const getMcpNames = (mcpServers) => new Set((mcpServers ?? []).map((server) => server.name));
|
|
18
|
+
const validateSkillRequirements = (skills, visibleMcpNames) => {
|
|
19
|
+
for (const skill of skills ?? []) {
|
|
20
|
+
for (const mcpName of skill.requires?.mcp ?? []) {
|
|
21
|
+
if (!visibleMcpNames.has(mcpName)) {
|
|
22
|
+
throw new SpawnfileError("validation_error", `Skill ${skill.ref} requires undeclared MCP server: ${mcpName}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const MARKDOWN_EXTENSION_PATTERN = /\.(md|markdown)$/i;
|
|
28
|
+
const assertMarkdownDocumentPath = (documentPath) => {
|
|
29
|
+
if (!MARKDOWN_EXTENSION_PATTERN.test(documentPath)) {
|
|
30
|
+
throw new SpawnfileError("validation_error", `Document paths must point to Markdown files: ${documentPath}`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const validateDocFiles = async (manifestPath, manifest) => {
|
|
34
|
+
const docs = manifest.docs;
|
|
35
|
+
if (!docs) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const docPaths = [
|
|
39
|
+
docs.heartbeat,
|
|
40
|
+
docs.identity,
|
|
41
|
+
docs.memory,
|
|
42
|
+
docs.soul,
|
|
43
|
+
docs.system,
|
|
44
|
+
...Object.values(docs.extras ?? {})
|
|
45
|
+
].filter((value) => Boolean(value));
|
|
46
|
+
await Promise.all(docPaths.map(async (documentPath) => {
|
|
47
|
+
assertMarkdownDocumentPath(documentPath);
|
|
48
|
+
const resolvedPath = resolveProjectPath(manifestPath, documentPath);
|
|
49
|
+
if (await isSymlink(resolvedPath)) {
|
|
50
|
+
throw new SpawnfileError("validation_error", `Symlinks are not allowed: ${documentPath}`);
|
|
51
|
+
}
|
|
52
|
+
if (!(await fileExists(resolvedPath))) {
|
|
53
|
+
throw new SpawnfileError("validation_error", `Document not found for manifest ${manifestPath}: ${documentPath}`);
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
const validateManifestRefs = async (manifestPath, refs, label) => {
|
|
58
|
+
await Promise.all((refs ?? []).map(async (ref) => {
|
|
59
|
+
const refDirectoryPath = resolveProjectPath(manifestPath, ref);
|
|
60
|
+
if (await isSymlink(refDirectoryPath)) {
|
|
61
|
+
throw new SpawnfileError("validation_error", `Symlinks are not allowed: ${ref}`);
|
|
62
|
+
}
|
|
63
|
+
const childManifestPath = path.join(refDirectoryPath, "Spawnfile");
|
|
64
|
+
if (await isSymlink(childManifestPath)) {
|
|
65
|
+
throw new SpawnfileError("validation_error", `Symlinks are not allowed: ${ref}/Spawnfile`);
|
|
66
|
+
}
|
|
67
|
+
if (!(await fileExists(childManifestPath))) {
|
|
68
|
+
throw new SpawnfileError("validation_error", `${label} ref must point to a directory containing a Spawnfile: ${ref}`);
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
};
|
|
72
|
+
const validateSkills = async (manifestPath, skills) => {
|
|
73
|
+
await Promise.all((skills ?? []).map(async (skill) => {
|
|
74
|
+
const skillDirPath = resolveProjectPath(manifestPath, skill.ref);
|
|
75
|
+
if (await isSymlink(skillDirPath)) {
|
|
76
|
+
throw new SpawnfileError("validation_error", `Symlinks are not allowed: ${skill.ref}`);
|
|
77
|
+
}
|
|
78
|
+
const skillFilePath = resolveProjectPath(manifestPath, `${skill.ref}/SKILL.md`);
|
|
79
|
+
if (await isSymlink(skillFilePath)) {
|
|
80
|
+
throw new SpawnfileError("validation_error", `Symlinks are not allowed: ${skill.ref}/SKILL.md`);
|
|
81
|
+
}
|
|
82
|
+
if (!(await fileExists(skillFilePath))) {
|
|
83
|
+
throw new SpawnfileError("validation_error", `Skill directory missing SKILL.md: ${skill.ref}`);
|
|
84
|
+
}
|
|
85
|
+
parseSkillFrontmatter(await readUtf8File(skillFilePath));
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
const validateLocalAgentManifest = async (manifestPath, manifest) => {
|
|
89
|
+
ensureUniqueNames((manifest.mcp_servers ?? []).map((server) => server.name), "MCP server");
|
|
90
|
+
ensureUniqueNames((manifest.subagents ?? []).map((subagent) => subagent.id), "subagent id");
|
|
91
|
+
await validateDocFiles(manifestPath, manifest);
|
|
92
|
+
await validateSkills(manifestPath, manifest.skills);
|
|
93
|
+
await validateManifestRefs(manifestPath, manifest.subagents?.map((subagent) => subagent.ref), "Subagent");
|
|
94
|
+
validateSkillRequirements(manifest.skills, getMcpNames(manifest.mcp_servers));
|
|
95
|
+
};
|
|
96
|
+
const getSharedMcpNames = (shared) => getMcpNames(shared?.mcp_servers);
|
|
97
|
+
const validateLocalTeamManifest = async (manifestPath, manifest) => {
|
|
98
|
+
ensureUniqueNames((manifest.shared?.mcp_servers ?? []).map((server) => server.name), "MCP server");
|
|
99
|
+
ensureUniqueNames(manifest.members.map((member) => member.id), "member id");
|
|
100
|
+
await validateDocFiles(manifestPath, manifest);
|
|
101
|
+
await validateSkills(manifestPath, manifest.shared?.skills);
|
|
102
|
+
await validateManifestRefs(manifestPath, manifest.members.map((member) => member.ref), "Member");
|
|
103
|
+
validateSkillRequirements(manifest.shared?.skills, getSharedMcpNames(manifest.shared));
|
|
104
|
+
const memberIds = new Set(manifest.members.map((member) => member.id));
|
|
105
|
+
if (manifest.structure.leader && !memberIds.has(manifest.structure.leader)) {
|
|
106
|
+
throw new SpawnfileError("validation_error", `Structure leader is not a declared team member: ${manifest.structure.leader}`);
|
|
107
|
+
}
|
|
108
|
+
for (const id of manifest.structure.external ?? []) {
|
|
109
|
+
if (!memberIds.has(id)) {
|
|
110
|
+
throw new SpawnfileError("validation_error", `Structure external references undeclared member: ${id}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const parseManifest = (source) => {
|
|
115
|
+
try {
|
|
116
|
+
return manifestSchema.parse(parseYaml(source));
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof z.ZodError) {
|
|
120
|
+
const issue = error.issues[0];
|
|
121
|
+
throw new SpawnfileError("invalid_manifest", `Invalid Spawnfile manifest: ${issue.message}`);
|
|
122
|
+
}
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
export const loadManifest = async (manifestPath) => {
|
|
127
|
+
const source = await readUtf8File(manifestPath);
|
|
128
|
+
const manifest = parseManifest(source);
|
|
129
|
+
if (isAgentManifest(manifest)) {
|
|
130
|
+
await validateLocalAgentManifest(manifestPath, manifest);
|
|
131
|
+
}
|
|
132
|
+
if (isTeamManifest(manifest)) {
|
|
133
|
+
await validateLocalTeamManifest(manifestPath, manifest);
|
|
134
|
+
}
|
|
135
|
+
return { manifest, manifestPath };
|
|
136
|
+
};
|
|
137
|
+
export const normalizeRuntimeBinding = (runtime) => {
|
|
138
|
+
if (!runtime) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
if (typeof runtime === "string") {
|
|
142
|
+
return { name: runtime, options: {} };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
name: runtime.name,
|
|
146
|
+
options: runtime.options ?? {}
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
export const mergeExecution = (parentExecution, childExecution) => {
|
|
150
|
+
if (!parentExecution) {
|
|
151
|
+
return childExecution;
|
|
152
|
+
}
|
|
153
|
+
if (!childExecution) {
|
|
154
|
+
return parentExecution;
|
|
155
|
+
}
|
|
156
|
+
const model = parentExecution.model || childExecution.model
|
|
157
|
+
? {
|
|
158
|
+
auth: childExecution.model?.auth ?? parentExecution.model?.auth,
|
|
159
|
+
fallback: childExecution.model?.fallback ?? parentExecution.model?.fallback,
|
|
160
|
+
primary: childExecution.model?.primary ?? parentExecution.model?.primary
|
|
161
|
+
}
|
|
162
|
+
: undefined;
|
|
163
|
+
if (model && !model.primary) {
|
|
164
|
+
throw new SpawnfileError("validation_error", "Merged execution model is missing a primary model");
|
|
165
|
+
}
|
|
166
|
+
const sandbox = parentExecution.sandbox || childExecution.sandbox
|
|
167
|
+
? {
|
|
168
|
+
...parentExecution.sandbox,
|
|
169
|
+
...childExecution.sandbox
|
|
170
|
+
}
|
|
171
|
+
: undefined;
|
|
172
|
+
if (sandbox && !sandbox.mode) {
|
|
173
|
+
throw new SpawnfileError("validation_error", "Merged execution sandbox is missing mode");
|
|
174
|
+
}
|
|
175
|
+
const workspace = parentExecution.workspace || childExecution.workspace
|
|
176
|
+
? {
|
|
177
|
+
...parentExecution.workspace,
|
|
178
|
+
...childExecution.workspace
|
|
179
|
+
}
|
|
180
|
+
: undefined;
|
|
181
|
+
if (workspace && !workspace.isolation) {
|
|
182
|
+
throw new SpawnfileError("validation_error", "Merged execution workspace is missing isolation");
|
|
183
|
+
}
|
|
184
|
+
const resolvedModel = model
|
|
185
|
+
? {
|
|
186
|
+
auth: model.auth,
|
|
187
|
+
fallback: model.fallback,
|
|
188
|
+
primary: model.primary
|
|
189
|
+
}
|
|
190
|
+
: undefined;
|
|
191
|
+
const resolvedSandbox = sandbox
|
|
192
|
+
? {
|
|
193
|
+
mode: sandbox.mode
|
|
194
|
+
}
|
|
195
|
+
: undefined;
|
|
196
|
+
const resolvedWorkspace = workspace
|
|
197
|
+
? {
|
|
198
|
+
isolation: workspace.isolation
|
|
199
|
+
}
|
|
200
|
+
: undefined;
|
|
201
|
+
return {
|
|
202
|
+
...parentExecution,
|
|
203
|
+
...childExecution,
|
|
204
|
+
model: resolvedModel,
|
|
205
|
+
sandbox: resolvedSandbox,
|
|
206
|
+
workspace: resolvedWorkspace
|
|
207
|
+
};
|
|
208
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
const withDefinedEntries = (entries) => Object.fromEntries(entries.filter((entry) => entry[1] !== undefined));
|
|
3
|
+
const hasEntries = (value) => Object.keys(value).length > 0;
|
|
4
|
+
const orderRuntimeBinding = (runtime) => {
|
|
5
|
+
if (!runtime || typeof runtime === "string") {
|
|
6
|
+
return runtime;
|
|
7
|
+
}
|
|
8
|
+
return withDefinedEntries([
|
|
9
|
+
["name", runtime.name],
|
|
10
|
+
["options", runtime.options]
|
|
11
|
+
]);
|
|
12
|
+
};
|
|
13
|
+
const orderDocs = (docs) => {
|
|
14
|
+
if (!docs) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return withDefinedEntries([
|
|
18
|
+
["identity", docs.identity],
|
|
19
|
+
["soul", docs.soul],
|
|
20
|
+
["system", docs.system],
|
|
21
|
+
["memory", docs.memory],
|
|
22
|
+
["heartbeat", docs.heartbeat],
|
|
23
|
+
["extras", docs.extras]
|
|
24
|
+
]);
|
|
25
|
+
};
|
|
26
|
+
const orderDiscordSurface = (surface) => {
|
|
27
|
+
if (!surface) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return withDefinedEntries([
|
|
31
|
+
["access", orderDiscordSurfaceAccess(surface.access)],
|
|
32
|
+
["bot_token_secret", surface.bot_token_secret]
|
|
33
|
+
]);
|
|
34
|
+
};
|
|
35
|
+
const orderDiscordSurfaceAccess = (access) => {
|
|
36
|
+
if (!access) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return withDefinedEntries([
|
|
40
|
+
["mode", access.mode],
|
|
41
|
+
["users", access.users],
|
|
42
|
+
["guilds", access.guilds],
|
|
43
|
+
["channels", access.channels]
|
|
44
|
+
]);
|
|
45
|
+
};
|
|
46
|
+
const orderTelegramSurface = (surface) => {
|
|
47
|
+
if (!surface) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return withDefinedEntries([
|
|
51
|
+
["access", orderTelegramSurfaceAccess(surface.access)],
|
|
52
|
+
["bot_token_secret", surface.bot_token_secret]
|
|
53
|
+
]);
|
|
54
|
+
};
|
|
55
|
+
const orderTelegramSurfaceAccess = (access) => {
|
|
56
|
+
if (!access) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return withDefinedEntries([
|
|
60
|
+
["mode", access.mode],
|
|
61
|
+
["users", access.users],
|
|
62
|
+
["chats", access.chats]
|
|
63
|
+
]);
|
|
64
|
+
};
|
|
65
|
+
const orderWhatsAppSurface = (surface) => {
|
|
66
|
+
if (!surface) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return withDefinedEntries([["access", orderWhatsAppSurfaceAccess(surface.access)]]);
|
|
70
|
+
};
|
|
71
|
+
const orderWhatsAppSurfaceAccess = (access) => {
|
|
72
|
+
if (!access) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return withDefinedEntries([
|
|
76
|
+
["mode", access.mode],
|
|
77
|
+
["users", access.users],
|
|
78
|
+
["groups", access.groups]
|
|
79
|
+
]);
|
|
80
|
+
};
|
|
81
|
+
const orderSlackSurface = (surface) => {
|
|
82
|
+
if (!surface) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return withDefinedEntries([
|
|
86
|
+
["access", orderSlackSurfaceAccess(surface.access)],
|
|
87
|
+
["bot_token_secret", surface.bot_token_secret],
|
|
88
|
+
["app_token_secret", surface.app_token_secret]
|
|
89
|
+
]);
|
|
90
|
+
};
|
|
91
|
+
const orderSlackSurfaceAccess = (access) => {
|
|
92
|
+
if (!access) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return withDefinedEntries([
|
|
96
|
+
["mode", access.mode],
|
|
97
|
+
["users", access.users],
|
|
98
|
+
["channels", access.channels]
|
|
99
|
+
]);
|
|
100
|
+
};
|
|
101
|
+
const orderModelEntryAuth = (auth) => {
|
|
102
|
+
if (!auth) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return withDefinedEntries([
|
|
106
|
+
["method", auth.method],
|
|
107
|
+
["key", auth.key]
|
|
108
|
+
]);
|
|
109
|
+
};
|
|
110
|
+
const orderModelTarget = (target) => {
|
|
111
|
+
if (!target) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return withDefinedEntries([
|
|
115
|
+
["provider", target.provider],
|
|
116
|
+
["name", target.name],
|
|
117
|
+
["auth", orderModelEntryAuth(target.auth)],
|
|
118
|
+
["endpoint", target.endpoint]
|
|
119
|
+
]);
|
|
120
|
+
};
|
|
121
|
+
const orderExecution = (execution) => {
|
|
122
|
+
if (!execution) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return withDefinedEntries([
|
|
126
|
+
[
|
|
127
|
+
"model",
|
|
128
|
+
execution.model
|
|
129
|
+
? withDefinedEntries([
|
|
130
|
+
["primary", orderModelTarget(execution.model.primary)],
|
|
131
|
+
["fallback", execution.model.fallback?.map(orderModelTarget)],
|
|
132
|
+
["auth", execution.model.auth]
|
|
133
|
+
])
|
|
134
|
+
: undefined
|
|
135
|
+
],
|
|
136
|
+
["workspace", execution.workspace],
|
|
137
|
+
["sandbox", execution.sandbox]
|
|
138
|
+
]);
|
|
139
|
+
};
|
|
140
|
+
const orderSharedSurface = (shared) => {
|
|
141
|
+
if (!shared) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
return withDefinedEntries([
|
|
145
|
+
["env", shared.env],
|
|
146
|
+
["mcp_servers", shared.mcp_servers],
|
|
147
|
+
["secrets", shared.secrets],
|
|
148
|
+
["skills", shared.skills]
|
|
149
|
+
]);
|
|
150
|
+
};
|
|
151
|
+
const orderSurfaces = (surfaces) => {
|
|
152
|
+
if (!surfaces) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
return withDefinedEntries([
|
|
156
|
+
["discord", orderDiscordSurface(surfaces.discord)],
|
|
157
|
+
["telegram", orderTelegramSurface(surfaces.telegram)],
|
|
158
|
+
["whatsapp", orderWhatsAppSurface(surfaces.whatsapp)],
|
|
159
|
+
["slack", orderSlackSurface(surfaces.slack)]
|
|
160
|
+
]);
|
|
161
|
+
};
|
|
162
|
+
const renderSections = (sections) => sections
|
|
163
|
+
.filter(hasEntries)
|
|
164
|
+
.map((section) => YAML.stringify(section))
|
|
165
|
+
.join("\n");
|
|
166
|
+
const orderAgentManifestSections = (manifest) => [
|
|
167
|
+
withDefinedEntries([
|
|
168
|
+
["spawnfile_version", manifest.spawnfile_version],
|
|
169
|
+
["kind", manifest.kind],
|
|
170
|
+
["name", manifest.name]
|
|
171
|
+
]),
|
|
172
|
+
withDefinedEntries([["runtime", orderRuntimeBinding(manifest.runtime)]]),
|
|
173
|
+
withDefinedEntries([["execution", orderExecution(manifest.execution)]]),
|
|
174
|
+
withDefinedEntries([["docs", orderDocs(manifest.docs)]]),
|
|
175
|
+
withDefinedEntries([["surfaces", orderSurfaces(manifest.surfaces)]]),
|
|
176
|
+
withDefinedEntries([
|
|
177
|
+
["skills", manifest.skills],
|
|
178
|
+
["mcp_servers", manifest.mcp_servers],
|
|
179
|
+
["secrets", manifest.secrets],
|
|
180
|
+
["env", manifest.env],
|
|
181
|
+
["policy", manifest.policy]
|
|
182
|
+
]),
|
|
183
|
+
withDefinedEntries([["subagents", manifest.subagents]])
|
|
184
|
+
];
|
|
185
|
+
const orderTeamManifestSections = (manifest) => [
|
|
186
|
+
withDefinedEntries([
|
|
187
|
+
["spawnfile_version", manifest.spawnfile_version],
|
|
188
|
+
["kind", manifest.kind],
|
|
189
|
+
["name", manifest.name]
|
|
190
|
+
]),
|
|
191
|
+
withDefinedEntries([["runtime", orderRuntimeBinding(manifest.runtime)]]),
|
|
192
|
+
withDefinedEntries([["execution", orderExecution(manifest.execution)]]),
|
|
193
|
+
withDefinedEntries([
|
|
194
|
+
["docs", orderDocs(manifest.docs)],
|
|
195
|
+
["shared", orderSharedSurface(manifest.shared)]
|
|
196
|
+
]),
|
|
197
|
+
withDefinedEntries([
|
|
198
|
+
["skills", manifest.skills],
|
|
199
|
+
["mcp_servers", manifest.mcp_servers],
|
|
200
|
+
["secrets", manifest.secrets],
|
|
201
|
+
["env", manifest.env],
|
|
202
|
+
["policy", manifest.policy]
|
|
203
|
+
]),
|
|
204
|
+
withDefinedEntries([
|
|
205
|
+
["members", manifest.members],
|
|
206
|
+
["structure", manifest.structure]
|
|
207
|
+
])
|
|
208
|
+
];
|
|
209
|
+
export const renderSpawnfile = (manifest) => renderSections(manifest.kind === "agent"
|
|
210
|
+
? orderAgentManifestSections(manifest)
|
|
211
|
+
: orderTeamManifestSections(manifest));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentManifest, TeamManifest } from "./schemas.js";
|
|
2
|
+
import type { ModelAuthMethod } from "../shared/index.js";
|
|
3
|
+
export interface AgentScaffoldManifestOptions {
|
|
4
|
+
authMethod?: ModelAuthMethod;
|
|
5
|
+
docs: {
|
|
6
|
+
identity?: string;
|
|
7
|
+
soul?: string;
|
|
8
|
+
system: string;
|
|
9
|
+
};
|
|
10
|
+
modelName: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
provider: string;
|
|
13
|
+
runtime: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const createAgentScaffoldManifest: (options: AgentScaffoldManifestOptions) => AgentManifest;
|
|
16
|
+
export declare const createTeamScaffoldManifest: () => TeamManifest;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const createAgentScaffoldManifest = (options) => {
|
|
2
|
+
const authMethod = options.authMethod;
|
|
3
|
+
return {
|
|
4
|
+
spawnfile_version: "0.1",
|
|
5
|
+
kind: "agent",
|
|
6
|
+
name: options.name ?? "my-agent",
|
|
7
|
+
runtime: options.runtime,
|
|
8
|
+
execution: {
|
|
9
|
+
model: {
|
|
10
|
+
...(authMethod
|
|
11
|
+
? {
|
|
12
|
+
auth: {
|
|
13
|
+
method: authMethod
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
: {}),
|
|
17
|
+
primary: {
|
|
18
|
+
name: options.modelName,
|
|
19
|
+
provider: options.provider
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
docs: {
|
|
24
|
+
...(options.docs.identity ? { identity: options.docs.identity } : {}),
|
|
25
|
+
...(options.docs.soul ? { soul: options.docs.soul } : {}),
|
|
26
|
+
system: options.docs.system
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export const createTeamScaffoldManifest = () => ({
|
|
31
|
+
spawnfile_version: "0.1",
|
|
32
|
+
kind: "team",
|
|
33
|
+
name: "my-team",
|
|
34
|
+
docs: {
|
|
35
|
+
system: "TEAM.md"
|
|
36
|
+
},
|
|
37
|
+
members: [],
|
|
38
|
+
structure: {
|
|
39
|
+
mode: "swarm"
|
|
40
|
+
}
|
|
41
|
+
});
|