@clawscarf/cli 0.1.0-alpha.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.
Files changed (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/THIRD_PARTY_NOTICES.md +102 -0
  4. package/deploy/execution/browser/LICENSE.playwright +202 -0
  5. package/deploy/execution/browser/seccomp.json +640 -0
  6. package/deploy/execution/browser-node/configuration.js +85 -0
  7. package/deploy/execution/browser-node/operator.js +167 -0
  8. package/deploy/execution/network/node-ingress.cfg +35 -0
  9. package/deploy/models/catalog.json +275 -0
  10. package/deploy/openshell/policy.yaml +24 -0
  11. package/generated/http/LICENSE.md +21 -0
  12. package/generated/http/client/client.gen.d.ts +2 -0
  13. package/generated/http/client/client.gen.js +216 -0
  14. package/generated/http/client/index.d.ts +10 -0
  15. package/generated/http/client/index.js +6 -0
  16. package/generated/http/client/types.gen.d.ts +120 -0
  17. package/generated/http/client/types.gen.js +2 -0
  18. package/generated/http/client/utils.gen.d.ts +37 -0
  19. package/generated/http/client/utils.gen.js +228 -0
  20. package/generated/http/core/auth.gen.d.ts +25 -0
  21. package/generated/http/core/auth.gen.js +14 -0
  22. package/generated/http/core/bodySerializer.gen.d.ts +25 -0
  23. package/generated/http/core/bodySerializer.gen.js +57 -0
  24. package/generated/http/core/params.gen.d.ts +43 -0
  25. package/generated/http/core/params.gen.js +109 -0
  26. package/generated/http/core/pathSerializer.gen.d.ts +33 -0
  27. package/generated/http/core/pathSerializer.gen.js +106 -0
  28. package/generated/http/core/queryKeySerializer.gen.d.ts +18 -0
  29. package/generated/http/core/queryKeySerializer.gen.js +92 -0
  30. package/generated/http/core/serverSentEvents.gen.d.ts +71 -0
  31. package/generated/http/core/serverSentEvents.gen.js +132 -0
  32. package/generated/http/core/types.gen.d.ts +83 -0
  33. package/generated/http/core/types.gen.js +2 -0
  34. package/generated/http/core/utils.gen.d.ts +19 -0
  35. package/generated/http/core/utils.gen.js +87 -0
  36. package/package.json +37 -0
  37. package/packs/README.md +130 -0
  38. package/packs/research-team/pack.json +20 -0
  39. package/packs/research-team/researcher/CLAW.md +19 -0
  40. package/packs/research-team/researcher/package.json +8 -0
  41. package/packs/research-team/researcher/profiles/openclaw.yml +6 -0
  42. package/packs/research-team/reviewer/CLAW.md +19 -0
  43. package/packs/research-team/reviewer/package.json +8 -0
  44. package/packs/research-team/reviewer/profiles/openclaw.yml +6 -0
  45. package/pnpm-lock.yaml +7158 -0
  46. package/recipes/README.md +37 -0
  47. package/recipes/team-server/recipe.json +28 -0
  48. package/release/README.md +137 -0
  49. package/release/components.json +29 -0
  50. package/release/operator.md +63 -0
  51. package/runtime/configuration.js +135 -0
  52. package/runtime/connections-configuration.js +70 -0
  53. package/runtime/model-contract.js +92 -0
  54. package/runtime/releases/0.1.0-alpha.1.json +37 -0
  55. package/scripts/clawscarf.js +92 -0
  56. package/scripts/cloud/login.js +134 -0
  57. package/scripts/cloud/registration.js +232 -0
  58. package/scripts/cloud/url.js +9 -0
  59. package/scripts/connections.js +193 -0
  60. package/scripts/controller.js +143 -0
  61. package/scripts/deployment/browser-node-compose.js +81 -0
  62. package/scripts/deployment/browser-node-helper.js +9 -0
  63. package/scripts/deployment/browser-node-pairing.js +165 -0
  64. package/scripts/deployment/browser-node.js +154 -0
  65. package/scripts/deployment/browser.js +143 -0
  66. package/scripts/deployment/certificates.js +76 -0
  67. package/scripts/deployment/compose.js +191 -0
  68. package/scripts/deployment/configuration.js +275 -0
  69. package/scripts/deployment/connection-policy.js +80 -0
  70. package/scripts/deployment/connection-settings.js +60 -0
  71. package/scripts/deployment/connections-runtime.js +31 -0
  72. package/scripts/deployment/connections.js +213 -0
  73. package/scripts/deployment/controller-compose.js +59 -0
  74. package/scripts/deployment/database.js +199 -0
  75. package/scripts/deployment/entrypoint.js +7 -0
  76. package/scripts/deployment/images.js +17 -0
  77. package/scripts/deployment/initial-services.js +53 -0
  78. package/scripts/deployment/launch.js +177 -0
  79. package/scripts/deployment/logs.js +15 -0
  80. package/scripts/deployment/model-gateway-compose.js +74 -0
  81. package/scripts/deployment/model-gateway.js +125 -0
  82. package/scripts/deployment/models.js +82 -0
  83. package/scripts/deployment/networks.js +354 -0
  84. package/scripts/deployment/policy.js +56 -0
  85. package/scripts/deployment/preflight.js +124 -0
  86. package/scripts/deployment/prepare.js +205 -0
  87. package/scripts/deployment/process.js +70 -0
  88. package/scripts/deployment/relay.js +58 -0
  89. package/scripts/deployment/runtime-binding.js +100 -0
  90. package/scripts/deployment/runtime.js +232 -0
  91. package/scripts/deployment/state.js +154 -0
  92. package/scripts/deployment/team.js +76 -0
  93. package/scripts/deployment/upgrade-rpc.py +148 -0
  94. package/scripts/deployment/upgrade-state.js +47 -0
  95. package/scripts/deployment/upgrade.js +298 -0
  96. package/scripts/deployment/volumes.js +24 -0
  97. package/scripts/errors.js +9 -0
  98. package/scripts/installation/administrator.js +23 -0
  99. package/scripts/installation/command.js +176 -0
  100. package/scripts/installation/configuration.js +104 -0
  101. package/scripts/installation/configure.js +122 -0
  102. package/scripts/installation/delete.js +111 -0
  103. package/scripts/installation/doctor.js +8 -0
  104. package/scripts/installation/errors.js +8 -0
  105. package/scripts/installation/files.js +51 -0
  106. package/scripts/installation/installer/cloud.js +22 -0
  107. package/scripts/installation/installer/collect.js +245 -0
  108. package/scripts/installation/installer/inputs.js +59 -0
  109. package/scripts/installation/installer/menu.js +49 -0
  110. package/scripts/installation/installer/prompts.js +139 -0
  111. package/scripts/installation/installer/run.js +251 -0
  112. package/scripts/installation/installer/secrets.js +16 -0
  113. package/scripts/installation/installer/sections/access.js +69 -0
  114. package/scripts/installation/installer/sections/certificates.js +18 -0
  115. package/scripts/installation/installer/sections/connections.js +4 -0
  116. package/scripts/installation/installer/sections/external-models.js +35 -0
  117. package/scripts/installation/installer/sections/models.js +119 -0
  118. package/scripts/installation/installer/sections/packs.js +82 -0
  119. package/scripts/installation/installer/sections/resources.js +10 -0
  120. package/scripts/installation/installer/settings.js +192 -0
  121. package/scripts/installation/installer/summary.js +44 -0
  122. package/scripts/installation/lifecycle.js +127 -0
  123. package/scripts/installation/location.js +3 -0
  124. package/scripts/installation/models.js +42 -0
  125. package/scripts/installation/options.js +360 -0
  126. package/scripts/installation/packs.js +150 -0
  127. package/scripts/installation/plan.js +121 -0
  128. package/scripts/installation/prerequisites.js +170 -0
  129. package/scripts/installation/recipes/catalog.js +55 -0
  130. package/scripts/installation/recipes/definition.js +32 -0
  131. package/scripts/installation/reconfigure.js +255 -0
  132. package/scripts/installation/requirements.js +23 -0
  133. package/scripts/installation/resolve.js +217 -0
  134. package/scripts/installation/runtime.js +87 -0
  135. package/scripts/installation/save.js +95 -0
  136. package/scripts/installation/setup.js +91 -0
  137. package/scripts/models/catalog.js +9 -0
  138. package/scripts/models/configuration.js +184 -0
  139. package/scripts/models/credentials.js +132 -0
  140. package/scripts/models/runtime.js +87 -0
  141. package/scripts/output.js +34 -0
  142. package/scripts/packs/connections.js +95 -0
  143. package/scripts/packs/lifecycle.js +125 -0
  144. package/scripts/packs/model.js +66 -0
  145. package/scripts/packs/native.js +69 -0
  146. package/scripts/packs/openshell.js +148 -0
  147. package/scripts/packs/policy.js +189 -0
  148. package/scripts/packs/requirements.in +1 -0
  149. package/scripts/packs/requirements.js +11 -0
  150. package/scripts/packs/requirements.txt +116 -0
  151. package/scripts/packs/source.js +90 -0
  152. package/scripts/packs/transport.py +68 -0
  153. package/scripts/people.js +52 -0
  154. package/scripts/release/create.js +62 -0
  155. package/scripts/release/definition.js +51 -0
  156. package/scripts/session.js +27 -0
  157. package/services/access/generated/client.gen.d.ts +12 -0
  158. package/services/access/generated/client.gen.js +3 -0
  159. package/services/access/generated/fastify.gen.d.ts +49 -0
  160. package/services/access/generated/fastify.gen.js +2 -0
  161. package/services/access/generated/index.d.ts +2 -0
  162. package/services/access/generated/index.js +2 -0
  163. package/services/access/generated/sdk.gen.d.ts +28 -0
  164. package/services/access/generated/sdk.gen.js +107 -0
  165. package/services/access/generated/types.gen.d.ts +349 -0
  166. package/services/access/generated/types.gen.js +2 -0
  167. package/services/access/migrations/001_access.sql +39 -0
  168. package/services/access/migrations/002_administrator_setup.sql +16 -0
  169. package/services/access/migrations/003_local_login_completion.sql +5 -0
  170. package/services/access/migrations/004_invitations.sql +21 -0
  171. package/services/access/repo/postgres.js +341 -0
  172. package/services/access/repo/session-logout.js +28 -0
  173. package/services/access/types/credential.js +2 -0
  174. package/services/access/types/errors.js +7 -0
  175. package/services/access/types/ingress.js +1 -0
  176. package/services/access/types/model.js +1 -0
  177. package/services/access/types/native-errors.js +7 -0
  178. package/services/access/types/native.js +1 -0
  179. package/services/cloud/generated/client.gen.d.ts +12 -0
  180. package/services/cloud/generated/client.gen.js +3 -0
  181. package/services/cloud/generated/index.d.ts +2 -0
  182. package/services/cloud/generated/index.js +2 -0
  183. package/services/cloud/generated/sdk.gen.d.ts +145 -0
  184. package/services/cloud/generated/sdk.gen.js +488 -0
  185. package/services/cloud/generated/types.gen.d.ts +2034 -0
  186. package/services/cloud/generated/types.gen.js +2 -0
  187. package/services/connections/cloud/generated/client.gen.d.ts +12 -0
  188. package/services/connections/cloud/generated/client.gen.js +3 -0
  189. package/services/connections/cloud/generated/fastify.gen.d.ts +68 -0
  190. package/services/connections/cloud/generated/fastify.gen.js +2 -0
  191. package/services/connections/cloud/generated/index.d.ts +2 -0
  192. package/services/connections/cloud/generated/index.js +2 -0
  193. package/services/connections/cloud/generated/sdk.gen.d.ts +95 -0
  194. package/services/connections/cloud/generated/sdk.gen.js +210 -0
  195. package/services/connections/cloud/generated/types.gen.d.ts +1044 -0
  196. package/services/connections/cloud/generated/types.gen.js +2 -0
  197. package/services/connections/migrations/001_connections.sql +220 -0
@@ -0,0 +1,170 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { createInterface } from "node:readline";
3
+ import { spawn } from "node:child_process";
4
+ import { LocalSetupError, run } from "../deployment/process.js";
5
+ import { portAvailable, verifyLocalPorts } from "../deployment/preflight.js";
6
+ import { readState } from "../deployment/state.js";
7
+ import { openshellGatewayImage, verifyRuntimeImage, } from "../deployment/images.js";
8
+ import { installationSchema, } from "./configuration.js";
9
+ import { readRuntime, assertReleaseCapabilities } from "./setup.js";
10
+ import { readJson } from "./files.js";
11
+ import { acquireRuntimeTools } from "./runtime.js";
12
+ import { InstallationError } from "./errors.js";
13
+ /** No configuration, credentials or cloud account is needed for these checks. */
14
+ export async function checkHost(command = run) {
15
+ if (process.platform !== "darwin" || process.arch !== "arm64")
16
+ throw new InstallationError("unsupported_platform", "This release requires macOS on Apple Silicon with Docker Desktop.");
17
+ let os;
18
+ try {
19
+ os = await command("docker", ["info", "--format", "{{.OSType}}"]);
20
+ }
21
+ catch (error) {
22
+ const missing = error instanceof LocalSetupError &&
23
+ error.commandFailure?.reason === "spawn" &&
24
+ error.commandFailure.systemCode === "ENOENT";
25
+ throw new InstallationError("unavailable", missing
26
+ ? "Docker is not installed or is not on PATH. Install Docker Desktop, start it, then run configure again."
27
+ : "Cannot reach Docker. Start Docker Desktop and wait until it is running, then try again.");
28
+ }
29
+ if (os.trim() !== "linux")
30
+ throw new InstallationError("unsupported_platform", "Docker must run Linux containers.");
31
+ try {
32
+ await command("docker", ["compose", "version"]);
33
+ }
34
+ catch {
35
+ throw new InstallationError("unavailable", "Docker Compose is unavailable. Install or update Docker Desktop, then try again.");
36
+ }
37
+ }
38
+ /** Stream only Docker's layer progress, never registry error payloads or credentials. */
39
+ export function pullImage(image, report, signal) {
40
+ return new Promise((resolve, reject) => {
41
+ const child = spawn("docker", ["pull", image], {
42
+ stdio: ["ignore", "pipe", "pipe"],
43
+ ...(signal ? { signal } : {}),
44
+ timeout: 30 * 60_000,
45
+ });
46
+ let last = 0;
47
+ let spawnFailed = false;
48
+ const readers = [child.stdout, child.stderr].map((stream) => createInterface({ input: stream }));
49
+ for (const reader of readers)
50
+ reader.on("line", (line) => {
51
+ if (/^[a-f0-9]{12}: (?:Pulling fs layer|Waiting|Downloading|Extracting|Verifying Checksum|Download complete|Pull complete)(?:\s+\[[=> ]*\]\s+[\d.]+[kMGT]?B\/[\d.]+[kMGT]?B)?$/.test(line) &&
52
+ Date.now() - last >= 500) {
53
+ report(`${image.split("@")[0] ?? image} — ${line}`);
54
+ last = Date.now();
55
+ }
56
+ });
57
+ child.once("error", () => {
58
+ spawnFailed = true;
59
+ });
60
+ child.once("close", (code) => {
61
+ readers.forEach((reader) => {
62
+ reader.close();
63
+ });
64
+ if (signal?.aborted)
65
+ reject(signal.reason instanceof Error
66
+ ? signal.reason
67
+ : new Error("Download cancelled."));
68
+ else if (spawnFailed)
69
+ reject(new InstallationError("unavailable", "Image download could not start. Check Docker and try again."));
70
+ else if (code === 0)
71
+ resolve();
72
+ else
73
+ reject(new InstallationError("unavailable", `Could not download ${image}. Check network access and registry authentication, then run configure again.`));
74
+ });
75
+ });
76
+ }
77
+ export async function checkNewInstallationPorts(exposure) {
78
+ const ports = exposure.mode === "local"
79
+ ? [exposure.applicationPort, exposure.widgetPort]
80
+ : [
81
+ Number(new URL(exposure.applicationOrigin).port || 443),
82
+ Number(new URL(exposure.widgetOrigin).port || 443),
83
+ ];
84
+ const host = exposure.mode === "local" ? "127.0.0.1" : "0.0.0.0";
85
+ for (const port of ports)
86
+ if (!(await portAvailable(port, host)))
87
+ throw new InstallationError("unavailable", `Port ${host}:${String(port)} is already in use. Stop its current owner or choose different listener ports in configure.`);
88
+ }
89
+ /** Runs before cloud registration; tools and registry images may be fetched. */
90
+ export async function checkInstallationPrerequisites(configFile, options = {}, command = run, pull = pullImage) {
91
+ const report = options.report ?? (() => { });
92
+ const config = installationSchema.parse(await readJson(configFile));
93
+ const base = dirname(resolve(configFile));
94
+ const { release, releaseFile } = await readRuntime(resolve(base, config.releaseFile));
95
+ assertReleaseCapabilities({ release }, config);
96
+ await acquireRuntimeTools(releaseFile, options);
97
+ report("Runtime tools verified");
98
+ let state;
99
+ try {
100
+ state = await readState(resolve(base, config.stateDirectory));
101
+ }
102
+ catch (error) {
103
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
104
+ throw error;
105
+ }
106
+ if (state)
107
+ await verifyLocalPorts(state, command);
108
+ else {
109
+ await checkNewInstallationPorts(config.exposure);
110
+ }
111
+ if (config.packOperator)
112
+ await command(resolve(base, config.packOperator.pythonExecutable), ["-c", "from openshell.sandbox import SandboxClient"], { timeout: 10000 });
113
+ const images = [
114
+ ...new Set([
115
+ release.images.gateway,
116
+ release.images.companion,
117
+ release.images.openshellClient,
118
+ release.images.postgres,
119
+ openshellGatewayImage,
120
+ ...(config.models.mode === "litellm" && release.images.models
121
+ ? [release.images.models]
122
+ : []),
123
+ ...(config.browser.enabled && release.images.browser
124
+ ? [
125
+ ...Object.values(release.images.browser),
126
+ ...(release.images.relay ? [release.images.relay] : []),
127
+ ]
128
+ : []),
129
+ ]),
130
+ ];
131
+ const missing = [];
132
+ for (const image of images) {
133
+ options.signal?.throwIfAborted();
134
+ try {
135
+ await command("docker", ["image", "inspect", image]);
136
+ }
137
+ catch (error) {
138
+ if (!(error instanceof LocalSetupError &&
139
+ error.commandFailure?.reason === "exit" &&
140
+ error.commandFailure.exitCode === 1))
141
+ throw error;
142
+ missing.push(image);
143
+ }
144
+ }
145
+ if (missing.length) {
146
+ await checkHost(command);
147
+ const local = missing.filter((image) => image.startsWith("sha256:"));
148
+ if (local.length)
149
+ throw new InstallationError("unavailable", `Missing local development images: ${local.join(", ")}. Rebuild the development images and update the runtime definition; these image IDs cannot be downloaded.`);
150
+ if (!options.acquire)
151
+ throw new InstallationError("unavailable", `Missing images: ${missing.join(", ")}. Run configure to download them.`);
152
+ report(`Downloading ${String(missing.length)} required images (first setup can take several minutes):\n${missing.map((image) => ` ${image}`).join("\n")}`);
153
+ for (const image of missing) {
154
+ options.signal?.throwIfAborted();
155
+ report(`Downloading ${image}`);
156
+ await pull(image, report, options.signal);
157
+ await command("docker", ["image", "inspect", image]);
158
+ }
159
+ }
160
+ else
161
+ report(`All ${String(images.length)} required images are already available`);
162
+ await verifyRuntimeImage(release.images.gateway, command);
163
+ options.signal?.throwIfAborted();
164
+ return {
165
+ state: "prerequisites_available",
166
+ platform: `${process.platform}-${process.arch}`,
167
+ release: release.version,
168
+ images: images.length,
169
+ };
170
+ }
@@ -0,0 +1,55 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { recipeSchema, recipesSchema } from "./definition.js";
5
+ import { readJson } from "../files.js";
6
+ import { modelCatalogSchema } from "../../models/catalog.js";
7
+ import { openPack } from "../../packs/source.js";
8
+ import { recipeModelRoutes } from "../models.js";
9
+ import { InstallationError } from "../errors.js";
10
+ const packageRoot = fileURLToPath(new URL("../../../", import.meta.url));
11
+ export async function readRecipe(file) {
12
+ const recipe = recipeSchema.parse(await readJson(file));
13
+ return { ...recipe, runtime: resolve(dirname(file), recipe.runtime) };
14
+ }
15
+ /** Small, local package assets. Runtime images and executables are separate artifacts. */
16
+ export async function installationCatalog(root = packageRoot) {
17
+ const entries = await readdir(join(root, "recipes"), { withFileTypes: true });
18
+ const recipes = recipesSchema.parse(await Promise.all(entries
19
+ .filter((entry) => entry.isDirectory())
20
+ .sort((a, b) => a.name.localeCompare(b.name))
21
+ .map((entry) => readRecipe(join(root, "recipes", entry.name, "recipe.json")))));
22
+ const modelCatalog = modelCatalogSchema.parse(await readJson(join(root, "deploy/models/catalog.json")));
23
+ const packEntries = await readdir(join(root, "packs"), {
24
+ withFileTypes: true,
25
+ });
26
+ const packs = await Promise.all(packEntries
27
+ .filter((entry) => entry.isDirectory())
28
+ .sort((a, b) => a.name.localeCompare(b.name))
29
+ .map(async (entry) => {
30
+ const directory = join(root, "packs", entry.name);
31
+ const pack = await openPack(directory);
32
+ return {
33
+ id: pack.manifest.id,
34
+ directory,
35
+ members: pack.manifest.members.map((member) => member.id),
36
+ };
37
+ }));
38
+ if (new Set(packs.map((pack) => pack.id)).size !== packs.length)
39
+ throw new InstallationError("invalid_configuration", "Pack IDs must be unique.");
40
+ for (const recipe of recipes)
41
+ validateRecipe(recipe, { packs, modelCatalog });
42
+ return { recipes, packs, modelCatalog };
43
+ }
44
+ export function validateRecipe(recipe, catalog) {
45
+ recipeModelRoutes(recipe.models, catalog.modelCatalog);
46
+ const selected = new Set();
47
+ for (const entry of recipe.packs) {
48
+ const pack = catalog.packs.find((pack) => pack.id === entry.id);
49
+ for (const member of entry.members) {
50
+ if (!pack?.members.includes(member) || selected.has(member))
51
+ throw new InstallationError("invalid_configuration", `Recipe ${recipe.id} selects a missing or duplicate pack agent: ${entry.id}/${member}.`);
52
+ selected.add(member);
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ import { installationSchema } from "../configuration.js";
3
+ /** Recipes are CLI-owned defaults, never executable installation hooks. */
4
+ export const recipeSchema = z.strictObject({
5
+ id: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
6
+ version: z.string().regex(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/),
7
+ runtime: z.string().min(1),
8
+ name: z.string().min(1).max(100),
9
+ description: z.string().min(1).max(500),
10
+ maturity: z.enum(["example", "supported"]),
11
+ defaults: z.strictObject({
12
+ resources: installationSchema.shape.resources,
13
+ browser: installationSchema.shape.browser,
14
+ connections: z.strictObject({ enabled: z.boolean() }).optional(),
15
+ }),
16
+ models: z.strictObject({
17
+ model: z.string().min(1),
18
+ provider: z.string().min(1),
19
+ reasoning: z.enum(["low", "medium", "high"]).optional(),
20
+ }),
21
+ packs: z
22
+ .array(z.strictObject({
23
+ id: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/),
24
+ members: installationSchema.shape.packs.element.shape.members,
25
+ }))
26
+ .max(32)
27
+ .default([]),
28
+ });
29
+ export const recipesSchema = z
30
+ .array(recipeSchema)
31
+ .max(32)
32
+ .refine((recipes) => new Set(recipes.map((recipe) => recipe.id)).size === recipes.length, "Recipe IDs must be unique.");
@@ -0,0 +1,255 @@
1
+ import { applyConnectionSettings } from "../deployment/connection-settings.js";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { z } from "zod";
6
+ import { compose } from "../deployment/compose.js";
7
+ import { setRuntimeCredentialModels } from "../models/credentials.js";
8
+ import { configurationChanges, resolveConfigurationInputs, } from "./configure.js";
9
+ import { ModelConfigurationError } from "../../runtime/model-contract.js";
10
+ import { loadGatewayConfiguration, prepareModelGateway, } from "../deployment/model-gateway.js";
11
+ import { readInitialConnectionsEndpoint, loadInitialConnections, prepareInitialConnections, } from "../deployment/connections.js";
12
+ import { configureStoppedRuntimeModels } from "../models/runtime.js";
13
+ import { loadInitialModels } from "../deployment/models.js";
14
+ import { readState, resourceNames, withInstallationLock, writePrivate, } from "../deployment/state.js";
15
+ import { requireNoUpgrade } from "../deployment/upgrade-state.js";
16
+ import { run } from "../deployment/process.js";
17
+ import { selectionSchema } from "./packs.js";
18
+ import { installationSchema } from "./configuration.js";
19
+ import { resolveInstallation } from "./resolve.js";
20
+ import { readJson, fingerprint } from "./files.js";
21
+ import { InstallationError } from "./errors.js";
22
+ function unsupported(message) {
23
+ throw new InstallationError("change_unsupported", message);
24
+ }
25
+ /** Deliberately bounded: initial preparation is never used to replace retained state. */
26
+ export async function planSettingsChange(configFile, reapply) {
27
+ const config = installationSchema.parse(await readJson(configFile));
28
+ const directory = resolve(dirname(resolve(configFile)), config.stateDirectory);
29
+ const state = await readState(directory);
30
+ const old = state.input;
31
+ const desired = await resolveInstallation(configFile, [
32
+ old.ports.controller,
33
+ old.ports.management,
34
+ old.ports.native,
35
+ old.ports.nativeWidgets,
36
+ old.ports.database,
37
+ old.browser?.port ?? 65534,
38
+ old.modelGateway?.port ?? 65533,
39
+ ]);
40
+ const next = desired.input;
41
+ const previousPacks = selectionSchema.parse(await readJson(join(directory, "packs.json")));
42
+ if (!desired.packSelection.python && previousPacks.python)
43
+ desired.packSelection.python = previousPacks.python;
44
+ const { models: _oldModels, modelGateway: oldGateway, connections: oldConnections, ...oldFixed } = old;
45
+ const { models: _nextModels, modelGateway: nextGateway, connections: nextConnections, ...nextFixed } = next;
46
+ if (!isDeepStrictEqual(oldFixed, nextFixed) ||
47
+ !isDeepStrictEqual(await readJson(join(directory, "release.json")), desired.release))
48
+ unsupported("Settings changes cannot replace the release, identity, addresses, access, resources or execution protection.");
49
+ if (Boolean(oldGateway) !== Boolean(nextGateway) ||
50
+ (oldGateway &&
51
+ nextGateway &&
52
+ (oldGateway.image !== nextGateway.image ||
53
+ oldGateway.port !== nextGateway.port)))
54
+ unsupported("Switching between bundled and external model gateways requires a deployment change.");
55
+ if (!nextGateway) {
56
+ const before = await loadInitialModels(old.models);
57
+ const after = await loadInitialModels(next.models);
58
+ if (!before || !after || !isDeepStrictEqual(before.network, after.network))
59
+ unsupported("Changing the external model gateway address requires updating its protected network policy.");
60
+ }
61
+ const previousEndpoint = await readInitialConnectionsEndpoint(directory);
62
+ const connections = await loadInitialConnections(next.connections);
63
+ await prepareInitialConnections(directory, connections, {
64
+ state: { ...state, input: next },
65
+ apply: false,
66
+ });
67
+ const modelConfiguration = nextGateway
68
+ ? (await loadGatewayConfiguration(nextGateway.configurationFile, nextGateway.upstreamEnvironmentFile)).configuration
69
+ : (await loadInitialModels(next.models))?.configuration;
70
+ const pending = z
71
+ .strictObject({
72
+ ownerId: z.literal(state.ownerId),
73
+ settingsPending: z.string().optional(),
74
+ settingsCandidate: z.string().optional(),
75
+ settingsReapply: z.enum(["models", "connections"]).optional(),
76
+ })
77
+ .parse(await readJson(join(directory, "prepared.json")));
78
+ if (pending.settingsPending &&
79
+ pending.settingsPending !== desired.fingerprint)
80
+ unsupported("Finish the pending settings change with its original candidate before selecting another configuration.");
81
+ if (pending.settingsReapply && reapply && pending.settingsReapply !== reapply)
82
+ unsupported("Resume the pending change before selecting another capability to reapply.");
83
+ reapply ??= pending.settingsReapply;
84
+ const scopes = await configurationChanges(installationSchema.parse(await readJson(join(directory, "settings.json"))), resolveConfigurationInputs(config, dirname(resolve(configFile))));
85
+ if (reapply)
86
+ scopes[reapply] = true;
87
+ return {
88
+ directory,
89
+ state,
90
+ desired,
91
+ previousEndpoint,
92
+ scopes,
93
+ reapply,
94
+ changes: {
95
+ models: modelConfiguration
96
+ ? {
97
+ enabled: modelConfiguration.models
98
+ .filter((model) => model.enabled)
99
+ .map((model) => model.id),
100
+ default: modelConfiguration.defaultModel,
101
+ reasoning: modelConfiguration.thinkingDefault ?? "unchanged",
102
+ }
103
+ : undefined,
104
+ connections: {
105
+ from: oldConnections?.mode ?? "disabled",
106
+ to: nextConnections?.mode ?? "disabled",
107
+ },
108
+ packs: {
109
+ selected: desired.packSelection.packs.flatMap((pack) => pack.members),
110
+ removed: previousPacks.packs
111
+ .flatMap((pack) => pack.members)
112
+ .filter((member) => !desired.packSelection.packs.some((pack) => pack.members.includes(member))),
113
+ },
114
+ },
115
+ fingerprint: fingerprint(JSON.stringify({
116
+ scopes,
117
+ reapply,
118
+ desired: desired.fingerprint,
119
+ state,
120
+ previousPacks,
121
+ accepted: await readFile(join(directory, "inputs.sha256"), "utf8"),
122
+ pending: pending.settingsPending ?? null,
123
+ })),
124
+ resuming: !!pending.settingsPending,
125
+ };
126
+ }
127
+ /** Explicit retained capability changes; native users and individual model overrides remain native-owned. */
128
+ export async function reconfigureInstallation(configFile, expectedFingerprint, reapply) {
129
+ const planned = await planSettingsChange(configFile, reapply);
130
+ return withInstallationLock(planned.directory, async () => {
131
+ const checked = await planSettingsChange(configFile, reapply);
132
+ if (checked.fingerprint !== expectedFingerprint)
133
+ throw new InstallationError("stale_plan", "Settings changed after preview. Review them again.");
134
+ const { directory, state, desired, scopes } = checked;
135
+ if (!checked.resuming && !Object.values(scopes).some(Boolean))
136
+ return { state: "unchanged", restartRequired: false, directory };
137
+ await requireNoUpgrade(directory);
138
+ const names = resourceNames(state);
139
+ for (const filter of [
140
+ `label=com.docker.compose.project=${names.project}`,
141
+ `volume=${names.volume}`,
142
+ ])
143
+ if ((await run("docker", [
144
+ "container",
145
+ "ls",
146
+ "--quiet",
147
+ "--filter",
148
+ filter,
149
+ ])).trim())
150
+ unsupported("Stop this installation before applying settings.");
151
+ const volume = z
152
+ .object({
153
+ Name: z.literal(names.volume),
154
+ Labels: z.record(z.string(), z.string()),
155
+ })
156
+ .parse(JSON.parse(await run("docker", [
157
+ "volume",
158
+ "inspect",
159
+ "--format",
160
+ '{"Name":{{json .Name}},"Labels":{{json .Labels}}}',
161
+ names.volume,
162
+ ])));
163
+ if (volume.Labels["clawscarf.installation"] !== state.ownerId)
164
+ unsupported("The native home volume does not belong to this installation.");
165
+ const prepared = join(directory, "prepared.json");
166
+ let models = await loadInitialModels(state.input.models);
167
+ const gateway = desired.input.modelGateway;
168
+ const loaded = gateway
169
+ ? await loadGatewayConfiguration(gateway.configurationFile, gateway.upstreamEnvironmentFile)
170
+ : undefined;
171
+ if (!gateway)
172
+ models = await loadInitialModels(desired.input.models);
173
+ if (!models || (loaded && !loaded.configuration.defaultModel))
174
+ throw new InstallationError("invalid_configuration", "Model settings and an enabled default are required.");
175
+ if (loaded && loaded.configuration.defaultModel)
176
+ models.configuration = {
177
+ ...loaded.configuration,
178
+ defaultModel: loaded.configuration.defaultModel,
179
+ baseUrl: models.configuration.baseUrl,
180
+ };
181
+ // Check the packaged helper and native validation before changing retained service files.
182
+ if (scopes.models)
183
+ await configureStoppedRuntimeModels({
184
+ image: state.input.runtimeImage,
185
+ volume: names.volume,
186
+ configuration: models.configuration,
187
+ credential: models.credential,
188
+ apply: false,
189
+ });
190
+ // Startup already requires this exact prepared record. An interrupted mutation must not announce readiness.
191
+ await writePrivate(prepared, JSON.stringify({
192
+ ownerId: state.ownerId,
193
+ settingsPending: desired.fingerprint,
194
+ settingsCandidate: resolve(configFile),
195
+ ...(checked.reapply ? { settingsReapply: checked.reapply } : {}),
196
+ }));
197
+ let stage = "model gateway configuration";
198
+ try {
199
+ if (scopes.models) {
200
+ await prepareModelGateway(directory, { ...state, input: desired.input }, { replaceConfiguration: true });
201
+ if (gateway && loaded)
202
+ try {
203
+ stage = "model credential permissions";
204
+ await compose(directory, [
205
+ "up",
206
+ "-d",
207
+ "--wait",
208
+ "--wait-timeout",
209
+ "120",
210
+ "models",
211
+ ]);
212
+ await setRuntimeCredentialModels({
213
+ origin: `https://127.0.0.1:${String(gateway.port)}`,
214
+ masterKeyFile: join(directory, "private/models/master-key"),
215
+ keyFile: join(directory, "private/models/runtime-key"),
216
+ caFile: join(directory, "private/management-ca.pem"),
217
+ models: loaded.configuration.models
218
+ .filter((model) => model.enabled)
219
+ .map((model) => model.id),
220
+ });
221
+ }
222
+ finally {
223
+ await compose(directory, ["stop", "models", "models-database"]);
224
+ }
225
+ stage = "native model configuration";
226
+ await configureStoppedRuntimeModels({
227
+ image: state.input.runtimeImage,
228
+ volume: names.volume,
229
+ configuration: models.configuration,
230
+ credential: models.credential,
231
+ apply: true,
232
+ });
233
+ }
234
+ stage = "Connections configuration";
235
+ if (scopes.connections &&
236
+ (state.input.connections || desired.input.connections))
237
+ await applyConnectionSettings(directory, { ...state, input: desired.input }, checked.previousEndpoint, desired.connectorCredentialFile);
238
+ stage = "accepted settings";
239
+ if (scopes.models)
240
+ await writePrivate(join(directory, "private/model-bootstrap.json"), JSON.stringify(models));
241
+ await writePrivate(join(directory, "packs.json"), JSON.stringify(desired.packSelection));
242
+ await writePrivate(join(directory, "installation.json"), JSON.stringify({ ...state, input: desired.input }, null, 2));
243
+ await writePrivate(join(directory, "inputs.sha256"), desired.fingerprint);
244
+ await writePrivate(join(directory, "settings.json"), JSON.stringify({
245
+ ...resolveConfigurationInputs(desired.config, dirname(resolve(configFile))),
246
+ stateDirectory: directory,
247
+ }, null, 2));
248
+ await writePrivate(prepared, JSON.stringify({ ownerId: state.ownerId }));
249
+ return { state: "configured", restartRequired: true, directory };
250
+ }
251
+ catch (error) {
252
+ throw new InstallationError("unavailable", `Settings were not confirmed during ${stage}; the installation remains stopped. Run configure --directory again to review and explicitly resume the same change. The gateway key is observed first and matching native settings are not repeated.${error instanceof ModelConfigurationError ? ` Native result: ${error.code}.` : ""}`);
253
+ }
254
+ });
255
+ }
@@ -0,0 +1,23 @@
1
+ import { openPack } from "../packs/source.js";
2
+ /** Draft feedback only; preview/apply still revalidate real inputs and native state. */
3
+ export async function packRequirements(config) {
4
+ const issues = [];
5
+ const members = new Set();
6
+ for (const selected of config.packs) {
7
+ const pack = await openPack(selected.directory);
8
+ for (const id of selected.members) {
9
+ const member = pack.manifest.members.find((entry) => entry.id === id);
10
+ if (!member || members.has(id)) {
11
+ issues.push(`Pack agent ${id} must exist and be selected only once.`);
12
+ continue;
13
+ }
14
+ members.add(id);
15
+ if (member.requirements.model !== "none" && !config.models)
16
+ issues.push(`${id} requires Models to be configured.`);
17
+ if (member.requirements.connections.length &&
18
+ (config.connections.mode === "disabled" || !selected.bindingsFile))
19
+ issues.push(`${id} requires Connections and account bindings.`);
20
+ }
21
+ }
22
+ return issues;
23
+ }