@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,217 @@
1
+ import { verifyReleaseTool } from "./files.js";
2
+ import { hostedOidc, hostedConnections } from "../cloud/registration.js";
3
+ import { dirname, resolve, join } from "node:path";
4
+ import { loadGatewayConfiguration } from "../deployment/model-gateway.js";
5
+ import { createServer } from "node:net";
6
+ import { releaseSchema } from "../release/definition.js";
7
+ import { parseLocalInput, } from "../deployment/configuration.js";
8
+ import { loadInitialModels } from "../deployment/models.js";
9
+ import { loadInitialConnections, readInitialConnectionToken, } from "../deployment/connections.js";
10
+ import { readTeamMaterials } from "../deployment/team.js";
11
+ import { installationSchema } from "./configuration.js";
12
+ import { fingerprint, readInputFile, readJson } from "./files.js";
13
+ import { InstallationError } from "./errors.js";
14
+ import { openPack } from "../packs/source.js";
15
+ export async function allocatePorts() {
16
+ const servers = Array.from({ length: 7 }, () => createServer());
17
+ try {
18
+ const ports = [];
19
+ for (const server of servers) {
20
+ await new Promise((resolve, reject) => {
21
+ server.once("error", reject);
22
+ server.listen(0, "127.0.0.1", resolve);
23
+ });
24
+ const address = server.address();
25
+ if (!address || typeof address === "string")
26
+ throw new InstallationError("unavailable", "Cannot allocate local ports.");
27
+ ports.push(address.port);
28
+ }
29
+ return ports;
30
+ }
31
+ finally {
32
+ await Promise.all(servers
33
+ .filter((s) => s.listening)
34
+ .map((s) => new Promise((resolve, reject) => s.close((error) => {
35
+ if (error)
36
+ reject(error);
37
+ else
38
+ resolve();
39
+ }))));
40
+ }
41
+ }
42
+ /** Resolve files against their owning document; validate before any allocation. */
43
+ export async function resolveInstallation(configFile, internalPorts) {
44
+ const config = installationSchema.parse(await readJson(configFile));
45
+ const base = dirname(resolve(configFile));
46
+ const path = (value) => resolve(base, value);
47
+ const releasePath = path(config.releaseFile);
48
+ const release = releaseSchema.parse(await readJson(releasePath));
49
+ if (!release.platforms.some((platform) => platform === `${process.platform}-${process.arch}`))
50
+ throw new InstallationError("unsupported_platform", "This release does not support this host platform.");
51
+ const toolBase = dirname(releasePath);
52
+ const cli = resolve(toolBase, release.tools.openshell.cli.file);
53
+ const gateway = resolve(toolBase, release.tools.openshell.gateway.file);
54
+ await verifyReleaseTool(cli, release.tools.openshell.cli.sha256);
55
+ await verifyReleaseTool(gateway, release.tools.openshell.gateway.sha256);
56
+ const [controller, management, native, nativeWidgets, database, browser, models,] = internalPorts;
57
+ const inputs = {};
58
+ async function inputFile(value, secret) {
59
+ const filename = path(value);
60
+ inputs[filename] = fingerprint(await readInputFile(filename, secret));
61
+ return filename;
62
+ }
63
+ const exposure = config.exposure;
64
+ const access = config.access.mode === "hosted"
65
+ ? {
66
+ ...config.access,
67
+ ...(await hostedOidc(await inputFile(config.access.registrationFile, true))),
68
+ }
69
+ : config.access;
70
+ const cloudConnections = await hostedConnections(config, configFile);
71
+ const input = parseLocalInput({
72
+ name: config.name,
73
+ administratorName: access.administratorName,
74
+ runtimeImage: release.images.gateway,
75
+ companionImage: release.images.companion,
76
+ ...(config.browser.enabled ? { relayImage: release.images.relay } : {}),
77
+ openshellCli: cli,
78
+ openshellGateway: gateway,
79
+ openshellClientImage: release.images.openshellClient,
80
+ cpu: config.resources.runtime.cpu,
81
+ memory: config.resources.runtime.memory,
82
+ ports: {
83
+ controller,
84
+ management,
85
+ native,
86
+ nativeWidgets,
87
+ database,
88
+ application: exposure.mode === "local"
89
+ ? exposure.applicationPort
90
+ : Number(new URL(exposure.applicationOrigin).port || "443"),
91
+ widgets: exposure.mode === "local"
92
+ ? exposure.widgetPort
93
+ : Number(new URL(exposure.widgetOrigin).port || "443"),
94
+ },
95
+ ...(config.browser.enabled
96
+ ? {
97
+ browser: {
98
+ image: release.images.browser?.chromium,
99
+ nodeImage: release.images.browser?.node,
100
+ dnsImage: release.images.browser?.dns,
101
+ egressImage: release.images.browser?.egress,
102
+ port: browser,
103
+ },
104
+ }
105
+ : {}),
106
+ team: {
107
+ ...(exposure.mode === "https"
108
+ ? {
109
+ origin: exposure.applicationOrigin,
110
+ widgetOrigin: exposure.widgetOrigin,
111
+ certificateFile: await inputFile(exposure.certificateFile, false),
112
+ keyFile: await inputFile(exposure.keyFile, true),
113
+ }
114
+ : {
115
+ origin: `http://127.0.0.1:${String(exposure.applicationPort)}`,
116
+ widgetOrigin: `http://127.0.0.1:${String(exposure.widgetPort)}`,
117
+ }),
118
+ issuer: access.issuer,
119
+ clientId: access.clientId,
120
+ clientSecretFile: await inputFile(access.clientSecretFile, true),
121
+ ...("administratorSubject" in access && access.administratorSubject
122
+ ? {
123
+ administratorSubject: access.administratorSubject,
124
+ administratorEmail: access.administratorEmail,
125
+ }
126
+ : {}),
127
+ },
128
+ ...(config.models.mode === "external"
129
+ ? {
130
+ models: {
131
+ configurationFile: await inputFile(config.models.configurationFile, false),
132
+ runtimeKeyFile: await inputFile(config.models.credentialFile, true),
133
+ ...(config.models.caFile
134
+ ? { caFile: await inputFile(config.models.caFile, false) }
135
+ : {}),
136
+ },
137
+ }
138
+ : {}),
139
+ ...(config.models.mode === "litellm"
140
+ ? {
141
+ modelGateway: {
142
+ configurationFile: await inputFile(config.models.configurationFile, false),
143
+ upstreamEnvironmentFile: await inputFile(config.models.upstreamEnvironmentFile, true),
144
+ image: release.images.models,
145
+ port: models,
146
+ },
147
+ models: {
148
+ configurationFile: join(path(config.stateDirectory), "private/models/native.json"),
149
+ runtimeKeyFile: join(path(config.stateDirectory), "private/models/runtime-key"),
150
+ caFile: join(path(config.stateDirectory), "private/management-ca.pem"),
151
+ },
152
+ }
153
+ : {}),
154
+ ...(cloudConnections
155
+ ? {
156
+ connections: {
157
+ mode: "external",
158
+ brokerUrl: cloudConnections.brokerUrl,
159
+ managementKeyFile: await inputFile(cloudConnections.managementKeyFile, true),
160
+ },
161
+ }
162
+ : {}),
163
+ });
164
+ if (input.modelGateway)
165
+ await loadGatewayConfiguration(input.modelGateway.configurationFile, input.modelGateway.upstreamEnvironmentFile);
166
+ else
167
+ await loadInitialModels(input.models);
168
+ await readTeamMaterials(input.team);
169
+ await loadInitialConnections(input.connections);
170
+ const connectorCredentialFile = cloudConnections
171
+ ? await inputFile(cloudConnections.credentialFile, true)
172
+ : undefined;
173
+ if (connectorCredentialFile)
174
+ await readInitialConnectionToken(connectorCredentialFile);
175
+ const members = new Set();
176
+ const packs = [];
177
+ for (const selected of config.packs) {
178
+ const pack = await openPack(path(selected.directory));
179
+ for (const id of selected.members) {
180
+ const member = pack.manifest.members.find((m) => m.id === id);
181
+ if (!member || members.has(id))
182
+ throw new InstallationError("invalid_configuration", "Pack member selections must exist and have unique agent IDs.");
183
+ members.add(id);
184
+ if (member.requirements.connections.length &&
185
+ (config.connections.mode === "disabled" || !selected.bindingsFile))
186
+ throw new InstallationError("invalid_configuration", "This pack requires Connections and explicit account bindings.");
187
+ }
188
+ const bindingsFile = selected.bindingsFile
189
+ ? await inputFile(selected.bindingsFile, true)
190
+ : undefined;
191
+ inputs[`pack:${pack.root}`] = pack.digest;
192
+ packs.push({
193
+ directory: pack.root,
194
+ digest: pack.digest,
195
+ members: selected.members,
196
+ ...(bindingsFile
197
+ ? { bindingsFile, bindingsDigest: inputs[bindingsFile] }
198
+ : {}),
199
+ });
200
+ }
201
+ const packSelection = {
202
+ packs,
203
+ ...(config.packOperator
204
+ ? { python: path(config.packOperator.pythonExecutable) }
205
+ : {}),
206
+ };
207
+ return {
208
+ config,
209
+ release,
210
+ input,
211
+ stateDirectory: path(config.stateDirectory),
212
+ connectorCredentialFile,
213
+ packSelection,
214
+ fingerprint: fingerprint(JSON.stringify({ config, release, inputs, releasePath })),
215
+ internalPorts,
216
+ };
217
+ }
@@ -0,0 +1,87 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmod, copyFile, mkdir, open, rename, rm, writeFile, } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { releaseSchema } from "../release/definition.js";
5
+ import { readJson, verifyReleaseTool } from "./files.js";
6
+ import { InstallationError } from "./errors.js";
7
+ /** Installation-owned paths survive npm cache cleanup and later CLI updates. */
8
+ export async function retainRuntime(source, directory) {
9
+ const release = releaseSchema.parse(await readJson(source));
10
+ const target = join(directory, "runtime");
11
+ await mkdir(join(target, "tools"), { recursive: true, mode: 0o700 });
12
+ for (const name of ["cli", "gateway"]) {
13
+ const tool = release.tools.openshell[name];
14
+ const original = resolve(dirname(source), tool.file);
15
+ tool.file = `tools/openshell${name === "gateway" ? "-gateway" : ""}`;
16
+ try {
17
+ await verifyReleaseTool(original, tool.sha256);
18
+ await copyFile(original, join(target, tool.file));
19
+ await chmod(join(target, tool.file), 0o700);
20
+ }
21
+ catch (error) {
22
+ if (!(error instanceof Error &&
23
+ "code" in error &&
24
+ error.code === "ENOENT" &&
25
+ tool.url))
26
+ throw error;
27
+ }
28
+ }
29
+ await writeFile(join(target, "release.json"), JSON.stringify(release, null, 2) + "\n", { mode: 0o600, flag: "wx" });
30
+ return "./runtime/release.json";
31
+ }
32
+ /** Download only absent tools; corrupt or replaced executables always fail verification. */
33
+ export async function acquireRuntimeTools(releaseFile, options) {
34
+ const release = releaseSchema.parse(await readJson(releaseFile));
35
+ for (const name of ["cli", "gateway"]) {
36
+ options.signal?.throwIfAborted();
37
+ const tool = release.tools.openshell[name];
38
+ const file = resolve(dirname(releaseFile), tool.file);
39
+ try {
40
+ await verifyReleaseTool(file, tool.sha256);
41
+ continue;
42
+ }
43
+ catch (error) {
44
+ if (!(error instanceof Error &&
45
+ "code" in error &&
46
+ error.code === "ENOENT"))
47
+ throw error;
48
+ }
49
+ if (!options.acquire || !tool.url)
50
+ throw new InstallationError("release_mismatch", `The OpenShell ${name} is missing. ${tool.url ? "Run configure to download it." : "This development runtime requires its locally prepared tools."}`);
51
+ options.report?.(`Downloading OpenShell ${name} ${release.tools.openshell.version}`);
52
+ const temporary = `${file}.${randomUUID()}.part`;
53
+ const signal = AbortSignal.any([
54
+ AbortSignal.timeout(5 * 60_000),
55
+ ...(options.signal ? [options.signal] : []),
56
+ ]);
57
+ await mkdir(dirname(file), { recursive: true, mode: 0o700 });
58
+ try {
59
+ const response = await fetch(tool.url, { signal }).catch(() => {
60
+ options.signal?.throwIfAborted();
61
+ throw new InstallationError("unavailable", `Could not download OpenShell ${name}. Check network access and retry configure.`);
62
+ });
63
+ if (!response.ok ||
64
+ !response.body ||
65
+ new URL(response.url).protocol !== "https:")
66
+ throw new InstallationError("unavailable", `Could not download OpenShell ${name}. Check network access and retry configure.`);
67
+ const output = await open(temporary, "wx", 0o700);
68
+ try {
69
+ let size = 0;
70
+ for await (const chunk of response.body) {
71
+ size += chunk.byteLength;
72
+ if (size > 256 * 1024 * 1024)
73
+ throw new InstallationError("release_mismatch", "Runtime tool download exceeds its size limit.");
74
+ await output.writeFile(chunk);
75
+ }
76
+ }
77
+ finally {
78
+ await output.close();
79
+ }
80
+ await verifyReleaseTool(temporary, tool.sha256);
81
+ await rename(temporary, file);
82
+ }
83
+ finally {
84
+ await rm(temporary, { force: true });
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,95 @@
1
+ import { openPack } from "../packs/source.js";
2
+ import { retainRuntime } from "./runtime.js";
3
+ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { readInputFile } from "./files.js";
6
+ import { installationSchema, } from "./configuration.js";
7
+ /** Unsaved generated inputs stay in memory until the user accepts the preview. */
8
+ export class SetupInputs {
9
+ directory;
10
+ files = new Map();
11
+ constructor(directory) {
12
+ this.directory = directory;
13
+ }
14
+ async readJson(path) {
15
+ const bytes = this.files.get(path) ?? (await readInputFile(path));
16
+ return JSON.parse(bytes.toString("utf8"));
17
+ }
18
+ set(name, value) {
19
+ const path = resolve(this.directory, "secrets", name);
20
+ this.files.set(path, Buffer.from(value));
21
+ return path;
22
+ }
23
+ }
24
+ /** Copy credentials into the new private installation directory, never into its JSON. */
25
+ export async function saveConfiguration(directory, value, inputs, retained = false) {
26
+ const config = installationSchema.parse(value);
27
+ const files = new Map();
28
+ async function secret(source, name) {
29
+ const path = join("secrets", name);
30
+ files.set(path, inputs?.files.get(source) ?? (await readInputFile(source, true)));
31
+ return `./${path}`;
32
+ }
33
+ const modelConfiguration = inputs?.files.get(config.models.configurationFile) ??
34
+ (await readInputFile(config.models.configurationFile));
35
+ files.set("models.json", modelConfiguration);
36
+ config.models.configurationFile = "./models.json";
37
+ if (!retained && config.access.mode === "oidc")
38
+ config.access.clientSecretFile = await secret(config.access.clientSecretFile, "oidc-client-secret");
39
+ if (!retained && config.exposure.mode === "https")
40
+ config.exposure.keyFile = await secret(config.exposure.keyFile, "tls-key.pem");
41
+ if (config.models.mode === "external")
42
+ config.models.credentialFile = await secret(config.models.credentialFile, "model-key");
43
+ if (config.models.mode === "litellm")
44
+ config.models.upstreamEnvironmentFile = await secret(config.models.upstreamEnvironmentFile, "models.env");
45
+ for (const [index, pack] of config.packs.entries())
46
+ if (pack.bindingsFile)
47
+ pack.bindingsFile = await secret(pack.bindingsFile, `pack-${String(index)}-bindings.json`);
48
+ // Exclusive creation also protects against another installer winning the same path.
49
+ await mkdir(directory, { mode: 0o700 });
50
+ try {
51
+ if (!retained)
52
+ config.releaseFile = await retainRuntime(resolve(config.releaseFile), directory);
53
+ if (config.packs.length)
54
+ await mkdir(resolve(directory, config.stateDirectory), {
55
+ recursive: true,
56
+ mode: 0o700,
57
+ });
58
+ for (const pack of config.packs) {
59
+ const source = await openPack(pack.directory);
60
+ const destination = join(resolve(directory, config.stateDirectory), "pack-sources", source.digest);
61
+ if (resolve(pack.directory) !== destination) {
62
+ await cp(source.root, destination, {
63
+ recursive: true,
64
+ errorOnExist: true,
65
+ force: false,
66
+ }).catch(async (error) => {
67
+ if (!(error instanceof Error &&
68
+ "code" in error &&
69
+ error.code === "EEXIST"))
70
+ throw error;
71
+ if ((await openPack(destination)).digest !== source.digest)
72
+ throw error;
73
+ });
74
+ pack.directory = destination;
75
+ }
76
+ }
77
+ if (files.size)
78
+ await mkdir(join(directory, "secrets"), { mode: 0o700 });
79
+ for (const [path, bytes] of files)
80
+ await writeFile(join(directory, path), bytes, {
81
+ mode: 0o600,
82
+ flag: "wx",
83
+ });
84
+ const path = join(directory, "installation.json");
85
+ await writeFile(path, JSON.stringify(config, null, 2) + "\n", {
86
+ mode: 0o600,
87
+ flag: "wx",
88
+ });
89
+ return path;
90
+ }
91
+ catch (error) {
92
+ await rm(directory, { recursive: true, force: true });
93
+ throw error;
94
+ }
95
+ }
@@ -0,0 +1,91 @@
1
+ import { recipeModelRoutes } from "./models.js";
2
+ import { resolve } from "node:path";
3
+ import { releaseSchema } from "../release/definition.js";
4
+ import { readJson } from "./files.js";
5
+ import { InstallationError } from "./errors.js";
6
+ import { installationCatalog, readRecipe, validateRecipe, } from "./recipes/catalog.js";
7
+ import { defaultCloudUrl } from "../cloud/url.js";
8
+ export async function readRuntime(releaseFile) {
9
+ releaseFile = resolve(releaseFile);
10
+ const release = releaseSchema.parse(await readJson(releaseFile));
11
+ if (!release.platforms.some((platform) => platform === `${process.platform}-${process.arch}`))
12
+ throw new InstallationError("unsupported_platform", "This runtime release does not support this host platform. No fallback protection mode is available.");
13
+ return { release, releaseFile };
14
+ }
15
+ export async function setupContext(options, retainedRelease) {
16
+ const catalog = await installationCatalog();
17
+ let recipe = catalog.recipes.find((recipe) => recipe.id === options.recipe);
18
+ if (!retainedRelease && !recipe) {
19
+ if (!options.recipe)
20
+ throw new InstallationError("invalid_configuration", "Choose a recipe from clawscarf recipes or supply a recipe file.");
21
+ if (!options.recipe.endsWith(".json"))
22
+ throw new InstallationError("invalid_configuration", `Unknown recipe: ${options.recipe}`);
23
+ recipe = await readRecipe(resolve(options.recipe));
24
+ validateRecipe(recipe, catalog);
25
+ const selectedId = recipe.id;
26
+ catalog.recipes = [
27
+ recipe,
28
+ ...catalog.recipes.filter((item) => item.id !== selectedId),
29
+ ];
30
+ }
31
+ const file = retainedRelease ?? recipe?.runtime;
32
+ if (!file)
33
+ throw new InstallationError("invalid_configuration", "The recipe must select a runtime release.");
34
+ return {
35
+ ...catalog,
36
+ ...(await readRuntime(file)),
37
+ recipeId: recipe?.id,
38
+ cloudUrl: options.cloudUrl ?? defaultCloudUrl,
39
+ };
40
+ }
41
+ /** Both the menu and noninteractive configuration start from these exact defaults. */
42
+ export function recipeConfiguration(context, recipeId) {
43
+ const recipe = context.recipes.find((recipe) => recipe.id === recipeId);
44
+ if (!recipe)
45
+ throw new InstallationError("invalid_configuration", `Unknown recipe: ${recipeId}`);
46
+ return {
47
+ schemaVersion: 1,
48
+ name: "team",
49
+ releaseFile: context.releaseFile,
50
+ stateDirectory: "./state",
51
+ exposure: { mode: "local", applicationPort: 18800, widgetPort: 18802 },
52
+ access: {
53
+ mode: "hosted",
54
+ administratorName: "Administrator",
55
+ registrationFile: "./secrets/hosted-login.json",
56
+ cloudUrl: context.cloudUrl,
57
+ },
58
+ resources: structuredClone(recipe.defaults.resources),
59
+ browser: structuredClone(recipe.defaults.browser),
60
+ connections: {
61
+ mode: recipe.defaults.connections?.enabled ? "hosted" : "disabled",
62
+ cloudUrl: context.cloudUrl,
63
+ registrationFile: "./secrets/connections-registration.json",
64
+ },
65
+ packs: recipe.packs.map((pack) => ({
66
+ directory: packDirectory(context, pack.id),
67
+ members: [...pack.members],
68
+ })),
69
+ recipe: {
70
+ id: recipe.id,
71
+ version: recipe.version,
72
+ release: context.release.version,
73
+ },
74
+ };
75
+ }
76
+ export function recipeModelFile(models, inputs, catalog) {
77
+ return inputs.set("recipe-models.json", JSON.stringify(recipeModelRoutes(models, catalog), null, 2) + "\n");
78
+ }
79
+ export function assertReleaseCapabilities(context, config) {
80
+ if (config.browser.enabled &&
81
+ (!context.release.images.browser || !context.release.images.relay))
82
+ throw new InstallationError("invalid_configuration", "This release does not include browser images.");
83
+ if (config.models.mode === "litellm" && !context.release.images.models)
84
+ throw new InstallationError("invalid_configuration", "This release does not include LiteLLM.");
85
+ }
86
+ function packDirectory(context, id) {
87
+ const pack = context.packs.find((item) => item.id === id);
88
+ if (!pack)
89
+ throw new InstallationError("invalid_configuration", `Unknown pack: ${id}`);
90
+ return pack.directory;
91
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ import { modelSchema } from "./configuration.js";
3
+ export const modelCatalogSchema = z
4
+ .array(z.strictObject({
5
+ provider: z.string().min(1),
6
+ model: modelSchema.extend({ route: modelSchema.shape.route.unwrap() }),
7
+ reasoningLevels: z.array(z.enum(["low", "medium", "high"])),
8
+ }))
9
+ .max(512);
@@ -0,0 +1,184 @@
1
+ import { OperatorError } from "../errors.js";
2
+ import { z } from "zod";
3
+ export const modelSchema = z.strictObject({
4
+ id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._/-]{0,159}$/),
5
+ name: z.string().min(1).max(200),
6
+ api: z.enum(["openai-completions", "openai-responses"]).optional(),
7
+ enabled: z.boolean(),
8
+ contextWindow: z.number().int().positive(),
9
+ maxTokens: z.number().int().positive(),
10
+ reasoning: z.boolean(),
11
+ tools: z.boolean(),
12
+ input: z.array(z.enum(["text", "image"])).min(1),
13
+ route: z
14
+ .strictObject({
15
+ model: z.string().min(1).max(200),
16
+ apiKeyEnv: z.string().regex(/^[A-Z][A-Z0-9_]+$/),
17
+ apiBase: z.url().optional(),
18
+ })
19
+ .optional(),
20
+ });
21
+ const thinkingDefault = z.enum(["low", "medium", "high"]);
22
+ export const gatewayRoutesSchema = z.strictObject({
23
+ models: z.array(modelSchema).min(1).max(512),
24
+ defaultModel: z.string().min(1),
25
+ thinkingDefault: thinkingDefault.optional(),
26
+ });
27
+ export const configurationSchema = z
28
+ .discriminatedUnion("mode", [
29
+ z.strictObject({ mode: z.literal("disabled") }),
30
+ z.strictObject({
31
+ mode: z.enum(["external", "litellm"]),
32
+ baseUrl: z.url(),
33
+ models: z.array(modelSchema).min(1).max(512),
34
+ defaultModel: z.string().nullable(),
35
+ thinkingDefault: thinkingDefault.optional(),
36
+ }),
37
+ ])
38
+ .superRefine((value, context) => {
39
+ if (value.mode === "disabled")
40
+ return;
41
+ const url = new URL(value.baseUrl);
42
+ if (url.username ||
43
+ url.password ||
44
+ url.search ||
45
+ url.hash ||
46
+ !(url.protocol === "https:" ||
47
+ (url.protocol === "http:" &&
48
+ ["127.0.0.1", "[::1]", "localhost"].includes(url.hostname))))
49
+ context.addIssue({
50
+ code: "custom",
51
+ message: "Use HTTPS, or HTTP only on loopback, without URL credentials.",
52
+ });
53
+ const ids = new Set(value.models.map((model) => model.id));
54
+ if (ids.size !== value.models.length ||
55
+ !value.models.some((model) => model.enabled))
56
+ context.addIssue({
57
+ code: "custom",
58
+ message: "Require unique models and at least one enabled model.",
59
+ });
60
+ if (value.defaultModel !== null &&
61
+ !value.models.some((model) => model.enabled && model.id === value.defaultModel))
62
+ context.addIssue({
63
+ code: "custom",
64
+ message: "The default must be an enabled model.",
65
+ });
66
+ if (value.mode === "litellm" &&
67
+ value.models.some((model) => model.enabled && !model.route))
68
+ context.addIssue({
69
+ code: "custom",
70
+ message: "Every enabled LiteLLM model needs an upstream route.",
71
+ });
72
+ });
73
+ export const tokenVariable = "CLAWSCARF_MODEL_TOKEN";
74
+ export function nativeModelProvider(config) {
75
+ return {
76
+ baseUrl: config.baseUrl,
77
+ api: "openai-completions",
78
+ agentRuntime: { id: "openclaw" },
79
+ apiKey: {
80
+ source: "env",
81
+ provider: "clawscarf-models",
82
+ id: tokenVariable,
83
+ },
84
+ models: config.models
85
+ .filter((model) => model.enabled)
86
+ .map((model) => ({
87
+ id: model.id,
88
+ name: model.name,
89
+ ...(model.api ? { api: model.api } : {}),
90
+ input: model.input,
91
+ reasoning: model.reasoning,
92
+ contextWindow: model.contextWindow,
93
+ maxTokens: model.maxTokens,
94
+ compat: {
95
+ supportsTools: model.tools,
96
+ supportsUsageInStreaming: true,
97
+ // Permit explicit strict:false on compatible routes; preserve optional tool arguments.
98
+ supportsStrictMode: true,
99
+ },
100
+ })),
101
+ };
102
+ }
103
+ export function nativeAssignments(config) {
104
+ if (config.mode === "disabled")
105
+ return [];
106
+ const defaults = nativeModelDefaults(config);
107
+ const assignments = [
108
+ {
109
+ path: "secrets.providers.clawscarf-models",
110
+ value: { source: "env", allowlist: [tokenVariable] },
111
+ },
112
+ {
113
+ path: "models.providers.clawscarf",
114
+ value: nativeModelProvider(config),
115
+ },
116
+ ];
117
+ if (defaults.model.primary !== undefined)
118
+ assignments.push({
119
+ path: "agents.defaults.model.primary",
120
+ value: defaults.model.primary,
121
+ });
122
+ if (defaults.thinkingDefault)
123
+ assignments.push({
124
+ path: "agents.defaults.thinkingDefault",
125
+ value: defaults.thinkingDefault,
126
+ });
127
+ return assignments;
128
+ }
129
+ export function nativeModelDefaults(config) {
130
+ return {
131
+ model: {
132
+ ...(config.defaultModel === null
133
+ ? {}
134
+ : { primary: `clawscarf/${config.defaultModel}` }),
135
+ },
136
+ ...(config.thinkingDefault
137
+ ? { thinkingDefault: config.thinkingDefault }
138
+ : {}),
139
+ };
140
+ }
141
+ export function liteLlmConfiguration(config) {
142
+ if (config.mode !== "litellm")
143
+ throw new OperatorError("Select LiteLLM mode before rendering its configuration.");
144
+ return {
145
+ model_list: config.models
146
+ .filter((model) => model.enabled)
147
+ .map((model) => {
148
+ if (!model.route)
149
+ throw new OperatorError("A LiteLLM route is required.");
150
+ return {
151
+ model_name: model.id,
152
+ litellm_params: {
153
+ model: model.route.model,
154
+ api_key: `os.environ/${model.route.apiKeyEnv}`,
155
+ ...(model.route.apiBase ? { api_base: model.route.apiBase } : {}),
156
+ num_retries: 0,
157
+ timeout: 300,
158
+ },
159
+ model_info: {
160
+ max_input_tokens: model.contextWindow,
161
+ max_output_tokens: model.maxTokens,
162
+ supports_function_calling: model.tools,
163
+ supports_reasoning: model.reasoning,
164
+ },
165
+ };
166
+ }),
167
+ litellm_settings: {
168
+ num_retries: 0,
169
+ request_timeout: 300,
170
+ turn_off_message_logging: true,
171
+ set_verbose: false,
172
+ drop_params: false,
173
+ telemetry: false,
174
+ },
175
+ router_settings: { num_retries: 0, disable_cooldowns: true },
176
+ general_settings: {
177
+ master_key: "os.environ/LITELLM_MASTER_KEY",
178
+ database_url: "os.environ/DATABASE_URL",
179
+ disable_spend_logs: true,
180
+ disable_error_logs: true,
181
+ store_model_in_db: false,
182
+ },
183
+ };
184
+ }