spawnfile 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -54,31 +54,31 @@ Spawnfile v0.1 is implemented as a Node.js CLI in TypeScript.
54
54
  - manifest parsing: `yaml` + `zod`
55
55
  - tests: `vitest`
56
56
 
57
- That gives us fast iteration, a conventional `bin`-based CLI, and a clean path to future install surfaces such as npm or a shell bootstrapper.
57
+ That gives us fast iteration and a conventional `bin`-based CLI published to [npm](https://www.npmjs.com/package/spawnfile).
58
58
 
59
59
  ---
60
60
 
61
61
  ## Install
62
62
 
63
- From a repository checkout:
64
-
65
63
  ```bash
66
- nvm use
67
- npm install
68
- npm run build
69
- npm link
64
+ npm install -g spawnfile
70
65
  ```
71
66
 
72
- Or use the bootstrap script:
67
+ Verify:
73
68
 
74
69
  ```bash
75
- ./scripts/install.sh
70
+ spawnfile --help
76
71
  ```
77
72
 
78
- Then:
73
+ ### From source
79
74
 
80
75
  ```bash
81
- spawnfile --help
76
+ git clone https://github.com/noopolis/spawnfile.git
77
+ cd spawnfile
78
+ nvm use
79
+ npm install
80
+ npm run build
81
+ npm link
82
82
  ```
83
83
 
84
84
  To clone the target runtimes and generate reference blueprints:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spawnfile",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Canonical source compiler for autonomous agents and teams.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,10 +16,15 @@
16
16
  },
17
17
  "scripts": {
18
18
  "blueprints": "./scripts/blueprints.sh",
19
- "build": "tsc --project tsconfig.build.json && node ./scripts/copy-runtime-scaffold-assets.mjs",
19
+ "build": "rm -rf dist && tsc --project tsconfig.build.json && node ./scripts/copy-runtime-scaffold-assets.mjs",
20
20
  "clean": "rm -rf coverage dist",
21
21
  "coverage": "vitest run --coverage",
22
22
  "dev": "tsx src/cli/index.ts",
23
+ "prepack": "npm run build",
24
+ "prepublishOnly": "npm run typecheck && npm test",
25
+ "release:patch": "npm version patch && npm publish",
26
+ "release:minor": "npm version minor && npm publish",
27
+ "release:major": "npm version major && npm publish",
23
28
  "runtimes": "./scripts/runtimes.sh",
24
29
  "runtimes:sync": "./scripts/runtimes.sh && ./scripts/blueprints.sh",
25
30
  "test:e2e:docker-auth": "tsx src/e2e/cli.ts",
package/dist/.env.example DELETED
@@ -1,5 +0,0 @@
1
- # Generated by spawnfile compile
2
-
3
- # Required
4
- # Model provider auth for ANTHROPIC_API_KEY
5
- ANTHROPIC_API_KEY=
package/dist/Dockerfile DELETED
@@ -1,21 +0,0 @@
1
- FROM debian:bookworm-slim
2
- USER root
3
-
4
- WORKDIR /opt/spawnfile
5
- RUN apt-get update && apt-get install -y --no-install-recommends bash ca-certificates curl nodejs npm tar && rm -rf /var/lib/apt/lists/*
6
-
7
- RUN npm install -g --omit=dev --no-fund --no-audit @anthropic-ai/claude-code @openai/codex
8
-
9
- RUN mkdir -p /opt/spawnfile/runtime-installs/picoclaw/bin
10
- RUN arch="$(dpkg --print-architecture)" && case "$arch" in amd64) asset="picoclaw_Linux_x86_64.tar.gz" ;; arm64) asset="picoclaw_Linux_arm64.tar.gz" ;; *) echo "Unsupported PicoClaw release architecture: $arch" >&2; exit 1 ;; esac && url="https://github.com/sipeed/picoclaw/releases/download/v0.2.3/$asset" && curl -fsSL -o /tmp/picoclaw.tar.gz "$url" && rm -rf /tmp/picoclaw-extract && mkdir -p /tmp/picoclaw-extract && tar -xzf /tmp/picoclaw.tar.gz -C /tmp/picoclaw-extract && binary_path="$(find /tmp/picoclaw-extract -type f -name "picoclaw" | head -n 1)" && [ -n "$binary_path" ] && install -m 0755 "$binary_path" /opt/spawnfile/runtime-installs/picoclaw/bin/picoclaw && ln -sf /opt/spawnfile/runtime-installs/picoclaw/bin/picoclaw /usr/local/bin/picoclaw && rm -rf /tmp/picoclaw.tar.gz /tmp/picoclaw-extract
11
-
12
- RUN if ! id -u spawnfile >/dev/null 2>&1; then useradd --create-home --home-dir /home/spawnfile --shell /bin/bash spawnfile; fi
13
-
14
- COPY container/rootfs/ /
15
- COPY .env.example /opt/spawnfile/.env.example
16
- COPY entrypoint.sh /opt/spawnfile/entrypoint.sh
17
- RUN chmod +x /opt/spawnfile/entrypoint.sh
18
- RUN mkdir -p /var/lib/spawnfile && chown -R spawnfile:spawnfile /var/lib/spawnfile /opt/spawnfile
19
- EXPOSE 18790
20
- USER spawnfile
21
- ENTRYPOINT ["/opt/spawnfile/entrypoint.sh"]
@@ -1,4 +0,0 @@
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[];
@@ -1,28 +0,0 @@
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
- };
@@ -1,16 +0,0 @@
1
- {
2
- "agents": {
3
- "defaults": {
4
- "workspace": "/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/workspace",
5
- "restrict_to_workspace": true,
6
- "model_name": "claude-sonnet-4.6"
7
- }
8
- },
9
- "model_list": [
10
- {
11
- "api_key": "file://secrets/ANTHROPIC_API_KEY",
12
- "model_name": "claude-sonnet-4.6",
13
- "model": "anthropic/claude-sonnet-4.6"
14
- }
15
- ]
16
- }
@@ -1 +0,0 @@
1
- You are a concise assistant. Reply with exactly the requested token and nothing else.
package/dist/e2e/cli.d.ts DELETED
@@ -1 +0,0 @@
1
- export {};
package/dist/e2e/cli.js DELETED
@@ -1,40 +0,0 @@
1
- import { Command } from "commander";
2
- import { isSpawnfileError } from "../shared/index.js";
3
- import { runDockerAuthE2E } from "./dockerAuth.js";
4
- const collect = (value, previous) => [...previous, value];
5
- const main = async () => {
6
- const program = new Command();
7
- program
8
- .name("spawnfile-e2e")
9
- .description("Run opt-in Docker auth E2E scenarios against real runtime images")
10
- .option("--scenario <id>", "Scenario id to run", collect, [])
11
- .option("--runtime <runtime>", "Runtime filter", collect, [])
12
- .option("--auth <method>", "Auth method filter", collect, [])
13
- .option("--env-file <path>", "Env file for api_key scenarios")
14
- .option("--claude-from <directory>", "Claude Code config directory override")
15
- .option("--codex-from <directory>", "Codex config directory override")
16
- .option("--keep-artifacts", "Keep temporary projects and compile output")
17
- .option("--keep-images", "Keep built Docker images after each scenario");
18
- await program.parseAsync(process.argv);
19
- const options = program.opts();
20
- const result = await runDockerAuthE2E({
21
- authMethods: options.auth,
22
- claudeCodeDirectory: options.claudeFrom,
23
- codexDirectory: options.codexFrom,
24
- envFilePath: options.envFile,
25
- keepArtifacts: options.keepArtifacts,
26
- keepImages: options.keepImages,
27
- runtimes: options.runtime,
28
- scenarioIds: options.scenario
29
- });
30
- console.log(`Docker auth E2E passed (${result.results.length} scenarios)`);
31
- };
32
- main().catch((error) => {
33
- const message = isSpawnfileError(error)
34
- ? `${error.code}: ${error.message}`
35
- : error instanceof Error
36
- ? error.message
37
- : String(error);
38
- process.stderr.write(`${message}\n`);
39
- process.exitCode = 1;
40
- });
@@ -1,18 +0,0 @@
1
- import type { DockerAuthE2EFilters, DockerAuthE2EScenarioResult } from "./types.js";
2
- export interface DockerAuthE2ELogger {
3
- error(message: string): void;
4
- info(message: string): void;
5
- }
6
- export interface RunDockerAuthE2EOptions extends DockerAuthE2EFilters {
7
- claudeCodeDirectory?: string;
8
- codexDirectory?: string;
9
- dockerCommand?: string;
10
- envFilePath?: string;
11
- keepArtifacts?: boolean;
12
- keepImages?: boolean;
13
- logger?: DockerAuthE2ELogger;
14
- }
15
- export interface RunDockerAuthE2EResult {
16
- results: DockerAuthE2EScenarioResult[];
17
- }
18
- export declare const runDockerAuthE2E: (options?: RunDockerAuthE2EOptions) => Promise<RunDockerAuthE2EResult>;
@@ -1,212 +0,0 @@
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
- };
@@ -1,2 +0,0 @@
1
- import type { DockerAuthE2EScenario } from "./types.js";
2
- export declare const materializeDockerAuthFixture: (scenario: DockerAuthE2EScenario, destinationDirectory: string) => Promise<void>;
@@ -1,49 +0,0 @@
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
- };
@@ -1,4 +0,0 @@
1
- export * from "./dockerAuth.js";
2
- export * from "./fixtures.js";
3
- export * from "./scenarios.js";
4
- export * from "./types.js";
package/dist/e2e/index.js DELETED
@@ -1,4 +0,0 @@
1
- export * from "./dockerAuth.js";
2
- export * from "./fixtures.js";
3
- export * from "./scenarios.js";
4
- export * from "./types.js";
@@ -1,13 +0,0 @@
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 {};
@@ -1,132 +0,0 @@
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);
@@ -1,3 +0,0 @@
1
- import type { DockerAuthE2EFilters, DockerAuthE2EScenario } from "./types.js";
2
- export declare const listDockerAuthE2EScenarios: () => DockerAuthE2EScenario[];
3
- export declare const filterDockerAuthE2EScenarios: (filters?: DockerAuthE2EFilters) => DockerAuthE2EScenario[];
@@ -1,84 +0,0 @@
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));
@@ -1,35 +0,0 @@
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
- }
package/dist/e2e/types.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,71 +0,0 @@
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'
@@ -1,16 +0,0 @@
1
- {
2
- "agents": {
3
- "defaults": {
4
- "workspace": "<workspace-path>",
5
- "restrict_to_workspace": true,
6
- "model_name": "claude-sonnet-4.6"
7
- }
8
- },
9
- "model_list": [
10
- {
11
- "api_key": "file://secrets/ANTHROPIC_API_KEY",
12
- "model_name": "claude-sonnet-4.6",
13
- "model": "anthropic/claude-sonnet-4.6"
14
- }
15
- ]
16
- }
@@ -1 +0,0 @@
1
- You are a concise assistant. Reply with exactly the requested token and nothing else.
@@ -1,71 +0,0 @@
1
- {
2
- "container": {
3
- "dockerfile": "Dockerfile",
4
- "entrypoint": "entrypoint.sh",
5
- "env_example": ".env.example",
6
- "model_secrets_required": [
7
- "ANTHROPIC_API_KEY"
8
- ],
9
- "ports": [
10
- 18790
11
- ],
12
- "runtime_instances": [
13
- {
14
- "config_path": "/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw/config.json",
15
- "home_path": "/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw",
16
- "id": "agent-assistant",
17
- "model_secrets_required": [
18
- "ANTHROPIC_API_KEY"
19
- ],
20
- "runtime": "picoclaw"
21
- }
22
- ],
23
- "runtime_homes": [
24
- "/var/lib/spawnfile/instances/picoclaw/agent-assistant/picoclaw"
25
- ],
26
- "runtime_secrets_required": [],
27
- "runtimes_installed": [
28
- "picoclaw"
29
- ],
30
- "secrets_required": [
31
- "ANTHROPIC_API_KEY"
32
- ]
33
- },
34
- "diagnostics": [],
35
- "nodes": [
36
- {
37
- "capabilities": [
38
- {
39
- "key": "docs.system",
40
- "message": "",
41
- "outcome": "supported"
42
- },
43
- {
44
- "key": "execution.model",
45
- "message": "",
46
- "outcome": "supported"
47
- },
48
- {
49
- "key": "execution.workspace",
50
- "message": "",
51
- "outcome": "supported"
52
- },
53
- {
54
- "key": "execution.sandbox",
55
- "message": "",
56
- "outcome": "supported"
57
- }
58
- ],
59
- "diagnostics": [],
60
- "id": "agent:assistant",
61
- "kind": "agent",
62
- "output_dir": "runtimes/picoclaw/agents/assistant",
63
- "runtime": "picoclaw",
64
- "runtime_ref": "v0.2.3",
65
- "runtime_status": "active",
66
- "source": "/tmp/spawnfile-e2e/picoclaw-anthropic/Spawnfile"
67
- }
68
- ],
69
- "root": "/tmp/spawnfile-e2e/picoclaw-anthropic/Spawnfile",
70
- "spawnfile_version": "0.1"
71
- }