spawnfile 0.1.6 → 0.1.8
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 +1 -0
- package/dist/cli/runCli.js +7 -1
- package/dist/compiler/containerArtifacts.js +11 -1
- package/dist/compiler/containerArtifactsRender.js +20 -2
- package/dist/compiler/containerEntrypointRender.d.ts +1 -0
- package/dist/compiler/containerEntrypointRender.js +13 -1
- package/dist/compiler/moltnetArtifacts.d.ts +7 -0
- package/dist/compiler/moltnetArtifacts.js +50 -3
- package/dist/compiler/moltnetConfigLowering.d.ts +11 -2
- package/dist/compiler/moltnetConfigLowering.js +35 -7
- package/dist/compiler/runProject.js +3 -0
- package/dist/manifest/renderSpawnfileNetworks.js +18 -1
- package/dist/manifest/schemas.d.ts +36 -4
- package/dist/manifest/teamNetworkSchemas.d.ts +54 -6
- package/dist/manifest/teamNetworkSchemas.js +44 -8
- package/dist/report/types.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli/runCli.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import { importClaudeCodeAuth, importCodexAuth, importEnvFile, requireAuthProfile } from "../auth/index.js";
|
|
3
4
|
import { addAgentProject, addProjectSurface, addProjectModelFallback, addSubagentProject, addTeamProject, buildOrganizationView, buildCompilePlan, buildProject, clearProjectModelFallbacks, compileProject, initProject, upProject, removeProjectSurface, runProject, setProjectPrimaryModel, setProjectRuntime, setProjectSurfaceAccess, showProjectSurfaces, syncProjectAuth } from "../compiler/index.js";
|
|
@@ -8,6 +9,11 @@ import { registerModelCommands } from "./modelCommands.js";
|
|
|
8
9
|
import { registerRuntimeCommands } from "./runtimeCommands.js";
|
|
9
10
|
import { registerSurfaceCommands } from "./surfaceCommands.js";
|
|
10
11
|
import { registerViewCommand } from "./viewCommand.js";
|
|
12
|
+
const packageJsonPath = new URL("../../package.json", import.meta.url);
|
|
13
|
+
const readPackageVersion = () => {
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
15
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
16
|
+
};
|
|
11
17
|
const createDefaultStreams = () => ({
|
|
12
18
|
stderr: (message) => process.stderr.write(`${message}\n`),
|
|
13
19
|
stdout: (message) => process.stdout.write(`${message}\n`)
|
|
@@ -77,7 +83,7 @@ export const runCli = async (argv, optionsOrStreams, handlerOverrides = {}) => {
|
|
|
77
83
|
const streams = cliOptions.streams;
|
|
78
84
|
const handlers = { ...createDefaultHandlers(), ...cliOptions.handlers };
|
|
79
85
|
const program = new Command();
|
|
80
|
-
program.name("spawnfile").description("Spawnfile v0.1 compiler");
|
|
86
|
+
program.name("spawnfile").description("Spawnfile v0.1 compiler").version(readPackageVersion());
|
|
81
87
|
program.exitOverride();
|
|
82
88
|
program.configureOutput({
|
|
83
89
|
outputError: (message, write) => write(message),
|
|
@@ -15,6 +15,14 @@ export const createContainerArtifacts = async (plan, compiledNodes, options = {}
|
|
|
15
15
|
.filter((variable) => variable.required && variable.categories.includes("runtime"))
|
|
16
16
|
.map((variable) => variable.name)
|
|
17
17
|
.sort();
|
|
18
|
+
const persistentMounts = (options.moltnet?.persistentMounts ?? [])
|
|
19
|
+
.map((mount) => ({
|
|
20
|
+
id: mount.id,
|
|
21
|
+
mount_path: mount.mountPath,
|
|
22
|
+
reason: mount.reason,
|
|
23
|
+
volume_name: mount.volumeName
|
|
24
|
+
}))
|
|
25
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
18
26
|
const files = [
|
|
19
27
|
...createRootfsFiles(runtimePlans),
|
|
20
28
|
...(options.moltnet?.files ?? []),
|
|
@@ -22,7 +30,8 @@ export const createContainerArtifacts = async (plan, compiledNodes, options = {}
|
|
|
22
30
|
content: await renderDockerfile(runtimePlans, {
|
|
23
31
|
hasMoltnet: Boolean(options.moltnet),
|
|
24
32
|
hasStagedMoltnetBinaries: options.hasStagedMoltnetBinaries,
|
|
25
|
-
moltnetPublishedPorts: options.moltnet?.publishedPorts ?? []
|
|
33
|
+
moltnetPublishedPorts: options.moltnet?.publishedPorts ?? [],
|
|
34
|
+
persistentMountPaths: persistentMounts.map((mount) => mount.mount_path)
|
|
26
35
|
}),
|
|
27
36
|
path: "Dockerfile"
|
|
28
37
|
},
|
|
@@ -95,6 +104,7 @@ export const createContainerArtifacts = async (plan, compiledNodes, options = {}
|
|
|
95
104
|
runtime_secrets_required: runtimeSecretsRequired,
|
|
96
105
|
runtimes_installed: runtimesInstalled,
|
|
97
106
|
secrets_required: requiredSecrets,
|
|
107
|
+
...(persistentMounts.length > 0 ? { persistent_mounts: persistentMounts } : {}),
|
|
98
108
|
...(workspaceResources.length > 0 ? { workspace_resources: workspaceResources } : {})
|
|
99
109
|
}
|
|
100
110
|
};
|
|
@@ -6,12 +6,30 @@ import { MOLTNET_BIN_DIRECTORY, MOLTNET_BINARY_NAMES } from "./moltnetBinaries.j
|
|
|
6
6
|
const CONTAINER_ROOTFS_ROOT = "container/rootfs";
|
|
7
7
|
const GATEWAY_PORT_PLACEHOLDER = "<gateway-port>";
|
|
8
8
|
const MOLTNET_INSTALL_SCRIPT_URL = "https://moltnet.dev/install.sh";
|
|
9
|
+
const MOLTNET_RELEASE_METADATA_URL = "https://api.github.com/repos/noopolis/moltnet/releases/latest";
|
|
9
10
|
const WORKSPACE_PLACEHOLDER = "<workspace-path>";
|
|
10
11
|
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
11
12
|
const extractNodeMajorVersion = (image) => Number(image.match(/^node:(\d+)/)?.[1] ?? "0");
|
|
12
13
|
const createPackageInstallCommand = (packages) => `RUN apt-get update && apt-get install -y --no-install-recommends ${packages.join(" ")} && rm -rf /var/lib/apt/lists/*`;
|
|
13
14
|
const createNpmPackageInstallCommand = (packages) => `RUN npm install -g --omit=dev --no-fund --no-audit ${packages.join(" ")}`;
|
|
14
15
|
const createPipxPackageInstallCommand = (packages) => `RUN PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install ${packages.join(" ")}`;
|
|
16
|
+
const createStateOwnershipCommand = (persistentMountPaths = []) => {
|
|
17
|
+
const mountPaths = [...new Set(persistentMountPaths)].sort();
|
|
18
|
+
const mkdirPaths = [...new Set(["/var/lib/spawnfile", ...mountPaths])].sort();
|
|
19
|
+
const markerCommands = mountPaths.map((mountPath) => `touch ${shellQuote(path.posix.join(mountPath, ".spawnfile-volume-init"))}`);
|
|
20
|
+
const chownPaths = [
|
|
21
|
+
...new Set([
|
|
22
|
+
"/var/lib/spawnfile",
|
|
23
|
+
"/opt/spawnfile",
|
|
24
|
+
...mountPaths.filter((mountPath) => !mountPath.startsWith("/var/lib/spawnfile/"))
|
|
25
|
+
])
|
|
26
|
+
].sort();
|
|
27
|
+
return [
|
|
28
|
+
`mkdir -p ${mkdirPaths.map(shellQuote).join(" ")}`,
|
|
29
|
+
...markerCommands,
|
|
30
|
+
`chown -R spawnfile:spawnfile ${chownPaths.map(shellQuote).join(" ")}`
|
|
31
|
+
].join(" && ");
|
|
32
|
+
};
|
|
15
33
|
const dedupePackages = (packages) => {
|
|
16
34
|
const seen = new Map();
|
|
17
35
|
for (const currentPackage of packages) {
|
|
@@ -163,7 +181,7 @@ export const renderDockerfile = async (runtimePlans, options = {}) => {
|
|
|
163
181
|
lines.push(`COPY ${MOLTNET_BIN_DIRECTORY}/ /usr/local/bin/`, `RUN chmod +x ${MOLTNET_BINARY_NAMES.map((binaryName) => `/usr/local/bin/${binaryName}`).join(" ")}`, "");
|
|
164
182
|
}
|
|
165
183
|
else if (options.hasMoltnet) {
|
|
166
|
-
lines.push(`RUN MOLTNET_INSTALL_DIR=/usr/local/bin sh -c ${shellQuote(`curl -fsSL ${MOLTNET_INSTALL_SCRIPT_URL} | sh`)}`, "");
|
|
184
|
+
lines.push(`ADD ${MOLTNET_RELEASE_METADATA_URL} /tmp/spawnfile-moltnet-release.json`, `RUN MOLTNET_RELEASE="$${"("}sed -n 's/.*\\"tag_name\\": *\\"\\([^\\"]*\\)\\".*/\\1/p' /tmp/spawnfile-moltnet-release.json | head -n 1${")"}" && echo "Installing Moltnet $${"{MOLTNET_RELEASE:-latest}"}" && MOLTNET_INSTALL_DIR=/usr/local/bin sh -c ${shellQuote(`curl -fsSL ${MOLTNET_INSTALL_SCRIPT_URL} | sh`)}`, "");
|
|
167
185
|
}
|
|
168
186
|
for (const recipe of runtimeRecipes) {
|
|
169
187
|
for (const copyCommand of recipe.copyCommands) {
|
|
@@ -176,7 +194,7 @@ export const renderDockerfile = async (runtimePlans, options = {}) => {
|
|
|
176
194
|
}
|
|
177
195
|
lines.push('RUN if ! id -u spawnfile >/dev/null 2>&1; then useradd --create-home --home-dir /home/spawnfile --shell /bin/bash spawnfile; fi', "");
|
|
178
196
|
lines.push("COPY container/rootfs/ /", "COPY .env.example /opt/spawnfile/.env.example", 'COPY entrypoint.sh /opt/spawnfile/entrypoint.sh', "RUN chmod +x /opt/spawnfile/entrypoint.sh");
|
|
179
|
-
lines.push(
|
|
197
|
+
lines.push(`RUN ${createStateOwnershipCommand(options.persistentMountPaths)}`);
|
|
180
198
|
if (exposedPorts.length > 0) {
|
|
181
199
|
lines.push(`EXPOSE ${exposedPorts.join(" ")}`);
|
|
182
200
|
}
|
|
@@ -8,5 +8,6 @@ export interface EntrypointOptions {
|
|
|
8
8
|
serverPlans: MoltnetArtifacts["serverPlans"];
|
|
9
9
|
};
|
|
10
10
|
moltnetPublishedPorts?: number[];
|
|
11
|
+
persistentMountPaths?: string[];
|
|
11
12
|
}
|
|
12
13
|
export declare const renderEntrypoint: (runtimePlans: RuntimeTargetPlan[], requiredSecrets: string[], options?: EntrypointOptions) => string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { resolveMoltnetStorePath } from "./moltnetConfigLowering.js";
|
|
2
3
|
import { createWorkspaceResourceCommands, createWorkspaceResourceShellFunctions } from "./containerWorkspaceResourceRender.js";
|
|
3
4
|
const MOLTNET_SERVER_DATA_DIRECTORY = "/var/lib/spawnfile/moltnet/servers";
|
|
4
5
|
const shellQuote = (value) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
@@ -36,6 +37,17 @@ const createAuthSetupCommands = (plan) => {
|
|
|
36
37
|
}
|
|
37
38
|
return [`configure_codex_api_key_auth ${shellQuote(plan.instancePaths.homePath)}`];
|
|
38
39
|
};
|
|
40
|
+
const createMoltnetStorePrepareCommands = (serverPlan) => {
|
|
41
|
+
const server = serverPlan.server;
|
|
42
|
+
if (server.mode !== "managed" || server.store.kind === "memory" || server.store.kind === "postgres") {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const storePath = resolveMoltnetStorePath(serverPlan.networkId, server.store);
|
|
46
|
+
if (!storePath) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return [`mkdir -p ${shellQuote(path.posix.dirname(storePath))}`];
|
|
50
|
+
};
|
|
39
51
|
const resolveStartCommand = (plan) => plan.meta.startCommand
|
|
40
52
|
.map((token) => token
|
|
41
53
|
.replaceAll("<runtime-root>", plan.runtimeRoot)
|
|
@@ -174,7 +186,7 @@ export const renderEntrypoint = (runtimePlans, requiredSecrets, options = {}) =>
|
|
|
174
186
|
for (const patch of serverPlan.secretPatches) {
|
|
175
187
|
lines.push(`apply_json_env_value ${shellQuote(serverPlan.configPath)} ${shellQuote(patch.envName)} ${shellQuote(patch.jsonPath)}`);
|
|
176
188
|
}
|
|
177
|
-
lines.push(`MOLTNET_CONFIG=${shellQuote(serverPlan.configPath)} /usr/local/bin/moltnet &`, 'PIDS+=("$!")', "");
|
|
189
|
+
lines.push(...createMoltnetStorePrepareCommands(serverPlan), `MOLTNET_CONFIG=${shellQuote(serverPlan.configPath)} /usr/local/bin/moltnet &`, 'PIDS+=("$!")', "");
|
|
178
190
|
}
|
|
179
191
|
for (const serverPlan of managedMoltnetServerPlans) {
|
|
180
192
|
if (!serverPlan.port) {
|
|
@@ -23,9 +23,16 @@ export interface MoltnetNodePlan {
|
|
|
23
23
|
configPath: string;
|
|
24
24
|
networkId: string;
|
|
25
25
|
}
|
|
26
|
+
export interface MoltnetPersistentMount {
|
|
27
|
+
id: string;
|
|
28
|
+
mountPath: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
volumeName: string;
|
|
31
|
+
}
|
|
26
32
|
export interface MoltnetArtifacts {
|
|
27
33
|
files: EmittedFile[];
|
|
28
34
|
nodePlans: MoltnetNodePlan[];
|
|
35
|
+
persistentMounts: MoltnetPersistentMount[];
|
|
29
36
|
ports: number[];
|
|
30
37
|
publishedPorts: number[];
|
|
31
38
|
serverPlans: MoltnetServerPlan[];
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
-
import { createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetBaseUrl, resolveMoltnetClientAuth } from "./moltnetConfigLowering.js";
|
|
2
|
+
import { createMoltnetOpenTokenDirectory, createMoltnetNativeServerConfig, createMoltnetNodeConfigPath, createMoltnetServerConfigPath, resolveMoltnetBaseUrl, resolveMoltnetClientAuth, resolveMoltnetStorePersistenceMountPath } from "./moltnetConfigLowering.js";
|
|
3
3
|
import { listConcreteMoltnetRoomMemberIds } from "./moltnetRoomMemberships.js";
|
|
4
4
|
import { resolveRuntimeConfig } from "./moltnetRuntimeConfig.js";
|
|
5
|
+
import { createShortHash, slugify } from "./helpers.js";
|
|
5
6
|
const DEFAULT_MOLTNET_PORT = 8787;
|
|
6
7
|
const ROOTFS_PREFIX = "container/rootfs";
|
|
7
8
|
const createServerKey = (networkId) => networkId;
|
|
9
|
+
const truncateSegment = (value, maxLength) => value.length > maxLength ? value.slice(0, maxLength).replace(/[-_.]+$/u, "") : value;
|
|
10
|
+
const createPersistentVolumeName = (planRoot, id, explicitName) => {
|
|
11
|
+
if (explicitName && explicitName.trim().length > 0) {
|
|
12
|
+
return explicitName.trim();
|
|
13
|
+
}
|
|
14
|
+
const project = truncateSegment(slugify(planRoot.split("/").slice(-2, -1)[0] ?? "project") || "project", 32);
|
|
15
|
+
const suffix = truncateSegment(slugify(id) || "state", 48);
|
|
16
|
+
return `spawnfile-${project}-${suffix}-${createShortHash(`${planRoot}:${id}`)}`;
|
|
17
|
+
};
|
|
8
18
|
const toContainerRootfsPath = (rootfsPath) => `/${rootfsPath.replace(`${ROOTFS_PREFIX}/`, "")}`;
|
|
9
19
|
const isNetworkHttpEnabled = (network) => network.server?.mode === "managed" && network.server.human_ingress === true;
|
|
10
20
|
const resolveNetworkPort = (network, fallbackPort) => network.server?.mode === "managed" ? network.server.listen.port : fallbackPort;
|
|
@@ -78,6 +88,17 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
78
88
|
const nodePlans = [];
|
|
79
89
|
const nodePlanKeys = new Set();
|
|
80
90
|
const configFiles = [];
|
|
91
|
+
const persistentMounts = new Map();
|
|
92
|
+
const addPersistentMount = (mount) => {
|
|
93
|
+
const existing = persistentMounts.get(mount.id);
|
|
94
|
+
if (!existing) {
|
|
95
|
+
persistentMounts.set(mount.id, mount);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (existing.mountPath !== mount.mountPath || existing.volumeName !== mount.volumeName) {
|
|
99
|
+
throw new SpawnfileError("validation_error", `Moltnet persistent mount ${mount.id} resolves to conflicting targets`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
81
102
|
for (const teamNode of teamNodes) {
|
|
82
103
|
for (const network of teamNode.value.networks ?? []) {
|
|
83
104
|
const serverPlan = serverPlans.get(createServerKey(network.id));
|
|
@@ -96,6 +117,20 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
96
117
|
mode: 0o600,
|
|
97
118
|
path: createMoltnetServerConfigPath(serverPlan.id)
|
|
98
119
|
});
|
|
120
|
+
const storeMountPath = resolveMoltnetStorePersistenceMountPath(network.id, network.server.store);
|
|
121
|
+
if (storeMountPath) {
|
|
122
|
+
const mountId = `moltnet-${network.id}-store`;
|
|
123
|
+
const store = network.server.store;
|
|
124
|
+
const volumeName = store.kind === "sqlite" || store.kind === "json"
|
|
125
|
+
? store.persistence?.name
|
|
126
|
+
: undefined;
|
|
127
|
+
addPersistentMount({
|
|
128
|
+
id: mountId,
|
|
129
|
+
mountPath: storeMountPath,
|
|
130
|
+
reason: `managed Moltnet ${network.server.store.kind} store for ${network.id}`,
|
|
131
|
+
volumeName: createPersistentVolumeName(plan.root, mountId, volumeName)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
99
134
|
}
|
|
100
135
|
}
|
|
101
136
|
for (const node of plan.nodes) {
|
|
@@ -134,17 +169,28 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
134
169
|
throw new SpawnfileError("validation_error", `Duplicate Moltnet node attachment for ${attachment.network}/${attachment.memberId}`);
|
|
135
170
|
}
|
|
136
171
|
nodePlanKeys.add(nodePlanKey);
|
|
137
|
-
const clientAuth = resolveMoltnetClientAuth(network.server, attachment.network, attachment.memberId);
|
|
172
|
+
const clientAuth = resolveMoltnetClientAuth(network.server, attachment.network, attachment.memberId, node.slug);
|
|
138
173
|
const usesPerAttachmentOpenToken = clientAuth.mode === "open" &&
|
|
139
174
|
clientAuth.staticToken !== true &&
|
|
140
175
|
Boolean(clientAuth.tokenEnv || clientAuth.tokenPath);
|
|
176
|
+
if (usesPerAttachmentOpenToken && clientAuth.tokenPath) {
|
|
177
|
+
const mountId = `agent-${node.slug}-moltnet-tokens`;
|
|
178
|
+
addPersistentMount({
|
|
179
|
+
id: mountId,
|
|
180
|
+
mountPath: createMoltnetOpenTokenDirectory(node.slug),
|
|
181
|
+
reason: `Moltnet open-mode generated agent tokens for ${agentNode.name}`,
|
|
182
|
+
volumeName: createPersistentVolumeName(plan.root, mountId)
|
|
183
|
+
});
|
|
184
|
+
}
|
|
141
185
|
configFiles.push({
|
|
142
186
|
content: `${JSON.stringify({
|
|
143
187
|
version: "moltnet.node.v1",
|
|
144
188
|
moltnet: {
|
|
145
189
|
base_url: serverPlan.baseUrl,
|
|
146
190
|
network_id: attachment.network,
|
|
147
|
-
|
|
191
|
+
...(clientAuth.mode === "none"
|
|
192
|
+
? {}
|
|
193
|
+
: { auth_mode: clientAuth.mode }),
|
|
148
194
|
...(clientAuth.staticToken
|
|
149
195
|
? { static_token: true }
|
|
150
196
|
: {}),
|
|
@@ -210,6 +256,7 @@ export const generateMoltnetArtifacts = async (plan) => {
|
|
|
210
256
|
return {
|
|
211
257
|
files: configFiles,
|
|
212
258
|
nodePlans: nodePlans.sort((left, right) => left.configPath.localeCompare(right.configPath)),
|
|
259
|
+
persistentMounts: [...persistentMounts.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
213
260
|
ports: [...new Set(managedServerPlans.map((serverPlan) => serverPlan.port).filter((port) => port !== undefined))].sort((left, right) => left - right),
|
|
214
261
|
publishedPorts: [
|
|
215
262
|
...new Set(teamNodes
|
|
@@ -22,15 +22,24 @@ export interface MoltnetNativeServerConfigInput {
|
|
|
22
22
|
mode: "managed";
|
|
23
23
|
}>;
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
type ManagedMoltnetStore = Extract<TeamNetworkServer, {
|
|
26
|
+
mode: "managed";
|
|
27
|
+
}>["store"];
|
|
28
|
+
export declare const createMoltnetOpenTokenDirectory: (agentSlug: string) => string;
|
|
29
|
+
export declare const createMoltnetOpenTokenPath: (networkId: string, memberId: string, agentSlug?: string) => string;
|
|
30
|
+
export declare const createMoltnetNetworkStateDirectory: (networkId: string) => string;
|
|
31
|
+
export declare const createDefaultMoltnetStorePath: (networkId: string, kind: "json" | "sqlite", mountPath?: string) => string;
|
|
32
|
+
export declare const resolveMoltnetStorePath: (networkId: string, store: ManagedMoltnetStore) => string | null;
|
|
33
|
+
export declare const resolveMoltnetStorePersistenceMountPath: (networkId: string, store: ManagedMoltnetStore) => string | null;
|
|
26
34
|
export declare const createMoltnetServerConfigPath: (serverId: string) => string;
|
|
27
35
|
export declare const createMoltnetNodeConfigPath: (teamSlug: string, networkId: string, agentId: string) => string;
|
|
28
36
|
export declare const renderMoltnetListenAddr: (server: Extract<TeamNetworkServer, {
|
|
29
37
|
mode: "managed";
|
|
30
38
|
}>) => string;
|
|
31
39
|
export declare const resolveMoltnetBaseUrl: (server: TeamNetworkServer) => string;
|
|
32
|
-
export declare const resolveMoltnetClientAuth: (server: TeamNetworkServer, networkId: string, memberId: string) => MoltnetClientAuthPlan;
|
|
40
|
+
export declare const resolveMoltnetClientAuth: (server: TeamNetworkServer, networkId: string, memberId: string, agentSlug?: string) => MoltnetClientAuthPlan;
|
|
33
41
|
export declare const createMoltnetNativeServerConfig: ({ networkId, networkName, rooms, server }: MoltnetNativeServerConfigInput) => {
|
|
34
42
|
config: Record<string, unknown>;
|
|
35
43
|
secretPatches: MoltnetSecretPatch[];
|
|
36
44
|
};
|
|
45
|
+
export {};
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
const pathSafeSegment = (value) => value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "") || "item";
|
|
2
2
|
const isIpv6Literal = (value) => value.includes(":");
|
|
3
|
-
|
|
3
|
+
const normalizePosixPath = (value) => value.replace(/\/+/g, "/").replace(/\/+$/u, "") || "/";
|
|
4
|
+
export const createMoltnetOpenTokenDirectory = (agentSlug) => `/var/lib/spawnfile/agents/${pathSafeSegment(agentSlug)}/state/moltnet`;
|
|
5
|
+
export const createMoltnetOpenTokenPath = (networkId, memberId, agentSlug = memberId) => `${createMoltnetOpenTokenDirectory(agentSlug)}/${pathSafeSegment(networkId)}-${pathSafeSegment(memberId)}.token`;
|
|
6
|
+
export const createMoltnetNetworkStateDirectory = (networkId) => `/var/lib/spawnfile/moltnet/networks/${pathSafeSegment(networkId)}`;
|
|
7
|
+
export const createDefaultMoltnetStorePath = (networkId, kind, mountPath) => {
|
|
8
|
+
const directory = mountPath
|
|
9
|
+
? normalizePosixPath(mountPath)
|
|
10
|
+
: createMoltnetNetworkStateDirectory(networkId);
|
|
11
|
+
return `${directory}/${kind === "sqlite" ? "moltnet.sqlite" : "state.json"}`;
|
|
12
|
+
};
|
|
13
|
+
export const resolveMoltnetStorePath = (networkId, store) => {
|
|
14
|
+
if (store.kind !== "sqlite" && store.kind !== "json") {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return store.path ?? createDefaultMoltnetStorePath(networkId, store.kind, store.persistence?.mount);
|
|
18
|
+
};
|
|
19
|
+
export const resolveMoltnetStorePersistenceMountPath = (networkId, store) => {
|
|
20
|
+
if (store.kind !== "sqlite" && store.kind !== "json") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (store.persistence?.mode === "ephemeral") {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (store.persistence?.mount) {
|
|
27
|
+
return normalizePosixPath(store.persistence.mount);
|
|
28
|
+
}
|
|
29
|
+
const storePath = resolveMoltnetStorePath(networkId, store);
|
|
30
|
+
return storePath ? storePath.slice(0, storePath.lastIndexOf("/")) || "/" : null;
|
|
31
|
+
};
|
|
4
32
|
export const createMoltnetServerConfigPath = (serverId) => `container/rootfs/var/lib/spawnfile/moltnet/servers/${pathSafeSegment(serverId)}/Moltnet.json`;
|
|
5
33
|
export const createMoltnetNodeConfigPath = (teamSlug, networkId, agentId) => `container/rootfs/var/lib/spawnfile/moltnet/nodes/${pathSafeSegment(teamSlug)}-${pathSafeSegment(networkId)}-${pathSafeSegment(agentId)}.json`;
|
|
6
34
|
export const renderMoltnetListenAddr = (server) => {
|
|
@@ -22,7 +50,7 @@ export const resolveMoltnetBaseUrl = (server) => {
|
|
|
22
50
|
: bind;
|
|
23
51
|
return `http://${host}:${server.listen.port}`;
|
|
24
52
|
};
|
|
25
|
-
export const resolveMoltnetClientAuth = (server, networkId, memberId) => {
|
|
53
|
+
export const resolveMoltnetClientAuth = (server, networkId, memberId, agentSlug) => {
|
|
26
54
|
if (server.auth.mode === "none") {
|
|
27
55
|
return { mode: "none" };
|
|
28
56
|
}
|
|
@@ -30,7 +58,7 @@ export const resolveMoltnetClientAuth = (server, networkId, memberId) => {
|
|
|
30
58
|
if (server.auth.mode === "open" && !client) {
|
|
31
59
|
return {
|
|
32
60
|
mode: "open",
|
|
33
|
-
tokenPath: createMoltnetOpenTokenPath(networkId, memberId)
|
|
61
|
+
tokenPath: createMoltnetOpenTokenPath(networkId, memberId, agentSlug)
|
|
34
62
|
};
|
|
35
63
|
}
|
|
36
64
|
if (!client) {
|
|
@@ -47,12 +75,12 @@ export const resolveMoltnetClientAuth = (server, networkId, memberId) => {
|
|
|
47
75
|
...(client.token_path ? { tokenPath: client.token_path } : {})
|
|
48
76
|
};
|
|
49
77
|
};
|
|
50
|
-
const storageConfigFor = (store) => {
|
|
78
|
+
const storageConfigFor = (networkId, store) => {
|
|
51
79
|
switch (store.kind) {
|
|
52
80
|
case "sqlite":
|
|
53
|
-
return { kind: "sqlite", sqlite: { path: store
|
|
81
|
+
return { kind: "sqlite", sqlite: { path: resolveMoltnetStorePath(networkId, store) } };
|
|
54
82
|
case "json":
|
|
55
|
-
return { kind: "json", json: { path: store
|
|
83
|
+
return { kind: "json", json: { path: resolveMoltnetStorePath(networkId, store) } };
|
|
56
84
|
case "postgres":
|
|
57
85
|
return { kind: "postgres", postgres: { dsn: "" } };
|
|
58
86
|
case "memory":
|
|
@@ -112,7 +140,7 @@ export const createMoltnetNativeServerConfig = ({ networkId, networkName, rooms,
|
|
|
112
140
|
mode: server.auth.mode,
|
|
113
141
|
...(tokens.length > 0 ? { tokens } : {})
|
|
114
142
|
},
|
|
115
|
-
storage: storageConfigFor(server.store),
|
|
143
|
+
storage: storageConfigFor(networkId, server.store),
|
|
116
144
|
rooms: rooms.map((room) => ({
|
|
117
145
|
id: room.id,
|
|
118
146
|
...(room.name ? { name: room.name } : {}),
|
|
@@ -140,6 +140,9 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
|
|
|
140
140
|
for (const port of containerReport.ports) {
|
|
141
141
|
args.push("-p", `${port}:${port}`);
|
|
142
142
|
}
|
|
143
|
+
for (const mount of containerReport.persistent_mounts ?? []) {
|
|
144
|
+
args.push("-v", `${mount.volume_name}:${mount.mount_path}`);
|
|
145
|
+
}
|
|
143
146
|
args.push("--env-file", envFilePath);
|
|
144
147
|
args.push(...(await resolveAuthMountArgs(containerReport, options.authProfile ?? null)));
|
|
145
148
|
args.push(...preparedRuntimeAuth.mountArgs);
|
|
@@ -10,6 +10,23 @@ const orderTeamAuthPairing = (pairing) => withDefinedEntries([
|
|
|
10
10
|
["remote_network_id", pairing.remote_network_id],
|
|
11
11
|
["remote_network_name", pairing.remote_network_name]
|
|
12
12
|
]);
|
|
13
|
+
const orderTeamStore = (server) => server.mode === "managed"
|
|
14
|
+
? withDefinedEntries([
|
|
15
|
+
["kind", server.store.kind],
|
|
16
|
+
["path", server.store.kind === "sqlite" || server.store.kind === "json" ? server.store.path : undefined],
|
|
17
|
+
[
|
|
18
|
+
"persistence",
|
|
19
|
+
(server.store.kind === "sqlite" || server.store.kind === "json") && server.store.persistence
|
|
20
|
+
? withDefinedEntries([
|
|
21
|
+
["mode", server.store.persistence.mode],
|
|
22
|
+
["name", server.store.persistence.name],
|
|
23
|
+
["mount", server.store.persistence.mount]
|
|
24
|
+
])
|
|
25
|
+
: undefined
|
|
26
|
+
],
|
|
27
|
+
["dsn_secret", server.store.kind === "postgres" ? server.store.dsn_secret : undefined]
|
|
28
|
+
])
|
|
29
|
+
: undefined;
|
|
13
30
|
const orderTeamAuth = (server) => withDefinedEntries([
|
|
14
31
|
["mode", server.mode],
|
|
15
32
|
["url", server.url],
|
|
@@ -32,7 +49,7 @@ const orderTeamAuth = (server) => withDefinedEntries([
|
|
|
32
49
|
]
|
|
33
50
|
])
|
|
34
51
|
],
|
|
35
|
-
["store", server
|
|
52
|
+
["store", orderTeamStore(server)],
|
|
36
53
|
[
|
|
37
54
|
"pairings",
|
|
38
55
|
server.mode === "managed" && server.pairings
|
|
@@ -673,11 +673,27 @@ declare const teamManifestSchema: z.ZodObject<{
|
|
|
673
673
|
token_secret: z.ZodString;
|
|
674
674
|
}, z.core.$strict>>>;
|
|
675
675
|
store: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
676
|
+
path: z.ZodOptional<z.ZodString>;
|
|
677
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
678
|
+
mode: z.ZodEnum<{
|
|
679
|
+
durable: "durable";
|
|
680
|
+
ephemeral: "ephemeral";
|
|
681
|
+
}>;
|
|
682
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
683
|
+
name: z.ZodOptional<z.ZodString>;
|
|
684
|
+
}, z.core.$strict>>;
|
|
676
685
|
kind: z.ZodLiteral<"sqlite">;
|
|
677
|
-
path: z.ZodString;
|
|
678
686
|
}, z.core.$strict>, z.ZodObject<{
|
|
687
|
+
path: z.ZodOptional<z.ZodString>;
|
|
688
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
689
|
+
mode: z.ZodEnum<{
|
|
690
|
+
durable: "durable";
|
|
691
|
+
ephemeral: "ephemeral";
|
|
692
|
+
}>;
|
|
693
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
694
|
+
name: z.ZodOptional<z.ZodString>;
|
|
695
|
+
}, z.core.$strict>>;
|
|
679
696
|
kind: z.ZodLiteral<"json">;
|
|
680
|
-
path: z.ZodString;
|
|
681
697
|
}, z.core.$strict>, z.ZodObject<{
|
|
682
698
|
kind: z.ZodLiteral<"postgres">;
|
|
683
699
|
dsn_secret: z.ZodString;
|
|
@@ -1312,11 +1328,27 @@ export declare const manifestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1312
1328
|
token_secret: z.ZodString;
|
|
1313
1329
|
}, z.core.$strict>>>;
|
|
1314
1330
|
store: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1331
|
+
path: z.ZodOptional<z.ZodString>;
|
|
1332
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
1333
|
+
mode: z.ZodEnum<{
|
|
1334
|
+
durable: "durable";
|
|
1335
|
+
ephemeral: "ephemeral";
|
|
1336
|
+
}>;
|
|
1337
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
1338
|
+
name: z.ZodOptional<z.ZodString>;
|
|
1339
|
+
}, z.core.$strict>>;
|
|
1315
1340
|
kind: z.ZodLiteral<"sqlite">;
|
|
1316
|
-
path: z.ZodString;
|
|
1317
1341
|
}, z.core.$strict>, z.ZodObject<{
|
|
1342
|
+
path: z.ZodOptional<z.ZodString>;
|
|
1343
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
1344
|
+
mode: z.ZodEnum<{
|
|
1345
|
+
durable: "durable";
|
|
1346
|
+
ephemeral: "ephemeral";
|
|
1347
|
+
}>;
|
|
1348
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
1349
|
+
name: z.ZodOptional<z.ZodString>;
|
|
1350
|
+
}, z.core.$strict>>;
|
|
1318
1351
|
kind: z.ZodLiteral<"json">;
|
|
1319
|
-
path: z.ZodString;
|
|
1320
1352
|
}, z.core.$strict>, z.ZodObject<{
|
|
1321
1353
|
kind: z.ZodLiteral<"postgres">;
|
|
1322
1354
|
dsn_secret: z.ZodString;
|
|
@@ -27,11 +27,27 @@ declare const teamNetworkAuthSchema: z.ZodObject<{
|
|
|
27
27
|
}, z.core.$strict>>>;
|
|
28
28
|
}, z.core.$strict>;
|
|
29
29
|
declare const teamNetworkStoreSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
30
|
+
path: z.ZodOptional<z.ZodString>;
|
|
31
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
32
|
+
mode: z.ZodEnum<{
|
|
33
|
+
durable: "durable";
|
|
34
|
+
ephemeral: "ephemeral";
|
|
35
|
+
}>;
|
|
36
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
37
|
+
name: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strict>>;
|
|
30
39
|
kind: z.ZodLiteral<"sqlite">;
|
|
31
|
-
path: z.ZodString;
|
|
32
40
|
}, z.core.$strict>, z.ZodObject<{
|
|
41
|
+
path: z.ZodOptional<z.ZodString>;
|
|
42
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
43
|
+
mode: z.ZodEnum<{
|
|
44
|
+
durable: "durable";
|
|
45
|
+
ephemeral: "ephemeral";
|
|
46
|
+
}>;
|
|
47
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
48
|
+
name: z.ZodOptional<z.ZodString>;
|
|
49
|
+
}, z.core.$strict>>;
|
|
33
50
|
kind: z.ZodLiteral<"json">;
|
|
34
|
-
path: z.ZodString;
|
|
35
51
|
}, z.core.$strict>, z.ZodObject<{
|
|
36
52
|
kind: z.ZodLiteral<"postgres">;
|
|
37
53
|
dsn_secret: z.ZodString;
|
|
@@ -80,11 +96,27 @@ declare const teamNetworkServerSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
80
96
|
token_secret: z.ZodString;
|
|
81
97
|
}, z.core.$strict>>>;
|
|
82
98
|
store: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
99
|
+
path: z.ZodOptional<z.ZodString>;
|
|
100
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
101
|
+
mode: z.ZodEnum<{
|
|
102
|
+
durable: "durable";
|
|
103
|
+
ephemeral: "ephemeral";
|
|
104
|
+
}>;
|
|
105
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
106
|
+
name: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, z.core.$strict>>;
|
|
83
108
|
kind: z.ZodLiteral<"sqlite">;
|
|
84
|
-
path: z.ZodString;
|
|
85
109
|
}, z.core.$strict>, z.ZodObject<{
|
|
110
|
+
path: z.ZodOptional<z.ZodString>;
|
|
111
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
112
|
+
mode: z.ZodEnum<{
|
|
113
|
+
durable: "durable";
|
|
114
|
+
ephemeral: "ephemeral";
|
|
115
|
+
}>;
|
|
116
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
117
|
+
name: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, z.core.$strict>>;
|
|
86
119
|
kind: z.ZodLiteral<"json">;
|
|
87
|
-
path: z.ZodString;
|
|
88
120
|
}, z.core.$strict>, z.ZodObject<{
|
|
89
121
|
kind: z.ZodLiteral<"postgres">;
|
|
90
122
|
dsn_secret: z.ZodString;
|
|
@@ -178,11 +210,27 @@ export declare const teamNetworkSchema: z.ZodObject<{
|
|
|
178
210
|
token_secret: z.ZodString;
|
|
179
211
|
}, z.core.$strict>>>;
|
|
180
212
|
store: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
213
|
+
path: z.ZodOptional<z.ZodString>;
|
|
214
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
215
|
+
mode: z.ZodEnum<{
|
|
216
|
+
durable: "durable";
|
|
217
|
+
ephemeral: "ephemeral";
|
|
218
|
+
}>;
|
|
219
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
220
|
+
name: z.ZodOptional<z.ZodString>;
|
|
221
|
+
}, z.core.$strict>>;
|
|
181
222
|
kind: z.ZodLiteral<"sqlite">;
|
|
182
|
-
path: z.ZodString;
|
|
183
223
|
}, z.core.$strict>, z.ZodObject<{
|
|
224
|
+
path: z.ZodOptional<z.ZodString>;
|
|
225
|
+
persistence: z.ZodOptional<z.ZodObject<{
|
|
226
|
+
mode: z.ZodEnum<{
|
|
227
|
+
durable: "durable";
|
|
228
|
+
ephemeral: "ephemeral";
|
|
229
|
+
}>;
|
|
230
|
+
mount: z.ZodOptional<z.ZodString>;
|
|
231
|
+
name: z.ZodOptional<z.ZodString>;
|
|
232
|
+
}, z.core.$strict>>;
|
|
184
233
|
kind: z.ZodLiteral<"json">;
|
|
185
|
-
path: z.ZodString;
|
|
186
234
|
}, z.core.$strict>, z.ZodObject<{
|
|
187
235
|
kind: z.ZodLiteral<"postgres">;
|
|
188
236
|
dsn_secret: z.ZodString;
|
|
@@ -2,6 +2,19 @@ import { z } from "zod";
|
|
|
2
2
|
export { teamWorkspaceDocsSchema, teamWorkspaceSchema } from "./workspaceSchemas.js";
|
|
3
3
|
const moltnetScopeSchema = z.enum(["observe", "write", "admin", "attach", "pair"]);
|
|
4
4
|
const countTruthy = (value) => value.filter((entry) => Boolean(entry)).length;
|
|
5
|
+
const absolutePosixPathSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.trim()
|
|
8
|
+
.min(1)
|
|
9
|
+
.refine((value) => value.startsWith("/"), {
|
|
10
|
+
message: "path must be an absolute POSIX path"
|
|
11
|
+
});
|
|
12
|
+
const normalizePosixPath = (value) => value.replace(/\/+/g, "/").replace(/\/+$/u, "") || "/";
|
|
13
|
+
const pathIsInsideMount = (filePath, mountPath) => {
|
|
14
|
+
const normalizedPath = normalizePosixPath(filePath);
|
|
15
|
+
const normalizedMount = normalizePosixPath(mountPath);
|
|
16
|
+
return normalizedPath.startsWith(`${normalizedMount}/`);
|
|
17
|
+
};
|
|
5
18
|
const teamNetworkAuthTokenSchema = z
|
|
6
19
|
.object({
|
|
7
20
|
agents: z.array(z.string().trim().min(1)).optional(),
|
|
@@ -91,18 +104,41 @@ const teamNetworkAuthSchema = z
|
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
});
|
|
94
|
-
const
|
|
107
|
+
const teamNetworkStorePersistenceSchema = z
|
|
95
108
|
.object({
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
mode: z.enum(["durable", "ephemeral"]),
|
|
110
|
+
mount: absolutePosixPathSchema.optional(),
|
|
111
|
+
name: z.string().trim().min(1).optional()
|
|
98
112
|
})
|
|
99
|
-
.strict()
|
|
100
|
-
|
|
113
|
+
.strict()
|
|
114
|
+
.superRefine((value, context) => {
|
|
115
|
+
if (value.mode === "ephemeral" && (value.mount || value.name)) {
|
|
116
|
+
context.addIssue({
|
|
117
|
+
code: z.ZodIssueCode.custom,
|
|
118
|
+
message: "ephemeral persistence must not declare mount or name"
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const teamNetworkFileStoreSchema = z
|
|
101
123
|
.object({
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
path: absolutePosixPathSchema.optional(),
|
|
125
|
+
persistence: teamNetworkStorePersistenceSchema.optional()
|
|
104
126
|
})
|
|
105
|
-
.strict()
|
|
127
|
+
.strict()
|
|
128
|
+
.superRefine((value, context) => {
|
|
129
|
+
if (value.path && value.persistence?.mount && !pathIsInsideMount(value.path, value.persistence.mount)) {
|
|
130
|
+
context.addIssue({
|
|
131
|
+
code: z.ZodIssueCode.custom,
|
|
132
|
+
message: "store.path must be inside persistence.mount"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
const teamNetworkStoreSqliteSchema = teamNetworkFileStoreSchema.extend({
|
|
137
|
+
kind: z.literal("sqlite")
|
|
138
|
+
});
|
|
139
|
+
const teamNetworkStoreJsonSchema = teamNetworkFileStoreSchema.extend({
|
|
140
|
+
kind: z.literal("json")
|
|
141
|
+
});
|
|
106
142
|
const teamNetworkStorePostgresSchema = z
|
|
107
143
|
.object({
|
|
108
144
|
kind: z.literal("postgres"),
|
package/dist/report/types.d.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface ContainerWorkspaceResourceReport {
|
|
|
27
27
|
mount: string;
|
|
28
28
|
sharing: "per_agent" | "team";
|
|
29
29
|
}
|
|
30
|
+
export interface ContainerPersistentMountReport {
|
|
31
|
+
id: string;
|
|
32
|
+
mount_path: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
volume_name: string;
|
|
35
|
+
}
|
|
30
36
|
export interface NodeReport {
|
|
31
37
|
capabilities: CapabilityReport[];
|
|
32
38
|
diagnostics: DiagnosticReport[];
|
|
@@ -49,6 +55,7 @@ export interface ContainerReport {
|
|
|
49
55
|
runtime_secrets_required: string[];
|
|
50
56
|
runtimes_installed: string[];
|
|
51
57
|
secrets_required: string[];
|
|
58
|
+
persistent_mounts?: ContainerPersistentMountReport[];
|
|
52
59
|
workspace_resources?: ContainerWorkspaceResourceReport[];
|
|
53
60
|
}
|
|
54
61
|
export interface CompileReport {
|