spawnfile 0.1.10 → 0.1.12
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 +13 -2
- package/dist/cli/lifecycleCommands.js +101 -1
- package/dist/cli/modelCommands.js +3 -0
- package/dist/cli/resolveCommandInput.d.ts +19 -0
- package/dist/cli/resolveCommandInput.js +25 -0
- package/dist/cli/runCli.d.ts +4 -1
- package/dist/cli/runCli.js +37 -13
- package/dist/cli/runtimeCommands.js +1 -0
- package/dist/cli/statusCommand.d.ts +42 -0
- package/dist/cli/statusCommand.js +394 -0
- package/dist/cli/surfaceCommands.js +4 -0
- package/dist/compiler/agentSurfaces.js +2 -4
- package/dist/compiler/buildCompilePlanTeams.js +3 -1
- package/dist/compiler/compileProject.js +19 -10
- package/dist/compiler/compileProjectSupport.d.ts +1 -0
- package/dist/compiler/compileProjectSupport.js +1 -1
- package/dist/compiler/containerArtifacts.d.ts +1 -0
- package/dist/compiler/containerArtifacts.js +208 -32
- package/dist/compiler/containerArtifactsPlans.js +7 -4
- package/dist/compiler/containerArtifactsRender.d.ts +7 -1
- package/dist/compiler/containerArtifactsRender.js +6 -0
- package/dist/compiler/containerArtifactsTypes.d.ts +9 -0
- package/dist/compiler/containerEntrypointRender.js +18 -31
- package/dist/compiler/index.d.ts +1 -0
- package/dist/compiler/index.js +1 -0
- package/dist/compiler/moltnetArtifacts.d.ts +2 -0
- package/dist/compiler/moltnetArtifacts.js +58 -99
- package/dist/compiler/moltnetClientConfig.d.ts +2 -2
- package/dist/compiler/moltnetClientConfig.js +16 -23
- package/dist/compiler/moltnetConfigLowering.d.ts +3 -0
- package/dist/compiler/moltnetConfigLowering.js +13 -2
- package/dist/compiler/moltnetNodeConfig.d.ts +27 -0
- package/dist/compiler/moltnetNodeConfig.js +68 -0
- package/dist/compiler/moltnetResolution.js +1 -1
- package/dist/compiler/moltnetRoomMemberships.js +1 -2
- package/dist/compiler/moltnetRoomPolicyCompatibility.d.ts +4 -0
- package/dist/compiler/moltnetRoomPolicyCompatibility.js +28 -0
- package/dist/compiler/moltnetRuntimeConfig.d.ts +1 -1
- package/dist/compiler/moltnetRuntimeConfig.js +1 -13
- package/dist/compiler/networkBinding.d.ts +14 -0
- package/dist/compiler/networkBinding.js +34 -0
- package/dist/compiler/publishProject.d.ts +13 -0
- package/dist/compiler/publishProject.js +72 -0
- package/dist/compiler/runProject.d.ts +26 -12
- package/dist/compiler/runProject.js +102 -125
- package/dist/compiler/runProjectAuth.d.ts +5 -0
- package/dist/compiler/runProjectAuth.js +98 -0
- package/dist/compiler/runProjectDocker.d.ts +19 -0
- package/dist/compiler/runProjectDocker.js +57 -0
- package/dist/compiler/types.d.ts +3 -2
- package/dist/compiler/upProject.d.ts +6 -0
- package/dist/compiler/upProject.js +42 -8
- package/dist/compiler/view/buildOrganizationView.js +27 -3
- package/dist/compiler/view/declarations.d.ts +4 -0
- package/dist/compiler/view/declarations.js +104 -0
- package/dist/compiler/view/renderNetworks.js +12 -3
- package/dist/compiler/view/renderTree.js +16 -3
- package/dist/compiler/view/types.d.ts +77 -1
- package/dist/deployment/dockerInspect.d.ts +29 -0
- package/dist/deployment/dockerInspect.js +130 -0
- package/dist/deployment/dockerLabels.d.ts +16 -0
- package/dist/deployment/dockerLabels.js +32 -0
- package/dist/deployment/dockerLogs.d.ts +24 -0
- package/dist/deployment/dockerLogs.js +96 -0
- package/dist/deployment/dockerManager.d.ts +59 -0
- package/dist/deployment/dockerManager.js +76 -0
- package/dist/deployment/dockerProbeGateway.d.ts +16 -0
- package/dist/deployment/dockerProbeGateway.js +48 -0
- package/dist/deployment/homeStore.d.ts +19 -0
- package/dist/deployment/homeStore.js +130 -0
- package/dist/deployment/index.d.ts +9 -0
- package/dist/deployment/index.js +9 -0
- package/dist/deployment/names.d.ts +3 -0
- package/dist/deployment/names.js +12 -0
- package/dist/deployment/record.d.ts +76 -0
- package/dist/deployment/record.js +174 -0
- package/dist/deployment/target.d.ts +35 -0
- package/dist/deployment/target.js +96 -0
- package/dist/distribution/buildDistributionReport.d.ts +22 -0
- package/dist/distribution/buildDistributionReport.js +51 -0
- package/dist/distribution/consumeImage.d.ts +29 -0
- package/dist/distribution/consumeImage.js +254 -0
- package/dist/distribution/consumeImageSupport.d.ts +18 -0
- package/dist/distribution/consumeImageSupport.js +63 -0
- package/dist/distribution/distributionReportSchema.d.ts +84 -0
- package/dist/distribution/distributionReportSchema.js +107 -0
- package/dist/distribution/dockerRunner.d.ts +5 -0
- package/dist/distribution/dockerRunner.js +21 -0
- package/dist/distribution/extractImage.d.ts +25 -0
- package/dist/distribution/extractImage.js +96 -0
- package/dist/distribution/fingerprint.d.ts +2 -0
- package/dist/distribution/fingerprint.js +14 -0
- package/dist/distribution/imageRef.d.ts +9 -0
- package/dist/distribution/imageRef.js +65 -0
- package/dist/distribution/imageRuntimeAuth.d.ts +19 -0
- package/dist/distribution/imageRuntimeAuth.js +71 -0
- package/dist/distribution/index.d.ts +24 -0
- package/dist/distribution/index.js +16 -0
- package/dist/distribution/preflight.d.ts +11 -0
- package/dist/distribution/preflight.js +65 -0
- package/dist/distribution/projectImageView.d.ts +8 -0
- package/dist/distribution/projectImageView.js +83 -0
- package/dist/distribution/projectName.d.ts +1 -0
- package/dist/distribution/projectName.js +12 -0
- package/dist/distribution/renderImageInterface.d.ts +30 -0
- package/dist/distribution/renderImageInterface.js +86 -0
- package/dist/distribution/tarReader.d.ts +10 -0
- package/dist/distribution/tarReader.js +69 -0
- package/dist/distribution/types.d.ts +80 -0
- package/dist/distribution/types.js +4 -0
- package/dist/distribution/verifyDistributionReport.d.ts +12 -0
- package/dist/distribution/verifyDistributionReport.js +29 -0
- package/dist/filesystem/paths.d.ts +6 -0
- package/dist/filesystem/paths.js +11 -0
- package/dist/manifest/loadManifest.js +22 -2
- package/dist/manifest/renderSpawnfile.js +2 -4
- package/dist/manifest/renderSpawnfileNetworks.js +5 -0
- package/dist/manifest/schemas.d.ts +71 -41
- package/dist/manifest/surfaceSchemas.d.ts +19 -47
- package/dist/manifest/surfaceSchemas.js +3 -6
- package/dist/manifest/teamNetworkAccessSchemas.d.ts +15 -0
- package/dist/manifest/teamNetworkAccessSchemas.js +8 -0
- package/dist/manifest/teamNetworkConsoleSchemas.d.ts +7 -0
- package/dist/manifest/teamNetworkConsoleSchemas.js +19 -0
- package/dist/manifest/teamNetworkSchemas.d.ts +60 -0
- package/dist/manifest/teamNetworkSchemas.js +20 -7
- package/dist/report/createReport.d.ts +7 -1
- package/dist/report/createReport.js +28 -1
- package/dist/report/types.d.ts +43 -0
- package/dist/runtime/container.js +0 -16
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/install.d.ts +0 -10
- package/dist/runtime/install.js +0 -45
- package/dist/runtime/openclaw/adapter.js +9 -1
- package/dist/runtime/openclaw/configAuth.d.ts +15 -0
- package/dist/runtime/openclaw/configAuth.js +46 -0
- package/dist/runtime/openclaw/runAuth.d.ts +6 -0
- package/dist/runtime/openclaw/runAuth.js +7 -55
- package/dist/runtime/openclaw/statusProbes.d.ts +2 -0
- package/dist/runtime/openclaw/statusProbes.js +31 -0
- package/dist/runtime/picoclaw/adapter.js +10 -0
- package/dist/runtime/picoclaw/runAuth.d.ts +7 -0
- package/dist/runtime/picoclaw/runAuth.js +8 -68
- package/dist/runtime/picoclaw/statusProbes.d.ts +2 -0
- package/dist/runtime/picoclaw/statusProbes.js +100 -0
- package/dist/runtime/registry.d.ts +0 -5
- package/dist/runtime/registry.js +2 -12
- package/dist/runtime/statusProbes.d.ts +16 -0
- package/dist/runtime/statusProbes.js +41 -0
- package/dist/runtime/types.d.ts +52 -1
- package/dist/shared/errors.d.ts +6 -0
- package/dist/shared/errors.js +9 -0
- package/dist/status/buildStatus.d.ts +13 -0
- package/dist/status/buildStatus.js +266 -0
- package/dist/status/compileReport.d.ts +96 -0
- package/dist/status/compileReport.js +230 -0
- package/dist/status/compiledContainerObservations.d.ts +5 -0
- package/dist/status/compiledContainerObservations.js +63 -0
- package/dist/status/deploymentLogs.d.ts +12 -0
- package/dist/status/deploymentLogs.js +45 -0
- package/dist/status/deployments.d.ts +14 -0
- package/dist/status/deployments.js +128 -0
- package/dist/status/imageStatusReport.d.ts +10 -0
- package/dist/status/imageStatusReport.js +73 -0
- package/dist/status/index.d.ts +14 -0
- package/dist/status/index.js +14 -0
- package/dist/status/moltnetProbes.d.ts +21 -0
- package/dist/status/moltnetProbes.js +240 -0
- package/dist/status/registryDrift.d.ts +28 -0
- package/dist/status/registryDrift.js +119 -0
- package/dist/status/renderStatus.d.ts +2 -0
- package/dist/status/renderStatus.js +230 -0
- package/dist/status/runtimeProbes.d.ts +13 -0
- package/dist/status/runtimeProbes.js +111 -0
- package/dist/status/selectionSubjects.d.ts +8 -0
- package/dist/status/selectionSubjects.js +77 -0
- package/dist/status/selectors.d.ts +14 -0
- package/dist/status/selectors.js +90 -0
- package/dist/status/traversal.d.ts +6 -0
- package/dist/status/traversal.js +15 -0
- package/dist/status/types.d.ts +113 -0
- package/dist/status/types.js +1 -0
- package/package.json +4 -2
- package/runtimes.yaml +22 -15
- package/dist/runtime/tinyclaw/adapter.d.ts +0 -2
- package/dist/runtime/tinyclaw/adapter.js +0 -284
- package/dist/runtime/tinyclaw/runAuth.d.ts +0 -2
- package/dist/runtime/tinyclaw/runAuth.js +0 -49
- package/dist/runtime/tinyclaw/scaffold-assets/AGENTS.md +0 -160
- package/dist/runtime/tinyclaw/scaffold-assets/CLAUDE.md +0 -5
- package/dist/runtime/tinyclaw/scaffold-assets/SOUL.md +0 -177
- package/dist/runtime/tinyclaw/scaffold.d.ts +0 -2
- package/dist/runtime/tinyclaw/scaffold.js +0 -24
- package/dist/runtime/tinyclaw/schedules.d.ts +0 -9
- package/dist/runtime/tinyclaw/schedules.js +0 -62
- package/dist/runtime/tinyclaw/surfaces.d.ts +0 -8
- package/dist/runtime/tinyclaw/surfaces.js +0 -94
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { acquireHomeDeploymentLock, createDockerDeploymentLabels, homeDeploymentExists, normalizeDeploymentName, readHomeDeploymentRecord, resolveDockerDeploymentTarget, verifyDockerDeploymentTarget, writeHomeDeployment } from "../deployment/index.js";
|
|
5
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
6
|
+
import { deriveDeploymentName, deriveVolumeName, renderEnvFileContent, resolveImageEnvironment } from "./consumeImageSupport.js";
|
|
7
|
+
import { createConsumerDockerRunner } from "./dockerRunner.js";
|
|
8
|
+
import { extractImageReport, resolveDockerBaseArgs } from "./extractImage.js";
|
|
9
|
+
import { parseImageReference } from "./imageRef.js";
|
|
10
|
+
import { prepareImageRuntimeAuthMounts } from "./imageRuntimeAuth.js";
|
|
11
|
+
import { runImagePreflight } from "./preflight.js";
|
|
12
|
+
import { normalizeProjectLabelSlug } from "./projectName.js";
|
|
13
|
+
const containerNameFor = (deploymentName) => `spawnfile-${deploymentName}`;
|
|
14
|
+
const unitIdFor = (deploymentName) => `${deploymentName}-container`;
|
|
15
|
+
const resolveRegistryDigest = async (imageRef, runDocker) => {
|
|
16
|
+
const fromRef = imageRef.includes("@") ? imageRef.slice(imageRef.indexOf("@") + 1) : null;
|
|
17
|
+
if (fromRef) {
|
|
18
|
+
return fromRef;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const raw = (await runDocker([
|
|
22
|
+
"image",
|
|
23
|
+
"inspect",
|
|
24
|
+
"--format",
|
|
25
|
+
"{{json .RepoDigests}}",
|
|
26
|
+
imageRef
|
|
27
|
+
])).toString("utf8").trim();
|
|
28
|
+
const digests = JSON.parse(raw);
|
|
29
|
+
const match = digests
|
|
30
|
+
.map((entry) => (entry.includes("@") ? entry.slice(entry.indexOf("@") + 1) : null))
|
|
31
|
+
.find((entry) => Boolean(entry));
|
|
32
|
+
return match ?? null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const containerExists = async (runDocker, containerName) => {
|
|
39
|
+
try {
|
|
40
|
+
await runDocker(["container", "inspect", containerName]);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const resolveLocalImageId = async (imageRef, runDocker) => {
|
|
48
|
+
try {
|
|
49
|
+
const raw = (await runDocker([
|
|
50
|
+
"image",
|
|
51
|
+
"inspect",
|
|
52
|
+
"--format",
|
|
53
|
+
"{{.Id}}",
|
|
54
|
+
imageRef
|
|
55
|
+
])).toString("utf8").trim();
|
|
56
|
+
return raw.length > 0 ? raw : null;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const networkIdsFrom = (report) => report.moltnet.networks.map((network) => network.id).sort();
|
|
63
|
+
const buildContainsEntries = (report) => [
|
|
64
|
+
...report.organization.agents.map((agent) => ({ id: agent.id, kind: "agent" })),
|
|
65
|
+
...report.organization.teams.map((team) => ({ id: team.id, kind: "team" })),
|
|
66
|
+
...networkIdsFrom(report).map((id) => ({ id, kind: "network" }))
|
|
67
|
+
];
|
|
68
|
+
export const consumeImageUp = async (imageRef, options = {}) => {
|
|
69
|
+
const parsedRef = parseImageReference(imageRef);
|
|
70
|
+
if (!parsedRef) {
|
|
71
|
+
throw new SpawnfileError("validation_error", `Invalid image reference: ${imageRef}`);
|
|
72
|
+
}
|
|
73
|
+
const deploymentName = normalizeDeploymentName(options.deploymentName ?? deriveDeploymentName(parsedRef));
|
|
74
|
+
const isExplicitName = Boolean(options.deploymentName);
|
|
75
|
+
// Hold an exclusive lock for the whole operation so a concurrent `up` for the
|
|
76
|
+
// same deployment cannot race on the record or orphan a container.
|
|
77
|
+
const releaseLock = await acquireHomeDeploymentLock(deploymentName);
|
|
78
|
+
try {
|
|
79
|
+
return await consumeImageUpLocked(imageRef, parsedRef, deploymentName, isExplicitName, options);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await releaseLock();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const consumeImageUpLocked = async (imageRef, parsedRef, deploymentName, isExplicitName, options) => {
|
|
86
|
+
void parsedRef;
|
|
87
|
+
const alreadyExists = await homeDeploymentExists(deploymentName);
|
|
88
|
+
if (alreadyExists && !isExplicitName) {
|
|
89
|
+
throw new SpawnfileError("validation_error", `Deployment "${deploymentName}" already exists (derived from image ${imageRef}). ` +
|
|
90
|
+
`To redeploy it, re-run with --deployment ${deploymentName}; ` +
|
|
91
|
+
"to create a separate one, pass a different --deployment <name>.");
|
|
92
|
+
}
|
|
93
|
+
const dockerCommand = options.dockerCommand ?? "docker";
|
|
94
|
+
const baseArgs = resolveDockerBaseArgs({
|
|
95
|
+
dockerContext: options.dockerContext,
|
|
96
|
+
dockerHost: options.dockerHost
|
|
97
|
+
});
|
|
98
|
+
const inspection = await extractImageReport(imageRef, {
|
|
99
|
+
dockerCommand,
|
|
100
|
+
dockerContext: options.dockerContext,
|
|
101
|
+
dockerHost: options.dockerHost,
|
|
102
|
+
pull: options.pull,
|
|
103
|
+
runDocker: options.runDocker
|
|
104
|
+
});
|
|
105
|
+
const report = inspection.report;
|
|
106
|
+
// Resolve the full environment first (auth profile, env file, process-env
|
|
107
|
+
// overrides, and generated runtime tokens), then preflight against it so
|
|
108
|
+
// required secrets supplied via any of those sources satisfy the check.
|
|
109
|
+
const env = resolveImageEnvironment({
|
|
110
|
+
authValues: options.authValues ?? {},
|
|
111
|
+
envFileEnv: options.envFileEnv,
|
|
112
|
+
report
|
|
113
|
+
});
|
|
114
|
+
const availableImports = options.authProfile
|
|
115
|
+
? ["claude-code", "codex"].filter((kind) => options.authProfile.imports[kind])
|
|
116
|
+
: [];
|
|
117
|
+
runImagePreflight({ authValues: env, availableImports, report });
|
|
118
|
+
const target = await resolveExistingOrNewTarget(deploymentName, alreadyExists, options);
|
|
119
|
+
// Capture what this redeploy replaces so the caller can show previous → new
|
|
120
|
+
// ref/digest, per specs/DISTRIBUTION.md. Null on a first deploy.
|
|
121
|
+
const previous = alreadyExists ? await readPreviousSource(deploymentName) : null;
|
|
122
|
+
const runDocker = options.runDocker ?? createConsumerDockerRunner(dockerCommand, baseArgs);
|
|
123
|
+
const containerName = containerNameFor(deploymentName);
|
|
124
|
+
const projectSlug = normalizeProjectLabelSlug(report.organization.project);
|
|
125
|
+
const workDir = await mkdtemp(path.join(os.tmpdir(), "spawnfile-consume-"));
|
|
126
|
+
const envFilePath = path.join(workDir, "container.env");
|
|
127
|
+
// A redeploy reuses the live container's published host ports, and Docker
|
|
128
|
+
// cannot bind a host port twice. So when a live container exists we move it
|
|
129
|
+
// aside (rename + stop) to free its name and ports, start the new container,
|
|
130
|
+
// and on failure restore the old one — non-destructive without needing a free
|
|
131
|
+
// port for two containers at once. There is a brief restart gap, unavoidable
|
|
132
|
+
// when reusing host ports without a proxy.
|
|
133
|
+
const backupName = `${containerName}-previous-${Date.now().toString(36)}`;
|
|
134
|
+
try {
|
|
135
|
+
await writeFile(envFilePath, renderEnvFileContent(env), "utf8");
|
|
136
|
+
// Prepare import-based model auth (the consumer's Claude/Codex login) and
|
|
137
|
+
// validate its credentials BEFORE the destructive swap below. The OAuth-mode
|
|
138
|
+
// config is already baked into the image; this only resolves the credential
|
|
139
|
+
// mounts. Doing it first guarantees an unusable import fails while the live
|
|
140
|
+
// container is still untouched, never mid-swap.
|
|
141
|
+
const authMountArgs = options.authProfile && availableImports.length > 0
|
|
142
|
+
? (await prepareImageRuntimeAuthMounts({
|
|
143
|
+
authProfile: options.authProfile,
|
|
144
|
+
report,
|
|
145
|
+
tempRoot: workDir
|
|
146
|
+
})).mountArgs
|
|
147
|
+
: [];
|
|
148
|
+
const liveExists = await containerExists(runDocker, containerName);
|
|
149
|
+
if (liveExists) {
|
|
150
|
+
await runDocker(["rename", containerName, backupName]);
|
|
151
|
+
await runDocker(["stop", backupName]).catch(() => undefined);
|
|
152
|
+
}
|
|
153
|
+
const runArgs = ["run", "-d", "--name", containerName, "--env-file", envFilePath];
|
|
154
|
+
for (const port of report.ports) {
|
|
155
|
+
runArgs.push("-p", `${port}:${port}`);
|
|
156
|
+
}
|
|
157
|
+
for (const mount of report.persistent_mounts) {
|
|
158
|
+
runArgs.push("-v", `${deriveVolumeName(deploymentName, mount.id)}:${mount.target}`);
|
|
159
|
+
}
|
|
160
|
+
runArgs.push(...authMountArgs);
|
|
161
|
+
const labels = createDockerDeploymentLabels({
|
|
162
|
+
compileFingerprint: inspection.compileFingerprint,
|
|
163
|
+
deployment: deploymentName,
|
|
164
|
+
project: projectSlug,
|
|
165
|
+
unit: unitIdFor(deploymentName),
|
|
166
|
+
version: "0.1"
|
|
167
|
+
});
|
|
168
|
+
for (const [name, value] of Object.entries(labels)) {
|
|
169
|
+
runArgs.push("--label", `${name}=${value}`);
|
|
170
|
+
}
|
|
171
|
+
runArgs.push(imageRef);
|
|
172
|
+
let runOutput;
|
|
173
|
+
try {
|
|
174
|
+
runOutput = (await runDocker(runArgs)).toString("utf8").trim();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
// The new container failed to start; remove the failed attempt and restore
|
|
178
|
+
// the previous deployment so a failed redeploy never loses the live one.
|
|
179
|
+
await runDocker(["rm", "-f", containerName]).catch(() => undefined);
|
|
180
|
+
if (liveExists) {
|
|
181
|
+
await runDocker(["rename", backupName, containerName]).catch(() => undefined);
|
|
182
|
+
await runDocker(["start", containerName]).catch(() => undefined);
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
// The new container is up — discard the previous one.
|
|
187
|
+
if (liveExists) {
|
|
188
|
+
await runDocker(["rm", "-f", backupName]).catch(() => undefined);
|
|
189
|
+
}
|
|
190
|
+
const containerId = runOutput.split("\n").pop()?.trim() || null;
|
|
191
|
+
const imageId = await resolveLocalImageId(imageRef, runDocker);
|
|
192
|
+
const digest = await resolveRegistryDigest(imageRef, runDocker);
|
|
193
|
+
const record = {
|
|
194
|
+
auth_profile: options.authProfileName ?? null,
|
|
195
|
+
compile_fingerprint: inspection.compileFingerprint,
|
|
196
|
+
created_at: new Date().toISOString(),
|
|
197
|
+
...(options.envFilePath ? { env_file: path.resolve(options.envFilePath) } : {}),
|
|
198
|
+
manager: "docker",
|
|
199
|
+
name: deploymentName,
|
|
200
|
+
output_directory: null,
|
|
201
|
+
source: { digest, kind: "image", ref: imageRef },
|
|
202
|
+
target,
|
|
203
|
+
units: [
|
|
204
|
+
{
|
|
205
|
+
container_id: containerId,
|
|
206
|
+
container_name: containerName,
|
|
207
|
+
contains: buildContainsEntries(report),
|
|
208
|
+
id: unitIdFor(deploymentName),
|
|
209
|
+
image_id: imageId,
|
|
210
|
+
image_tag: imageRef,
|
|
211
|
+
kind: "container",
|
|
212
|
+
runtime_instances: report.runtime_instances.map((instance) => instance.id).sort()
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
version: "spawnfile.deployment.v2"
|
|
216
|
+
};
|
|
217
|
+
const written = await writeHomeDeployment(record, report);
|
|
218
|
+
return {
|
|
219
|
+
containerName,
|
|
220
|
+
deploymentName,
|
|
221
|
+
imageRef,
|
|
222
|
+
previous,
|
|
223
|
+
record,
|
|
224
|
+
recordPath: written.recordPath
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
await rm(workDir, { force: true, recursive: true }).catch(() => undefined);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const readPreviousSource = async (deploymentName) => {
|
|
232
|
+
try {
|
|
233
|
+
const existing = await readHomeDeploymentRecord(deploymentName);
|
|
234
|
+
if (existing.source.kind !== "image") {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return { digest: existing.source.digest, ref: existing.source.ref };
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const resolveExistingOrNewTarget = async (deploymentName, alreadyExists, options) => {
|
|
244
|
+
if (alreadyExists && !options.dockerContext && !options.dockerHost) {
|
|
245
|
+
const existing = await readHomeDeploymentRecord(deploymentName);
|
|
246
|
+
await verifyDockerDeploymentTarget(existing.target, { dockerCommand: options.dockerCommand });
|
|
247
|
+
return existing.target;
|
|
248
|
+
}
|
|
249
|
+
return resolveDockerDeploymentTarget({
|
|
250
|
+
context: options.dockerContext ?? null,
|
|
251
|
+
dockerCommand: options.dockerCommand,
|
|
252
|
+
dockerHost: options.dockerHost ?? null
|
|
253
|
+
});
|
|
254
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ParsedImageReference } from "./imageRef.js";
|
|
2
|
+
import type { DistributionReport } from "./types.js";
|
|
3
|
+
/** Derives a kebab-case deployment name from the image repository component. */
|
|
4
|
+
export declare const deriveDeploymentName: (ref: ParsedImageReference) => string;
|
|
5
|
+
/** Per-deployment volume name so two deployments never share a store. */
|
|
6
|
+
export declare const deriveVolumeName: (deploymentName: string, mountId: string) => string;
|
|
7
|
+
export interface ResolveImageEnvironmentInput {
|
|
8
|
+
authValues: Record<string, string>;
|
|
9
|
+
envFileEnv?: Record<string, string>;
|
|
10
|
+
report: DistributionReport;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds the container environment for a sourceless deployment: auth-profile
|
|
14
|
+
* and env-file values, process-env overrides for declared secrets, and
|
|
15
|
+
* generated runtime tokens that the user must never supply.
|
|
16
|
+
*/
|
|
17
|
+
export declare const resolveImageEnvironment: (input: ResolveImageEnvironmentInput) => Record<string, string>;
|
|
18
|
+
export declare const renderEnvFileContent: (env: Record<string, string>) => string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
const GENERATED_RUNTIME_SECRET_NAMES = new Set([
|
|
3
|
+
"OPENCLAW_GATEWAY_TOKEN",
|
|
4
|
+
"OPENCLAW_HOOKS_TOKEN"
|
|
5
|
+
]);
|
|
6
|
+
/** Derives a kebab-case deployment name from the image repository component. */
|
|
7
|
+
export const deriveDeploymentName = (ref) => {
|
|
8
|
+
const base = ref.name.split("/").pop() ?? ref.name;
|
|
9
|
+
const normalized = base
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
12
|
+
.replace(/^-+|-+$/g, "");
|
|
13
|
+
return normalized || "deployment";
|
|
14
|
+
};
|
|
15
|
+
/** Per-deployment volume name so two deployments never share a store. */
|
|
16
|
+
export const deriveVolumeName = (deploymentName, mountId) => {
|
|
17
|
+
const safeMount = mountId.replace(/[^A-Za-z0-9_.-]+/g, "-");
|
|
18
|
+
return `spawnfile_${deploymentName}_${safeMount}`;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Builds the container environment for a sourceless deployment: auth-profile
|
|
22
|
+
* and env-file values, process-env overrides for declared secrets, and
|
|
23
|
+
* generated runtime tokens that the user must never supply.
|
|
24
|
+
*/
|
|
25
|
+
export const resolveImageEnvironment = (input) => {
|
|
26
|
+
const env = {
|
|
27
|
+
...input.authValues,
|
|
28
|
+
...(input.envFileEnv ?? {})
|
|
29
|
+
};
|
|
30
|
+
const declaredSecrets = new Set();
|
|
31
|
+
for (const category of ["model", "project", "runtime", "surface"]) {
|
|
32
|
+
for (const entry of input.report.secrets[category]) {
|
|
33
|
+
declaredSecrets.add(entry.name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const name of new Set([...Object.keys(env), ...declaredSecrets])) {
|
|
37
|
+
const processValue = process.env[name];
|
|
38
|
+
if (typeof processValue === "string" && processValue.length > 0) {
|
|
39
|
+
env[name] = processValue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const category of ["runtime"]) {
|
|
43
|
+
for (const entry of input.report.secrets[category]) {
|
|
44
|
+
if (!entry.generated) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const hasValue = typeof env[entry.name] === "string" && env[entry.name].length > 0;
|
|
48
|
+
if (!hasValue && GENERATED_RUNTIME_SECRET_NAMES.has(entry.name)) {
|
|
49
|
+
env[entry.name] = randomBytes(24).toString("hex");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const [name, value] of Object.entries(env)) {
|
|
54
|
+
if (value.includes("\n")) {
|
|
55
|
+
delete env[name];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return env;
|
|
59
|
+
};
|
|
60
|
+
export const renderEnvFileContent = (env) => Object.entries(env)
|
|
61
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
62
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
63
|
+
.join("\n") + "\n";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { DistributionReport } from "./types.js";
|
|
3
|
+
export declare const distributionReportSchema: z.ZodObject<{
|
|
4
|
+
compile_fingerprint: z.ZodString;
|
|
5
|
+
generated_at: z.ZodString;
|
|
6
|
+
internal_ports: z.ZodArray<z.ZodNumber>;
|
|
7
|
+
model_auth_methods: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
8
|
+
moltnet: z.ZodObject<{
|
|
9
|
+
networks: z.ZodArray<z.ZodObject<{
|
|
10
|
+
binding: z.ZodOptional<z.ZodLiteral<"env">>;
|
|
11
|
+
id: z.ZodString;
|
|
12
|
+
server_mode: z.ZodUnion<readonly [z.ZodLiteral<"external">, z.ZodLiteral<"managed">]>;
|
|
13
|
+
}, z.core.$strict>>;
|
|
14
|
+
}, z.core.$strict>;
|
|
15
|
+
organization: z.ZodObject<{
|
|
16
|
+
agents: z.ZodArray<z.ZodObject<{
|
|
17
|
+
id: z.ZodString;
|
|
18
|
+
name: z.ZodString;
|
|
19
|
+
runtime: z.ZodNullable<z.ZodString>;
|
|
20
|
+
teams: z.ZodArray<z.ZodString>;
|
|
21
|
+
}, z.core.$strict>>;
|
|
22
|
+
project: z.ZodString;
|
|
23
|
+
teams: z.ZodArray<z.ZodObject<{
|
|
24
|
+
agents: z.ZodArray<z.ZodString>;
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
name: z.ZodString;
|
|
27
|
+
}, z.core.$strict>>;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
persistent_mounts: z.ZodArray<z.ZodObject<{
|
|
30
|
+
durability: z.ZodLiteral<"persistent">;
|
|
31
|
+
id: z.ZodString;
|
|
32
|
+
kind: z.ZodLiteral<"volume">;
|
|
33
|
+
target: z.ZodString;
|
|
34
|
+
}, z.core.$strict>>;
|
|
35
|
+
port_mappings: z.ZodArray<z.ZodObject<{
|
|
36
|
+
internal_port: z.ZodNumber;
|
|
37
|
+
published_port: z.ZodNumber;
|
|
38
|
+
}, z.core.$strict>>;
|
|
39
|
+
ports: z.ZodArray<z.ZodNumber>;
|
|
40
|
+
resources: z.ZodArray<z.ZodObject<{
|
|
41
|
+
id: z.ZodString;
|
|
42
|
+
kind: z.ZodUnion<readonly [z.ZodLiteral<"git">, z.ZodLiteral<"volume">]>;
|
|
43
|
+
link_path: z.ZodString;
|
|
44
|
+
mode: z.ZodUnion<readonly [z.ZodLiteral<"mutable">, z.ZodLiteral<"readonly">]>;
|
|
45
|
+
mount: z.ZodString;
|
|
46
|
+
sharing: z.ZodUnion<readonly [z.ZodLiteral<"per_agent">, z.ZodLiteral<"team">]>;
|
|
47
|
+
}, z.core.$strict>>;
|
|
48
|
+
runtime_instances: z.ZodArray<z.ZodObject<{
|
|
49
|
+
config_path: z.ZodString;
|
|
50
|
+
home_path: z.ZodNullable<z.ZodString>;
|
|
51
|
+
id: z.ZodString;
|
|
52
|
+
internal_port: z.ZodNullable<z.ZodNumber>;
|
|
53
|
+
model_auth_methods: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
54
|
+
model_secrets_required: z.ZodArray<z.ZodString>;
|
|
55
|
+
node_ids: z.ZodArray<z.ZodString>;
|
|
56
|
+
published_port: z.ZodNullable<z.ZodNumber>;
|
|
57
|
+
runtime: z.ZodString;
|
|
58
|
+
workspace_path: z.ZodString;
|
|
59
|
+
}, z.core.$strict>>;
|
|
60
|
+
secrets: z.ZodObject<{
|
|
61
|
+
model: z.ZodArray<z.ZodObject<{
|
|
62
|
+
generated: z.ZodBoolean;
|
|
63
|
+
name: z.ZodString;
|
|
64
|
+
required: z.ZodBoolean;
|
|
65
|
+
}, z.core.$strict>>;
|
|
66
|
+
project: z.ZodArray<z.ZodObject<{
|
|
67
|
+
generated: z.ZodBoolean;
|
|
68
|
+
name: z.ZodString;
|
|
69
|
+
required: z.ZodBoolean;
|
|
70
|
+
}, z.core.$strict>>;
|
|
71
|
+
runtime: z.ZodArray<z.ZodObject<{
|
|
72
|
+
generated: z.ZodBoolean;
|
|
73
|
+
name: z.ZodString;
|
|
74
|
+
required: z.ZodBoolean;
|
|
75
|
+
}, z.core.$strict>>;
|
|
76
|
+
surface: z.ZodArray<z.ZodObject<{
|
|
77
|
+
generated: z.ZodBoolean;
|
|
78
|
+
name: z.ZodString;
|
|
79
|
+
required: z.ZodBoolean;
|
|
80
|
+
}, z.core.$strict>>;
|
|
81
|
+
}, z.core.$strict>;
|
|
82
|
+
version: z.ZodLiteral<"spawnfile.distribution-report.v1">;
|
|
83
|
+
}, z.core.$strict>;
|
|
84
|
+
export declare const parseDistributionReport: (source: unknown, sourceLabel?: string) => DistributionReport;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
3
|
+
import { DISTRIBUTION_REPORT_VERSION } from "./types.js";
|
|
4
|
+
const secretEntrySchema = z.object({
|
|
5
|
+
generated: z.boolean(),
|
|
6
|
+
name: z.string().min(1),
|
|
7
|
+
required: z.boolean()
|
|
8
|
+
}).strict();
|
|
9
|
+
// Path fields from an untrusted image flow into `docker run -v` mount specs. A
|
|
10
|
+
// crafted value containing `:` would inject extra mount fields (e.g. drop `:ro`
|
|
11
|
+
// or add options), and `..` could redirect the mount target. Constrain every
|
|
12
|
+
// path the consumer interpolates to a clean absolute POSIX path so a hostile
|
|
13
|
+
// report is rejected up front; a legitimately compiled image always satisfies this.
|
|
14
|
+
const containerPathSchema = z
|
|
15
|
+
.string()
|
|
16
|
+
.min(1)
|
|
17
|
+
.refine((value) => value.startsWith("/") &&
|
|
18
|
+
!value.includes(":") &&
|
|
19
|
+
!/\s/.test(value) &&
|
|
20
|
+
!value.split("/").includes(".."), { message: "must be a clean absolute POSIX path (no ':', whitespace, or '..' segment)" });
|
|
21
|
+
// Ports are interpolated into `-p host:container`; bound them to the valid TCP range.
|
|
22
|
+
const portSchema = z.number().int().min(1).max(65535);
|
|
23
|
+
// A runtime instance id is interpolated into a host path (the auth-profiles temp
|
|
24
|
+
// file) and then into a `docker run -v` source by the runtime adapters, so an
|
|
25
|
+
// unconstrained value could traverse with `..` or inject mount fields with `:`.
|
|
26
|
+
// Legitimate ids are compile-generated `<kind>-<slug>` identifiers.
|
|
27
|
+
const instanceIdSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.min(1)
|
|
30
|
+
.refine((value) => /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value) && !value.includes(".."), {
|
|
31
|
+
message: "must be an identifier (letters, digits, '_', '.', '-'; no '/', ':', whitespace, or '..')"
|
|
32
|
+
});
|
|
33
|
+
const runtimeInstanceSchema = z.object({
|
|
34
|
+
config_path: containerPathSchema,
|
|
35
|
+
home_path: containerPathSchema.nullable(),
|
|
36
|
+
id: instanceIdSchema,
|
|
37
|
+
internal_port: portSchema.nullable(),
|
|
38
|
+
model_auth_methods: z.record(z.string(), z.string()),
|
|
39
|
+
model_secrets_required: z.array(z.string()),
|
|
40
|
+
node_ids: z.array(z.string()),
|
|
41
|
+
published_port: portSchema.nullable(),
|
|
42
|
+
runtime: z.string().min(1),
|
|
43
|
+
workspace_path: containerPathSchema
|
|
44
|
+
}).strict();
|
|
45
|
+
export const distributionReportSchema = z.object({
|
|
46
|
+
compile_fingerprint: z.string().min(1),
|
|
47
|
+
generated_at: z.string().min(1),
|
|
48
|
+
internal_ports: z.array(portSchema),
|
|
49
|
+
model_auth_methods: z.record(z.string(), z.string()),
|
|
50
|
+
moltnet: z.object({
|
|
51
|
+
networks: z.array(z.object({
|
|
52
|
+
binding: z.literal("env").optional(),
|
|
53
|
+
id: z.string().min(1),
|
|
54
|
+
server_mode: z.union([z.literal("external"), z.literal("managed")])
|
|
55
|
+
}).strict())
|
|
56
|
+
}).strict(),
|
|
57
|
+
organization: z.object({
|
|
58
|
+
agents: z.array(z.object({
|
|
59
|
+
id: z.string().min(1),
|
|
60
|
+
name: z.string(),
|
|
61
|
+
runtime: z.string().nullable(),
|
|
62
|
+
teams: z.array(z.string())
|
|
63
|
+
}).strict()),
|
|
64
|
+
project: z.string().min(1),
|
|
65
|
+
teams: z.array(z.object({
|
|
66
|
+
agents: z.array(z.string()),
|
|
67
|
+
id: z.string().min(1),
|
|
68
|
+
name: z.string()
|
|
69
|
+
}).strict())
|
|
70
|
+
}).strict(),
|
|
71
|
+
persistent_mounts: z.array(z.object({
|
|
72
|
+
durability: z.literal("persistent"),
|
|
73
|
+
// Sanitized by deriveVolumeName before use, but constrained here too so the
|
|
74
|
+
// schema alone keeps the volume-name source clean (defense in depth).
|
|
75
|
+
id: instanceIdSchema,
|
|
76
|
+
kind: z.literal("volume"),
|
|
77
|
+
target: containerPathSchema
|
|
78
|
+
}).strict()),
|
|
79
|
+
port_mappings: z.array(z.object({
|
|
80
|
+
internal_port: portSchema,
|
|
81
|
+
published_port: portSchema
|
|
82
|
+
}).strict()),
|
|
83
|
+
ports: z.array(portSchema),
|
|
84
|
+
resources: z.array(z.object({
|
|
85
|
+
id: z.string().min(1),
|
|
86
|
+
kind: z.union([z.literal("git"), z.literal("volume")]),
|
|
87
|
+
link_path: z.string(),
|
|
88
|
+
mode: z.union([z.literal("mutable"), z.literal("readonly")]),
|
|
89
|
+
mount: z.string(),
|
|
90
|
+
sharing: z.union([z.literal("per_agent"), z.literal("team")])
|
|
91
|
+
}).strict()),
|
|
92
|
+
runtime_instances: z.array(runtimeInstanceSchema),
|
|
93
|
+
secrets: z.object({
|
|
94
|
+
model: z.array(secretEntrySchema),
|
|
95
|
+
project: z.array(secretEntrySchema),
|
|
96
|
+
runtime: z.array(secretEntrySchema),
|
|
97
|
+
surface: z.array(secretEntrySchema)
|
|
98
|
+
}).strict(),
|
|
99
|
+
version: z.literal(DISTRIBUTION_REPORT_VERSION)
|
|
100
|
+
}).strict();
|
|
101
|
+
export const parseDistributionReport = (source, sourceLabel = "distribution report") => {
|
|
102
|
+
const parsed = distributionReportSchema.safeParse(source);
|
|
103
|
+
if (!parsed.success) {
|
|
104
|
+
throw new SpawnfileError("validation_error", `Invalid ${sourceLabel}: ${z.prettifyError(parsed.error)}`);
|
|
105
|
+
}
|
|
106
|
+
return parsed.data;
|
|
107
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface DockerCommandRunner {
|
|
2
|
+
/** Runs docker with args; resolves stdout as a Buffer. Rejects on non-zero exit. */
|
|
3
|
+
(args: string[]): Promise<Buffer>;
|
|
4
|
+
}
|
|
5
|
+
export declare const createConsumerDockerRunner: (dockerCommand: string, baseArgs: string[]) => DockerCommandRunner;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
3
|
+
export const createConsumerDockerRunner = (dockerCommand, baseArgs) => async (args) => new Promise((resolve, reject) => {
|
|
4
|
+
const finalArgs = [...baseArgs, ...args];
|
|
5
|
+
const child = spawn(dockerCommand, finalArgs, {
|
|
6
|
+
env: { ...process.env },
|
|
7
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8
|
+
});
|
|
9
|
+
const stdout = [];
|
|
10
|
+
const stderr = [];
|
|
11
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
12
|
+
child.stderr.on("data", (chunk) => stderr.push(String(chunk)));
|
|
13
|
+
child.on("error", reject);
|
|
14
|
+
child.on("close", (code) => {
|
|
15
|
+
if (code === 0) {
|
|
16
|
+
resolve(Buffer.concat(stdout));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
reject(new SpawnfileError("runtime_error", `docker ${finalArgs[0]} failed (${code}): ${stderr.join("").trim()}`));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DockerCommandRunner } from "./dockerRunner.js";
|
|
2
|
+
import type { DistributionReport } from "./types.js";
|
|
3
|
+
export type { DockerCommandRunner } from "./dockerRunner.js";
|
|
4
|
+
export interface ImageInspection {
|
|
5
|
+
compileFingerprint: string;
|
|
6
|
+
labels: Record<string, string>;
|
|
7
|
+
report: DistributionReport;
|
|
8
|
+
}
|
|
9
|
+
export interface ExtractImageOptions {
|
|
10
|
+
dockerCommand?: string;
|
|
11
|
+
dockerContext?: string;
|
|
12
|
+
dockerHost?: string;
|
|
13
|
+
pull?: boolean;
|
|
14
|
+
runDocker?: DockerCommandRunner;
|
|
15
|
+
}
|
|
16
|
+
export declare const resolveDockerBaseArgs: (options: {
|
|
17
|
+
dockerContext?: string;
|
|
18
|
+
dockerHost?: string;
|
|
19
|
+
}) => string[];
|
|
20
|
+
/**
|
|
21
|
+
* Pulls (optionally), inspects labels, and extracts the embedded distribution
|
|
22
|
+
* report from an image without starting its entrypoint. Uses a stopped helper
|
|
23
|
+
* container and guarantees cleanup.
|
|
24
|
+
*/
|
|
25
|
+
export declare const extractImageReport: (imageRef: string, options?: ExtractImageOptions) => Promise<ImageInspection>;
|