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,212 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdtemp } from "node:fs/promises";
|
|
4
|
+
import { requireAuthProfile } from "../auth/index.js";
|
|
5
|
+
import { buildProject, createDockerRunInvocation, runDockerContainer, syncProjectAuth } from "../compiler/index.js";
|
|
6
|
+
import { removeDirectory } from "../filesystem/index.js";
|
|
7
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
8
|
+
import { materializeDockerAuthFixture } from "./fixtures.js";
|
|
9
|
+
import { waitForRuntimeReady, promptRuntime } from "./runtimePrompts.js";
|
|
10
|
+
import { filterDockerAuthE2EScenarios } from "./scenarios.js";
|
|
11
|
+
const DEFAULT_ENV_FILE = path.resolve(process.cwd(), "../headhunter/.env");
|
|
12
|
+
const createLogger = (logger) => logger ?? {
|
|
13
|
+
error: (message) => console.error(message),
|
|
14
|
+
info: (message) => console.log(message)
|
|
15
|
+
};
|
|
16
|
+
const createScenarioImageTag = (scenario) => `spawnfile-e2e-${scenario.id}-${Date.now()}`;
|
|
17
|
+
const createScenarioPrompt = (scenarioId, runtime) => `Reply with exactly SF-E2E-${scenarioId.toUpperCase()}-${runtime.toUpperCase()} and nothing else.`;
|
|
18
|
+
const extractSentinel = (prompt) => prompt.replace("Reply with exactly ", "").replace(" and nothing else.", "");
|
|
19
|
+
const findPromptInstance = (scenario, instances, runtime) => {
|
|
20
|
+
const runtimeInstances = instances.filter((instance) => instance.runtime === runtime);
|
|
21
|
+
if (runtimeInstances.length === 0) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (scenario.kind === "single-agent") {
|
|
25
|
+
return runtimeInstances[0] ?? null;
|
|
26
|
+
}
|
|
27
|
+
return runtimeInstances[0] ?? null;
|
|
28
|
+
};
|
|
29
|
+
const resolveEnvFilePath = (inputPath) => inputPath ?? DEFAULT_ENV_FILE;
|
|
30
|
+
const withSpawnfileHome = async (spawnfileHome, fn) => {
|
|
31
|
+
const previousValue = process.env.SPAWNFILE_HOME;
|
|
32
|
+
process.env.SPAWNFILE_HOME = spawnfileHome;
|
|
33
|
+
try {
|
|
34
|
+
return await fn();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (typeof previousValue === "string") {
|
|
38
|
+
process.env.SPAWNFILE_HOME = previousValue;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
delete process.env.SPAWNFILE_HOME;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const runDockerCommand = async (dockerCommand, args) => {
|
|
46
|
+
const { spawn } = await import("node:child_process");
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const child = spawn(dockerCommand, args, {
|
|
49
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
50
|
+
});
|
|
51
|
+
const stdout = [];
|
|
52
|
+
const stderr = [];
|
|
53
|
+
child.stdout.on("data", (chunk) => stdout.push(String(chunk)));
|
|
54
|
+
child.stderr.on("data", (chunk) => stderr.push(String(chunk)));
|
|
55
|
+
child.once("error", (error) => {
|
|
56
|
+
reject(new SpawnfileError("runtime_error", `Unable to start docker command ${dockerCommand}: ${error.message}`));
|
|
57
|
+
});
|
|
58
|
+
child.once("exit", (code, signal) => {
|
|
59
|
+
if (code === 0) {
|
|
60
|
+
resolve(stdout.join("").trim());
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
reject(new SpawnfileError("runtime_error", signal
|
|
64
|
+
? `Docker command exited from signal ${signal}: ${dockerCommand} ${args.join(" ")}`
|
|
65
|
+
: `Docker command failed with exit code ${code ?? "unknown"}: ${dockerCommand} ${args.join(" ")}\n${stderr.join("")}`.trim()));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
const cleanupDockerArtifacts = async (dockerCommand, containerName, imageTag, options) => {
|
|
70
|
+
try {
|
|
71
|
+
await runDockerCommand(dockerCommand, ["rm", "-f", containerName]);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Ignore best-effort cleanup failures.
|
|
75
|
+
}
|
|
76
|
+
if (options.keepImages) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await runDockerCommand(dockerCommand, ["image", "rm", "-f", imageTag]);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore best-effort cleanup failures.
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const readDockerLogs = async (dockerCommand, containerName) => {
|
|
87
|
+
try {
|
|
88
|
+
return await runDockerCommand(dockerCommand, ["logs", containerName]);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const runScenario = async (scenario, options) => {
|
|
95
|
+
const startedAt = Date.now();
|
|
96
|
+
const logger = createLogger(options.logger);
|
|
97
|
+
const scenarioRoot = await mkdtemp(path.join(os.tmpdir(), `spawnfile-e2e-${scenario.id}-`));
|
|
98
|
+
const projectDirectory = path.join(scenarioRoot, "project");
|
|
99
|
+
const outputDirectory = path.join(scenarioRoot, "dist");
|
|
100
|
+
const spawnfileHome = path.join(scenarioRoot, "spawnfile-home");
|
|
101
|
+
const profileName = "e2e";
|
|
102
|
+
const imageTag = createScenarioImageTag(scenario);
|
|
103
|
+
const containerName = `spawnfile-e2e-${scenario.id}`;
|
|
104
|
+
logger.info(`scenario ${scenario.id}: materializing fixture`);
|
|
105
|
+
try {
|
|
106
|
+
await materializeDockerAuthFixture(scenario, projectDirectory);
|
|
107
|
+
await withSpawnfileHome(spawnfileHome, async () => {
|
|
108
|
+
logger.info(`scenario ${scenario.id}: syncing auth`);
|
|
109
|
+
await syncProjectAuth(projectDirectory, {
|
|
110
|
+
claudeCodeDirectory: options.claudeCodeDirectory,
|
|
111
|
+
codexDirectory: options.codexDirectory,
|
|
112
|
+
envFilePath: resolveEnvFilePath(options.envFilePath),
|
|
113
|
+
profileName
|
|
114
|
+
});
|
|
115
|
+
logger.info(`scenario ${scenario.id}: building image ${imageTag}`);
|
|
116
|
+
const buildResult = await buildProject(projectDirectory, {
|
|
117
|
+
dockerCommand: options.dockerCommand,
|
|
118
|
+
imageTag,
|
|
119
|
+
outputDirectory
|
|
120
|
+
});
|
|
121
|
+
const runtimeInstances = buildResult.report.container?.runtime_instances ?? [];
|
|
122
|
+
const authProfile = await requireAuthProfile(profileName);
|
|
123
|
+
const invocation = await createDockerRunInvocation(buildResult, imageTag, {
|
|
124
|
+
authProfile,
|
|
125
|
+
containerName,
|
|
126
|
+
detach: true,
|
|
127
|
+
dockerCommand: options.dockerCommand
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
logger.info(`scenario ${scenario.id}: starting container ${containerName}`);
|
|
131
|
+
await runDockerContainer(invocation);
|
|
132
|
+
for (const check of scenario.promptChecks) {
|
|
133
|
+
const prompt = createScenarioPrompt(scenario.id, check.runtime);
|
|
134
|
+
const sentinel = extractSentinel(prompt);
|
|
135
|
+
const promptInstance = findPromptInstance(scenario, runtimeInstances, check.runtime);
|
|
136
|
+
logger.info(`scenario ${scenario.id}: waiting for ${check.runtime}`);
|
|
137
|
+
await waitForRuntimeReady(check.runtime);
|
|
138
|
+
logger.info(`scenario ${scenario.id}: prompting ${check.runtime}`);
|
|
139
|
+
const output = await promptRuntime(check.runtime, {
|
|
140
|
+
agentName: check.agentName,
|
|
141
|
+
command: options.dockerCommand,
|
|
142
|
+
configPath: promptInstance?.config_path,
|
|
143
|
+
containerName,
|
|
144
|
+
homePath: promptInstance?.home_path ?? undefined,
|
|
145
|
+
prompt
|
|
146
|
+
});
|
|
147
|
+
if (!output.includes(sentinel)) {
|
|
148
|
+
throw new SpawnfileError("runtime_error", `Scenario ${scenario.id} did not return sentinel ${sentinel} for ${check.runtime}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const logs = await readDockerLogs(options.dockerCommand, containerName);
|
|
154
|
+
throw new SpawnfileError("runtime_error", `${error instanceof Error ? error.message : String(error)}${logs ? `\n\nDocker logs:\n${logs}` : ""}`);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
await cleanupDockerArtifacts(options.dockerCommand, containerName, imageTag, {
|
|
158
|
+
keepImages: options.keepImages
|
|
159
|
+
});
|
|
160
|
+
await removeDirectory(invocation.supportDirectory);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (!options.keepArtifacts) {
|
|
164
|
+
await removeDirectory(scenarioRoot);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
durationMs: Date.now() - startedAt,
|
|
168
|
+
id: scenario.id,
|
|
169
|
+
success: true
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (!options.keepArtifacts) {
|
|
174
|
+
await removeDirectory(scenarioRoot);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
durationMs: Date.now() - startedAt,
|
|
178
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
179
|
+
id: scenario.id,
|
|
180
|
+
success: false
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const formatFailures = (results) => results
|
|
185
|
+
.filter((result) => !result.success)
|
|
186
|
+
.map((result) => `- ${result.id}: ${result.errorMessage ?? "unknown error"}`)
|
|
187
|
+
.join("\n");
|
|
188
|
+
export const runDockerAuthE2E = async (options = {}) => {
|
|
189
|
+
const logger = createLogger(options.logger);
|
|
190
|
+
const scenarios = filterDockerAuthE2EScenarios(options);
|
|
191
|
+
if (scenarios.length === 0) {
|
|
192
|
+
throw new SpawnfileError("validation_error", "No Docker auth E2E scenarios matched the filter");
|
|
193
|
+
}
|
|
194
|
+
const results = [];
|
|
195
|
+
for (const scenario of scenarios) {
|
|
196
|
+
const result = await runScenario(scenario, {
|
|
197
|
+
claudeCodeDirectory: options.claudeCodeDirectory,
|
|
198
|
+
codexDirectory: options.codexDirectory,
|
|
199
|
+
dockerCommand: options.dockerCommand ?? "docker",
|
|
200
|
+
envFilePath: options.envFilePath,
|
|
201
|
+
keepArtifacts: options.keepArtifacts ?? false,
|
|
202
|
+
keepImages: options.keepImages ?? false,
|
|
203
|
+
logger
|
|
204
|
+
});
|
|
205
|
+
results.push(result);
|
|
206
|
+
logger.info(`${result.success ? "PASS" : "FAIL"} ${result.id} (${Math.round(result.durationMs / 1000)}s)`);
|
|
207
|
+
}
|
|
208
|
+
if (results.some((result) => !result.success)) {
|
|
209
|
+
throw new SpawnfileError("runtime_error", `Docker auth E2E failed:\n${formatFailures(results)}`);
|
|
210
|
+
}
|
|
211
|
+
return { results };
|
|
212
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { copyDirectory, readUtf8File, writeUtf8File } from "../filesystem/index.js";
|
|
5
|
+
const FIXTURES_ROOT = fileURLToPath(new URL("../../fixtures/e2e", import.meta.url));
|
|
6
|
+
const readYamlFile = async (filePath) => YAML.parse(await readUtf8File(filePath));
|
|
7
|
+
const writeYamlFile = async (filePath, value) => {
|
|
8
|
+
await writeUtf8File(filePath, YAML.stringify(value));
|
|
9
|
+
};
|
|
10
|
+
const applyAgentSpec = (manifest, spec) => ({
|
|
11
|
+
...manifest,
|
|
12
|
+
execution: {
|
|
13
|
+
...(manifest.execution ?? {}),
|
|
14
|
+
model: {
|
|
15
|
+
...((manifest.execution?.model ?? {})),
|
|
16
|
+
auth: {
|
|
17
|
+
method: spec.authMethod
|
|
18
|
+
},
|
|
19
|
+
primary: {
|
|
20
|
+
name: spec.modelName,
|
|
21
|
+
provider: spec.provider
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
kind: "agent",
|
|
26
|
+
name: spec.name,
|
|
27
|
+
runtime: spec.runtime
|
|
28
|
+
});
|
|
29
|
+
const patchSingleAgentFixture = async (destinationDirectory, scenario) => {
|
|
30
|
+
const manifestPath = path.join(destinationDirectory, "Spawnfile");
|
|
31
|
+
const manifest = await readYamlFile(manifestPath);
|
|
32
|
+
await writeYamlFile(manifestPath, applyAgentSpec(manifest, scenario.agents[0]));
|
|
33
|
+
};
|
|
34
|
+
const patchTeamFixture = async (destinationDirectory, scenario) => {
|
|
35
|
+
for (const agent of scenario.agents) {
|
|
36
|
+
const manifestPath = path.join(destinationDirectory, "agents", agent.directoryName, "Spawnfile");
|
|
37
|
+
const manifest = await readYamlFile(manifestPath);
|
|
38
|
+
await writeYamlFile(manifestPath, applyAgentSpec(manifest, agent));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export const materializeDockerAuthFixture = async (scenario, destinationDirectory) => {
|
|
42
|
+
const sourceDirectory = path.join(FIXTURES_ROOT, scenario.fixture);
|
|
43
|
+
await copyDirectory(sourceDirectory, destinationDirectory);
|
|
44
|
+
if (scenario.fixture === "agent") {
|
|
45
|
+
await patchSingleAgentFixture(destinationDirectory, scenario);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await patchTeamFixture(destinationDirectory, scenario);
|
|
49
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { E2ERuntime } from "./types.js";
|
|
2
|
+
interface RuntimePromptOptions {
|
|
3
|
+
agentName?: string;
|
|
4
|
+
command?: string;
|
|
5
|
+
configPath?: string;
|
|
6
|
+
containerName: string;
|
|
7
|
+
homePath?: string;
|
|
8
|
+
prompt: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const waitForRuntimeReady: (runtime: E2ERuntime, timeoutMs?: number) => Promise<void>;
|
|
12
|
+
export declare const promptRuntime: (runtime: E2ERuntime, options: RuntimePromptOptions) => Promise<string>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
3
|
+
const wait = async (delayMs) => new Promise((resolve) => {
|
|
4
|
+
setTimeout(resolve, delayMs);
|
|
5
|
+
});
|
|
6
|
+
const runCommand = async (command, args, timeoutMs = 180_000) => new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(command, args, {
|
|
8
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
9
|
+
});
|
|
10
|
+
const stdout = [];
|
|
11
|
+
const stderr = [];
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
child.kill("SIGTERM");
|
|
14
|
+
reject(new SpawnfileError("runtime_error", `Command timed out after ${timeoutMs}ms: ${command} ${args.join(" ")}`));
|
|
15
|
+
}, timeoutMs);
|
|
16
|
+
child.stdout.on("data", (chunk) => {
|
|
17
|
+
stdout.push(String(chunk));
|
|
18
|
+
});
|
|
19
|
+
child.stderr.on("data", (chunk) => {
|
|
20
|
+
stderr.push(String(chunk));
|
|
21
|
+
});
|
|
22
|
+
child.once("error", (error) => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
reject(new SpawnfileError("runtime_error", `Unable to start command ${command}: ${error.message}`));
|
|
25
|
+
});
|
|
26
|
+
child.once("exit", (code, signal) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
if (code === 0) {
|
|
29
|
+
resolve({
|
|
30
|
+
stderr: stderr.join(""),
|
|
31
|
+
stdout: stdout.join("")
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
reject(new SpawnfileError("runtime_error", signal
|
|
36
|
+
? `Command exited from signal ${signal}: ${command} ${args.join(" ")}`
|
|
37
|
+
: `Command failed with exit code ${code ?? "unknown"}: ${command} ${args.join(" ")}\n${stderr.join("")}`.trim()));
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const fetchText = async (url) => {
|
|
41
|
+
const response = await fetch(url);
|
|
42
|
+
return {
|
|
43
|
+
body: await response.text(),
|
|
44
|
+
status: response.status
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const getHealthUrl = (runtime) => runtime === "openclaw"
|
|
48
|
+
? "http://127.0.0.1:18789/healthz"
|
|
49
|
+
: runtime === "picoclaw"
|
|
50
|
+
? "http://127.0.0.1:18790/health"
|
|
51
|
+
: "http://127.0.0.1:3777/api/agents";
|
|
52
|
+
export const waitForRuntimeReady = async (runtime, timeoutMs = 120_000) => {
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
const url = getHealthUrl(runtime);
|
|
55
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(url);
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Ignore readiness races and keep polling.
|
|
64
|
+
}
|
|
65
|
+
await wait(2_000);
|
|
66
|
+
}
|
|
67
|
+
throw new SpawnfileError("runtime_error", `Runtime ${runtime} did not become ready within ${timeoutMs}ms (${url})`);
|
|
68
|
+
};
|
|
69
|
+
const promptOpenClaw = async (options) => {
|
|
70
|
+
const result = await runCommand(options.command ?? "docker", [
|
|
71
|
+
"exec",
|
|
72
|
+
"-u",
|
|
73
|
+
"0",
|
|
74
|
+
...(options.homePath ? ["-e", `OPENCLAW_HOME=${options.homePath}`] : []),
|
|
75
|
+
...(options.configPath ? ["-e", `OPENCLAW_CONFIG_PATH=${options.configPath}`] : []),
|
|
76
|
+
options.containerName,
|
|
77
|
+
"openclaw",
|
|
78
|
+
"agent",
|
|
79
|
+
"--local",
|
|
80
|
+
"--agent",
|
|
81
|
+
"main",
|
|
82
|
+
"--message",
|
|
83
|
+
options.prompt,
|
|
84
|
+
"--json"
|
|
85
|
+
], options.timeoutMs);
|
|
86
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
87
|
+
};
|
|
88
|
+
const promptPicoClaw = async (options) => {
|
|
89
|
+
const result = await runCommand(options.command ?? "docker", [
|
|
90
|
+
"exec",
|
|
91
|
+
...(options.homePath ? ["-e", `HOME=${options.homePath}`, "-e", `PICOCLAW_HOME=${options.homePath}`] : []),
|
|
92
|
+
...(options.configPath ? ["-e", `PICOCLAW_CONFIG=${options.configPath}`] : []),
|
|
93
|
+
options.containerName,
|
|
94
|
+
"picoclaw",
|
|
95
|
+
"agent",
|
|
96
|
+
"-m",
|
|
97
|
+
options.prompt
|
|
98
|
+
], options.timeoutMs);
|
|
99
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
100
|
+
};
|
|
101
|
+
const promptTinyClaw = async (options) => {
|
|
102
|
+
const enqueueResponse = await fetch("http://127.0.0.1:3777/api/message", {
|
|
103
|
+
body: JSON.stringify({
|
|
104
|
+
...(options.agentName ? { agent: options.agentName } : {}),
|
|
105
|
+
channel: "spawnfile-e2e",
|
|
106
|
+
message: options.prompt,
|
|
107
|
+
sender: "spawnfile-e2e"
|
|
108
|
+
}),
|
|
109
|
+
headers: {
|
|
110
|
+
"Content-Type": "application/json"
|
|
111
|
+
},
|
|
112
|
+
method: "POST"
|
|
113
|
+
});
|
|
114
|
+
if (!enqueueResponse.ok) {
|
|
115
|
+
throw new SpawnfileError("runtime_error", `TinyClaw enqueue failed with status ${enqueueResponse.status}`);
|
|
116
|
+
}
|
|
117
|
+
const startedAt = Date.now();
|
|
118
|
+
const timeoutMs = options.timeoutMs ?? 180_000;
|
|
119
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
120
|
+
const { body, status } = await fetchText("http://127.0.0.1:3777/api/responses?limit=20");
|
|
121
|
+
if (status === 200 && body.includes(options.prompt.replace("Reply with exactly ", "").replace(" and nothing else.", ""))) {
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
await wait(2_000);
|
|
125
|
+
}
|
|
126
|
+
throw new SpawnfileError("runtime_error", `TinyClaw did not return a response within ${timeoutMs}ms`);
|
|
127
|
+
};
|
|
128
|
+
export const promptRuntime = async (runtime, options) => runtime === "openclaw"
|
|
129
|
+
? promptOpenClaw(options)
|
|
130
|
+
: runtime === "picoclaw"
|
|
131
|
+
? promptPicoClaw(options)
|
|
132
|
+
: promptTinyClaw(options);
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const createSingleAgentScenario = (runtime, provider, modelName, authMethod) => {
|
|
2
|
+
const id = `${runtime}-${authMethod}`;
|
|
3
|
+
const agent = {
|
|
4
|
+
authMethod,
|
|
5
|
+
directoryName: runtime,
|
|
6
|
+
modelName,
|
|
7
|
+
name: `${runtime}-assistant`,
|
|
8
|
+
provider,
|
|
9
|
+
runtime
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
agents: [agent],
|
|
13
|
+
description: `${runtime} single-agent Docker auth smoke using ${authMethod}`,
|
|
14
|
+
fixture: "agent",
|
|
15
|
+
id,
|
|
16
|
+
kind: "single-agent",
|
|
17
|
+
promptChecks: [{ runtime }]
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const SINGLE_AGENT_SCENARIOS = [
|
|
21
|
+
createSingleAgentScenario("openclaw", "openai", "gpt-5", "api_key"),
|
|
22
|
+
createSingleAgentScenario("openclaw", "openai", "gpt-5", "codex"),
|
|
23
|
+
createSingleAgentScenario("openclaw", "anthropic", "claude-sonnet-4-5", "claude-code"),
|
|
24
|
+
createSingleAgentScenario("picoclaw", "openai", "gpt-5", "api_key"),
|
|
25
|
+
createSingleAgentScenario("picoclaw", "openai", "gpt-5", "codex"),
|
|
26
|
+
createSingleAgentScenario("picoclaw", "anthropic", "claude-sonnet-4-5", "claude-code"),
|
|
27
|
+
createSingleAgentScenario("tinyclaw", "openai", "gpt-5", "codex"),
|
|
28
|
+
createSingleAgentScenario("tinyclaw", "anthropic", "claude-sonnet-4-5", "claude-code")
|
|
29
|
+
];
|
|
30
|
+
const TEAM_SCENARIOS = [
|
|
31
|
+
{
|
|
32
|
+
agents: [
|
|
33
|
+
{
|
|
34
|
+
authMethod: "codex",
|
|
35
|
+
directoryName: "openclaw",
|
|
36
|
+
modelName: "gpt-5",
|
|
37
|
+
name: "openclaw",
|
|
38
|
+
provider: "openai",
|
|
39
|
+
runtime: "openclaw"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
authMethod: "api_key",
|
|
43
|
+
directoryName: "picoclaw",
|
|
44
|
+
modelName: "gpt-5",
|
|
45
|
+
name: "picoclaw",
|
|
46
|
+
provider: "openai",
|
|
47
|
+
runtime: "picoclaw"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
authMethod: "codex",
|
|
51
|
+
directoryName: "tinyclaw",
|
|
52
|
+
modelName: "gpt-5",
|
|
53
|
+
name: "tinyclaw",
|
|
54
|
+
provider: "openai",
|
|
55
|
+
runtime: "tinyclaw"
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
description: "multi-runtime Docker auth smoke team",
|
|
59
|
+
fixture: "team",
|
|
60
|
+
id: "team-multi-runtime",
|
|
61
|
+
kind: "team",
|
|
62
|
+
promptChecks: [
|
|
63
|
+
{ runtime: "openclaw" },
|
|
64
|
+
{ runtime: "picoclaw" },
|
|
65
|
+
{ agentName: "tinyclaw", runtime: "tinyclaw" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
];
|
|
69
|
+
export const listDockerAuthE2EScenarios = () => [
|
|
70
|
+
...SINGLE_AGENT_SCENARIOS,
|
|
71
|
+
...TEAM_SCENARIOS
|
|
72
|
+
];
|
|
73
|
+
const includesScenarioId = (filters, scenario) => !filters.scenarioIds ||
|
|
74
|
+
filters.scenarioIds.length === 0 ||
|
|
75
|
+
filters.scenarioIds.includes(scenario.id);
|
|
76
|
+
const includesAuthMethod = (filters, scenario) => !filters.authMethods ||
|
|
77
|
+
filters.authMethods.length === 0 ||
|
|
78
|
+
scenario.agents.some((agent) => filters.authMethods.includes(agent.authMethod));
|
|
79
|
+
const includesRuntime = (filters, scenario) => !filters.runtimes ||
|
|
80
|
+
filters.runtimes.length === 0 ||
|
|
81
|
+
scenario.agents.some((agent) => filters.runtimes.includes(agent.runtime));
|
|
82
|
+
export const filterDockerAuthE2EScenarios = (filters = {}) => listDockerAuthE2EScenarios().filter((scenario) => includesScenarioId(filters, scenario) &&
|
|
83
|
+
includesAuthMethod(filters, scenario) &&
|
|
84
|
+
includesRuntime(filters, scenario));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ModelAuthMethod } from "../shared/index.js";
|
|
2
|
+
export type E2ERuntime = "openclaw" | "picoclaw" | "tinyclaw";
|
|
3
|
+
export type E2EFixtureKind = "agent" | "team";
|
|
4
|
+
export type E2EScenarioKind = "single-agent" | "team";
|
|
5
|
+
export interface E2EAgentSpec {
|
|
6
|
+
authMethod: ModelAuthMethod;
|
|
7
|
+
directoryName: string;
|
|
8
|
+
modelName: string;
|
|
9
|
+
name: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
runtime: E2ERuntime;
|
|
12
|
+
}
|
|
13
|
+
export interface E2EPromptCheck {
|
|
14
|
+
agentName?: string;
|
|
15
|
+
runtime: E2ERuntime;
|
|
16
|
+
}
|
|
17
|
+
export interface DockerAuthE2EScenario {
|
|
18
|
+
agents: E2EAgentSpec[];
|
|
19
|
+
description: string;
|
|
20
|
+
fixture: E2EFixtureKind;
|
|
21
|
+
id: string;
|
|
22
|
+
kind: E2EScenarioKind;
|
|
23
|
+
promptChecks: E2EPromptCheck[];
|
|
24
|
+
}
|
|
25
|
+
export interface DockerAuthE2EFilters {
|
|
26
|
+
authMethods?: ModelAuthMethod[];
|
|
27
|
+
runtimes?: E2ERuntime[];
|
|
28
|
+
scenarioIds?: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface DockerAuthE2EScenarioResult {
|
|
31
|
+
durationMs: number;
|
|
32
|
+
errorMessage?: string;
|
|
33
|
+
id: string;
|
|
34
|
+
success: boolean;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
require_env() {
|
|
5
|
+
local name="$1"
|
|
6
|
+
if [ -z "${!name:-}" ]; then
|
|
7
|
+
echo "Missing required env: $name" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
require_file() {
|
|
13
|
+
local target="$1"
|
|
14
|
+
if [ ! -f "$target" ]; then
|
|
15
|
+
echo "Missing required file: $target" >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
write_env_file() {
|
|
21
|
+
local name="$1"
|
|
22
|
+
local target="$2"
|
|
23
|
+
if [ -z "${!name:-}" ]; then
|
|
24
|
+
return
|
|
25
|
+
fi
|
|
26
|
+
mkdir -p "$(dirname "$target")"
|
|
27
|
+
printf %s "${!name:-}" > "$target"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
apply_json_env_value() {
|
|
31
|
+
local target="$1"
|
|
32
|
+
local name="$2"
|
|
33
|
+
local json_path="$3"
|
|
34
|
+
if [ -z "${!name:-}" ]; then
|
|
35
|
+
return
|
|
36
|
+
fi
|
|
37
|
+
python3 - "$target" "$name" "$json_path" <<'PY'
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import sys
|
|
41
|
+
|
|
42
|
+
target_path = sys.argv[1]
|
|
43
|
+
env_name = sys.argv[2]
|
|
44
|
+
json_path = sys.argv[3].split('.')
|
|
45
|
+
value = os.environ.get(env_name)
|
|
46
|
+
if value is None:
|
|
47
|
+
raise SystemExit(0)
|
|
48
|
+
|
|
49
|
+
with open(target_path, encoding='utf-8') as handle:
|
|
50
|
+
data = json.load(handle)
|
|
51
|
+
|
|
52
|
+
cursor = data
|
|
53
|
+
for part in json_path[:-1]:
|
|
54
|
+
child = cursor.get(part)
|
|
55
|
+
if not isinstance(child, dict):
|
|
56
|
+
child = {}
|
|
57
|
+
cursor[part] = child
|
|
58
|
+
cursor = child
|
|
59
|
+
|
|
60
|
+
cursor[json_path[-1]] = value
|
|
61
|
+
|
|
62
|
+
with open(target_path, 'w', encoding='utf-8') as handle:
|
|
63
|
+
json.dump(data, handle, indent=2)
|
|
64
|
+
handle.write('\n')
|
|
65
|
+
PY
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
mkdir -p '/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/workspace'
|
|
69
|
+
require_file '/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/config.json'
|
|
70
|
+
write_env_file 'ANTHROPIC_API_KEY' '/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/secrets/ANTHROPIC_API_KEY'
|
|
71
|
+
HOME='/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw' PICOCLAW_HOME='/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw' PICOCLAW_CONFIG='/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/config.json' PICOCLAW_GATEWAY_PORT='18790' PICOCLAW_GATEWAY_HOST='0.0.0.0' exec 'picoclaw' 'gateway' '--allow-empty'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface CopyDirectoryOptions {
|
|
2
|
+
filter?: (sourcePath: string, destinationPath: string) => boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare const copyDirectory: (sourcePath: string, destinationPath: string, options?: CopyDirectoryOptions) => Promise<void>;
|
|
5
|
+
export declare const ensureDirectory: (directoryPath: string) => Promise<void>;
|
|
6
|
+
export declare const fileExists: (filePath: string) => Promise<boolean>;
|
|
7
|
+
export declare const isSymlink: (filePath: string) => Promise<boolean>;
|
|
8
|
+
export declare const readUtf8File: (filePath: string) => Promise<string>;
|
|
9
|
+
export declare const ensureGitignoreEntry: (directoryPath: string, entry: string) => Promise<boolean>;
|
|
10
|
+
export declare const removeDirectory: (directoryPath: string) => Promise<void>;
|
|
11
|
+
export declare const writeUtf8File: (filePath: string, content: string) => Promise<void>;
|