@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,298 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { verifyRuntimeImage } from "./images.js";
3
+ import { mkdtemp, writeFile, rm, chmod } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { isDeepStrictEqual } from "node:util";
7
+ import { setTimeout as delay } from "node:timers/promises";
8
+ import { z } from "zod";
9
+ import { parseLocalInput } from "./configuration.js";
10
+ import { readState, resourceNames, withInstallationLock, writePrivate, } from "./state.js";
11
+ import { readUpgrade, snapshotSchema } from "./upgrade-state.js";
12
+ import { runtimeManager } from "./runtime.js";
13
+ import { verifyRuntimeBinding } from "./runtime-binding.js";
14
+ import { verifyLocalNetworks } from "./networks.js";
15
+ import { verifyLocalExecutables } from "./preflight.js";
16
+ import { LocalSetupError, run } from "./process.js";
17
+ function refuse(detail) {
18
+ throw new LocalSetupError("upgrade_refused", detail);
19
+ }
20
+ function uncertain() {
21
+ throw new LocalSetupError("upgrade_outcome_unknown", "The upgrade has an unconfirmed external effect. Inspect its private state and controller before resuming; it will not repeat allocation or deletion.");
22
+ }
23
+ /** Requires exclusive operator control and the installation's own controller running. */
24
+ export async function upgradeLocal(directoryInput, image, python, report, command = run) {
25
+ const directory = resolve(directoryInput);
26
+ await withInstallationLock(directory, async () => {
27
+ const state = await readState(directory);
28
+ await verifyRuntimeImage(image, command);
29
+ const next = {
30
+ ...state,
31
+ input: parseLocalInput({ ...state.input, runtimeImage: image }),
32
+ };
33
+ const names = resourceNames(state);
34
+ const controller = join(directory, "controller");
35
+ if (Object.keys(process.env).some((key) => key.startsWith("OPENSHELL_")))
36
+ refuse("Unset OPENSHELL_* overrides before upgrading this installation.");
37
+ const env = {
38
+ ...process.env,
39
+ XDG_CONFIG_HOME: join(controller, "config"),
40
+ XDG_STATE_HOME: join(controller, "state"),
41
+ XDG_DATA_HOME: join(controller, "data"),
42
+ };
43
+ await verifyLocalExecutables(state);
44
+ await verifyLocalNetworks(directory, state, command);
45
+ const running = await command("docker", [
46
+ "container",
47
+ "ls",
48
+ "--quiet",
49
+ "--filter",
50
+ `label=com.docker.compose.project=${names.project}`,
51
+ "--filter",
52
+ "label=com.docker.compose.service=companion",
53
+ ]);
54
+ if (running.trim())
55
+ refuse("Stop the installation and its access companion before upgrading. Start only its private controller.");
56
+ const gate = await command("docker", [
57
+ "image",
58
+ "inspect",
59
+ "--format",
60
+ '{{index .Config.Labels "io.clawscarf.start-gate"}}',
61
+ image,
62
+ ]);
63
+ if (gate.trim() !== "1")
64
+ refuse("The selected exact runtime image does not support the replacement startup gate.");
65
+ const control = runtimeManager(directory, state, env, command);
66
+ let upgrade = await readUpgrade(directory);
67
+ if (upgrade?.stage === "complete" &&
68
+ upgrade.toImage === image &&
69
+ state.input.runtimeImage === image) {
70
+ report("This runtime upgrade is already complete.");
71
+ return;
72
+ }
73
+ if (upgrade?.stage === "complete")
74
+ upgrade = undefined;
75
+ async function rpc(action, value = {}) {
76
+ const response = z
77
+ .discriminatedUnion("ok", [
78
+ z.strictObject({ ok: z.literal(true), value: z.unknown() }),
79
+ z.strictObject({ ok: z.literal(false), code: z.string() }),
80
+ ])
81
+ .parse(JSON.parse(await command(python, [fileURLToPath(new URL("./upgrade-rpc.py", import.meta.url))], {
82
+ env,
83
+ timeout: 150000,
84
+ input: JSON.stringify({
85
+ action,
86
+ controller,
87
+ name: names.sandbox,
88
+ ownerId: state.ownerId,
89
+ ...value,
90
+ }),
91
+ })));
92
+ if (!response.ok) {
93
+ if (response.code === "global_overrides_unsupported")
94
+ refuse("This controller has global overrides. The pinned API cannot preserve hidden sandbox settings; remove neither compute nor data until that configuration has a supported upgrade path.");
95
+ if (response.code === "policy_not_loaded")
96
+ refuse("The current policy is not confirmed loaded. Reconcile it before upgrading.");
97
+ if (response.code === "runtime_command_unsupported")
98
+ refuse("The runtime uses a custom startup command. Its upgrade needs an explicit supported startup gate.");
99
+ refuse("The controller did not verify the upgrade configuration. Inspect the private upgrade record and current native state before resuming.");
100
+ }
101
+ return response.value;
102
+ }
103
+ async function save(value) {
104
+ await writePrivate(join(directory, "upgrade.json"), JSON.stringify(value) + "\n");
105
+ }
106
+ if (!upgrade) {
107
+ if (image === state.input.runtimeImage)
108
+ refuse("Choose a different exact runtime image.");
109
+ const records = await control.recorded();
110
+ const target = await control.observe();
111
+ if (!target ||
112
+ target.id !== records.receipt?.id ||
113
+ target.phase !== "Stopped")
114
+ refuse("Upgrade requires this installation's recorded runtime to be Stopped.");
115
+ const binding = await verifyRuntimeBinding(state, target, command);
116
+ const snapshot = snapshotSchema.parse(await rpc("snapshot", { id: target.id }));
117
+ upgrade = {
118
+ ownerId: state.ownerId,
119
+ generation: randomUUID(),
120
+ fromImage: state.input.runtimeImage,
121
+ toImage: image,
122
+ oldId: target.id,
123
+ oldContainerId: binding.containerId,
124
+ snapshot,
125
+ stage: "prepared",
126
+ };
127
+ await save(upgrade);
128
+ }
129
+ const operation = upgrade;
130
+ if (operation.ownerId !== state.ownerId ||
131
+ operation.toImage !== image ||
132
+ ![operation.fromImage, operation.toImage].includes(state.input.runtimeImage))
133
+ refuse("Resume the recorded upgrade with its exact image and installation.");
134
+ const advance = async (stage) => {
135
+ operation.stage = stage;
136
+ await save(operation);
137
+ };
138
+ const request = () => ({
139
+ snapshot: operation.snapshot,
140
+ id: operation.newId,
141
+ });
142
+ async function retainedVolume() {
143
+ const volume = z
144
+ .object({
145
+ Name: z.literal(names.volume),
146
+ Labels: z.record(z.string(), z.string()),
147
+ })
148
+ .parse(JSON.parse(await command("docker", [
149
+ "volume",
150
+ "inspect",
151
+ "--format",
152
+ '{"Name":{{json .Name}},"Labels":{{json .Labels}}}',
153
+ names.volume,
154
+ ])));
155
+ if (volume.Labels["clawscarf.installation"] !== state.ownerId)
156
+ refuse("The retained home volume's ownership changed.");
157
+ }
158
+ async function oldAbsent() {
159
+ if (await control.observe())
160
+ uncertain();
161
+ const ids = await command("docker", [
162
+ "container",
163
+ "ls",
164
+ "--all",
165
+ "--quiet",
166
+ "--no-trunc",
167
+ "--filter",
168
+ `id=${operation.oldContainerId}`,
169
+ ]);
170
+ if (ids.trim())
171
+ uncertain();
172
+ await retainedVolume();
173
+ }
174
+ if (operation.stage === "prepared") {
175
+ const current = snapshotSchema.parse(await rpc("snapshot", { id: operation.oldId }));
176
+ if (!isDeepStrictEqual(current, operation.snapshot))
177
+ refuse("Runtime configuration changed after the upgrade snapshot. No deletion was sent.");
178
+ await verifyRuntimeBinding({
179
+ ...state,
180
+ input: { ...state.input, runtimeImage: operation.fromImage },
181
+ }, { id: operation.oldId, name: names.sandbox }, command);
182
+ await advance("delete_sent");
183
+ report("Removing stopped compute; retaining its home volume…");
184
+ // Native deletion has no atomic UUID precondition. Never resend this request.
185
+ try {
186
+ await control.shell(["sandbox", "delete", names.sandbox]);
187
+ }
188
+ catch {
189
+ /* Reconcile absence below. */
190
+ }
191
+ }
192
+ if (operation.stage === "delete_sent") {
193
+ await oldAbsent();
194
+ await advance("deleted");
195
+ }
196
+ if (operation.stage === "deleted") {
197
+ await oldAbsent();
198
+ await advance("create_sent");
199
+ report("Creating the replacement with application startup gated…");
200
+ try {
201
+ await rpc("create", {
202
+ ...request(),
203
+ image,
204
+ generation: operation.generation,
205
+ });
206
+ }
207
+ catch {
208
+ /* Observe the unique generation; never replay create. */
209
+ }
210
+ }
211
+ if (operation.stage === "create_sent") {
212
+ const target = await control.observe();
213
+ if (!target ||
214
+ target.labels["clawscarf.upgrade"] !== operation.generation ||
215
+ target.id === operation.oldId)
216
+ uncertain();
217
+ operation.newId = target.id;
218
+ await advance("created");
219
+ }
220
+ const id = operation.newId ?? uncertain();
221
+ async function confirm() {
222
+ const target = await control.confirm(id);
223
+ if (target.labels["clawscarf.upgrade"] !== operation.generation)
224
+ refuse("The replacement generation changed.");
225
+ return target;
226
+ }
227
+ async function ready() {
228
+ const deadline = Date.now() + 90000;
229
+ while (Date.now() < deadline) {
230
+ const target = await confirm();
231
+ if (target.phase === "Ready")
232
+ return;
233
+ if (["Error", "Stopped"].includes(target.phase))
234
+ refuse("The replacement did not become ready. Inspect the controller before resuming.");
235
+ await delay(500);
236
+ }
237
+ refuse("The replacement is still starting. Resume this same upgrade after inspecting the controller.");
238
+ }
239
+ if (operation.stage === "created") {
240
+ await ready();
241
+ await verifyRuntimeBinding(next, { id, name: names.sandbox }, command);
242
+ await advance("restore_sent");
243
+ report("Restoring the current controller settings…");
244
+ }
245
+ if (operation.stage === "restore_sent") {
246
+ await rpc("restore", request());
247
+ await advance("restored");
248
+ }
249
+ if (operation.stage === "restored") {
250
+ // Next normal startup reads restored settings before launching the application.
251
+ const target = await confirm();
252
+ if (target.phase !== "Stopped")
253
+ await control.shell(["sandbox", "stop", names.sandbox]);
254
+ if ((await confirm()).phase !== "Stopped")
255
+ refuse("The replacement has not confirmed Stopped.");
256
+ await rpc("verify", request());
257
+ await advance("stopped");
258
+ }
259
+ if (operation.stage === "stopped") {
260
+ const binding = await verifyRuntimeBinding(next, { id, name: names.sandbox }, command);
261
+ if ((await confirm()).phase !== "Stopped")
262
+ refuse("Keep the replacement stopped until the upgrade commits.");
263
+ const staging = await mkdtemp(join(directory, "private", "upgrade-gate-"));
264
+ try {
265
+ const marker = join(staging, "ready");
266
+ await writeFile(marker, operation.generation + "\n", { mode: 0o644 });
267
+ await chmod(marker, 0o644);
268
+ // docker cp assigns root ownership at the container destination. No guest runs here.
269
+ await command("docker", [
270
+ "cp",
271
+ marker,
272
+ `${binding.containerId}:/etc/clawscarf-start-ready`,
273
+ ]);
274
+ }
275
+ finally {
276
+ await rm(staging, { recursive: true, force: true });
277
+ }
278
+ await advance("released");
279
+ }
280
+ if (operation.stage === "released") {
281
+ const target = await confirm();
282
+ if (target.phase !== "Stopped")
283
+ refuse("The upgraded runtime has not confirmed Stopped.");
284
+ await verifyRuntimeBinding(next, { id, name: names.sandbox }, command);
285
+ await advance("committing");
286
+ }
287
+ if (operation.stage === "committing") {
288
+ await confirm();
289
+ await verifyRuntimeBinding(next, { id, name: names.sandbox }, command);
290
+ const intent = { ownerId: state.ownerId, name: names.sandbox, image };
291
+ await writePrivate(join(directory, "runtime-create.json"), JSON.stringify(intent) + "\n");
292
+ await writePrivate(join(directory, "runtime.json"), JSON.stringify({ ...intent, id }) + "\n");
293
+ await writePrivate(join(directory, "installation.json"), JSON.stringify(next, null, 2) + "\n");
294
+ await advance("complete");
295
+ }
296
+ report("Runtime replacement complete; data retained. Stop the private controller, then start the installation to verify native administrator access. This is not a backup or data rollback.");
297
+ });
298
+ }
@@ -0,0 +1,24 @@
1
+ import { OperatorError } from "../errors.js";
2
+ import { run } from "./process.js";
3
+ export async function ensureOwnedVolume(name, ownerId) {
4
+ const listed = (await run("docker", ["volume", "ls", "--format", "{{.Name}}"]))
5
+ .trim()
6
+ .split("\n");
7
+ if (!listed.includes(name))
8
+ await run("docker", [
9
+ "volume",
10
+ "create",
11
+ "--label",
12
+ `clawscarf.installation=${ownerId}`,
13
+ name,
14
+ ]);
15
+ const label = (await run("docker", [
16
+ "volume",
17
+ "inspect",
18
+ name,
19
+ "--format",
20
+ '{{index .Labels "clawscarf.installation"}}',
21
+ ])).trim();
22
+ if (label !== ownerId)
23
+ throw new OperatorError("A volume with this name belongs to a different installation.");
24
+ }
@@ -0,0 +1,9 @@
1
+ /** Deliberate operator diagnostics only. Never wrap raw subprocess or provider messages. */
2
+ export class OperatorError extends Error {
3
+ code;
4
+ constructor(message, code = "operation_failed") {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "OperatorError";
8
+ }
9
+ }
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { compose } from "../deployment/compose.js";
3
+ /** Local operator command; no public endpoint can issue an ownership claim. */
4
+ export async function administratorSetup(directory, issue = false, verify = false) {
5
+ const output = await compose(directory, [
6
+ "exec",
7
+ "-T",
8
+ "-e",
9
+ "CLAWSCARF_ACCESS_CONFIG=/run/clawscarf/access.json",
10
+ "companion",
11
+ "node",
12
+ "services/access/runtime/setup.js",
13
+ ...(issue ? ["--issue"] : []),
14
+ ...(verify ? ["--verify"] : []),
15
+ ], issue || verify ? 60_000 : 3000);
16
+ return z
17
+ .strictObject({
18
+ complete: z.boolean(),
19
+ expiresAt: z.string().nullable(),
20
+ url: z.url().optional(),
21
+ })
22
+ .parse(JSON.parse(output));
23
+ }
@@ -0,0 +1,176 @@
1
+ import { connectionCommands } from "../connections.js";
2
+ import { peopleCommand } from "../people.js";
3
+ import { confirmDeletion, deleteInstallation } from "./delete.js";
4
+ import { writeResult } from "../output.js";
5
+ import { createDevelopmentRelease } from "../release/create.js";
6
+ import { Command, Option } from "commander";
7
+ import { z } from "zod";
8
+ import { access } from "node:fs/promises";
9
+ import { join, resolve } from "node:path";
10
+ import { installationSchema } from "./configuration.js";
11
+ import { readJson } from "./files.js";
12
+ import { startInstallation, controlInstallation, installationLogs, } from "./lifecycle.js";
13
+ import { doctorInstallation } from "./doctor.js";
14
+ import { localLogNames } from "../deployment/logs.js";
15
+ import { upgradeLocal } from "../deployment/upgrade.js";
16
+ import { installationCatalog } from "./recipes/catalog.js";
17
+ import { defaultInstallationDirectory } from "./location.js";
18
+ import { progress } from "./installer/prompts.js";
19
+ import { runConfiguration } from "./installer/run.js";
20
+ import { installationOptions } from "./options.js";
21
+ import { InstallationError } from "./errors.js";
22
+ import { administratorSetup } from "./administrator.js";
23
+ export function installationCommand() {
24
+ const program = new Command("clawscarf")
25
+ .description("Install and operate a protected OpenClaw team server.")
26
+ .option("--json", "Print machine-readable results; diagnostics go to stderr");
27
+ program.addCommand(peopleCommand(program));
28
+ const output = (value, human) => {
29
+ writeResult(program, value, human);
30
+ };
31
+ installationOptions(program.command("configure"))
32
+ .description("Create or change an installation; prompts unless --non-interactive")
33
+ .option("--directory <path>", "Installation directory", defaultInstallationDirectory)
34
+ .option("--recipe <name-or-file>", "New installation: bundled recipe name or recipe JSON file")
35
+ .option("--cloud-url <url>", "Development override: ClawScarf Cloud origin")
36
+ .option("--cloud-credential-file <file>", "Private cloud credential for unattended registration")
37
+ .option("--non-interactive", "Use explicit options without prompting")
38
+ .option("--yes", "Approve changes to an existing installation, including a restart")
39
+ .addOption(new Option("--reapply <capability>", "Explicitly restore selected managed settings on an existing installation").choices(["models", "connections"]))
40
+ .option("--start", "Start after configuration")
41
+ .option("--no-start", "Leave the installation stopped")
42
+ .action(async (options) => {
43
+ const result = await runConfiguration({
44
+ ...options,
45
+ ...program.opts(),
46
+ });
47
+ if (options.nonInteractive)
48
+ output(result);
49
+ });
50
+ program
51
+ .command("recipes")
52
+ .description("List bundled recipes, models and packs")
53
+ .action(async () => {
54
+ output(await installationCatalog());
55
+ });
56
+ program
57
+ .command("release-create")
58
+ .description("Bundle a development release from already-built component inputs")
59
+ .requiredOption("--input <file>")
60
+ .requiredOption("--output <directory>")
61
+ .action(async (options) => {
62
+ const release = await createDevelopmentRelease({
63
+ inputFile: options.input,
64
+ outputDirectory: options.output,
65
+ });
66
+ output({ version: release.version, directory: options.output });
67
+ });
68
+ withLocation(program.command("start")).action(async (options) => {
69
+ const state = await resolveLocation(options);
70
+ const result = await progress("Starting ClawScarf", (signal, report) => startInstallation(state, report, signal), program.opts());
71
+ output(result, statusText(result));
72
+ });
73
+ withLocation(program.command("administrator"))
74
+ .description("Observe or issue the private first-administrator setup link")
75
+ .option("--issue", "Replace the pending setup link; cannot reclaim an initialized server")
76
+ .action(async (options) => {
77
+ output(await administratorSetup(await resolveLocation(options), options.issue));
78
+ });
79
+ for (const action of ["status", "stop"]) {
80
+ const command = withLocation(program.command(action));
81
+ if (action === "stop")
82
+ command
83
+ .option("--delete", "Permanently delete this installation's Docker resources and data")
84
+ .option("--confirm-delete <directory>", "Unattended deletion: confirm the exact absolute installation directory")
85
+ .option("--accept-data-loss", "Unattended deletion: acknowledge permanent loss of all installation data");
86
+ command.action(async (options) => {
87
+ if (options.delete) {
88
+ await confirmDeletion(resolve(options.directory), {
89
+ ...options,
90
+ ...program.opts(),
91
+ });
92
+ const directory = await resolveLocation(options);
93
+ const result = await progress("Deleting installation", (signal) => deleteInstallation(directory, signal), program.opts());
94
+ output(result, "Installation deleted from Docker. You can now remove its installation folder.");
95
+ return;
96
+ }
97
+ if (options.confirmDelete !== undefined || options.acceptDataLoss)
98
+ throw new InstallationError("invalid_configuration", "Deletion confirmations require --delete.");
99
+ const result = await controlInstallation(await resolveLocation(options), action);
100
+ output(result, statusText(result));
101
+ });
102
+ }
103
+ withLocation(program.command("logs"))
104
+ .requiredOption("--service <name>", localLogNames.join(", "))
105
+ .action(async (options) => {
106
+ const { service } = options;
107
+ const text = await installationLogs(await resolveLocation(options), service);
108
+ if (program.opts().json)
109
+ output({ service, text });
110
+ else
111
+ process.stdout.write(text);
112
+ });
113
+ withLocation(program.command("doctor")).action(async ({ directory }) => {
114
+ const accepted = join(await resolveLocation({ directory }), "settings.json");
115
+ let config = accepted;
116
+ try {
117
+ await access(accepted);
118
+ }
119
+ catch (error) {
120
+ if (!(error instanceof Error &&
121
+ "code" in error &&
122
+ error.code === "ENOENT"))
123
+ throw error;
124
+ config = join(resolve(directory), "installation.json");
125
+ }
126
+ output(await doctorInstallation(config));
127
+ });
128
+ withLocation(program.command("upgrade"))
129
+ .requiredOption("--runtime-image <digest>")
130
+ .requiredOption("--python <executable>")
131
+ .requiredOption("--yes")
132
+ .action(async (options) => {
133
+ await upgradeLocal(await resolveLocation(options), options.runtimeImage, options.python, (message) => {
134
+ process.stderr.write(message + "\n");
135
+ });
136
+ output({ state: "upgraded" }, "Upgrade completed.");
137
+ });
138
+ const connections = program
139
+ .command("connections")
140
+ .description("Manage connected accounts and their agent access");
141
+ connectionCommands(connections, program);
142
+ return program;
143
+ }
144
+ function statusText(status) {
145
+ const lines = [
146
+ `Server: ${status.state}`,
147
+ `Ready: ${status.ready ? "Yes" : "No"}`,
148
+ ];
149
+ if ("administrator" in status) {
150
+ lines.push(`Administrator: ${status.administrator}`);
151
+ if (status.administrator === "pending")
152
+ lines.push("Complete administrator setup with clawscarf administrator --issue and your installation directory.");
153
+ for (const pack of status.packs)
154
+ lines.push(`Pack ${pack.member}: ${pack.state}`);
155
+ }
156
+ return lines.join("\n");
157
+ }
158
+ function withLocation(command) {
159
+ return command.option("--directory <path>", "Installation directory selected during configure", defaultInstallationDirectory);
160
+ }
161
+ /** Read only the location contract so stop/delete still work with invalid startup settings. */
162
+ async function resolveLocation({ directory }) {
163
+ let value;
164
+ try {
165
+ value = await readJson(join(resolve(directory), "installation.json"));
166
+ }
167
+ catch (error) {
168
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
169
+ throw new InstallationError("unavailable", `No installation found in ${resolve(directory)}. Run clawscarf configure to create one, or use --directory to select an existing installation.`);
170
+ throw error;
171
+ }
172
+ const config = z
173
+ .object({ stateDirectory: installationSchema.shape.stateDirectory })
174
+ .parse(value);
175
+ return resolve(directory, config.stateDirectory);
176
+ }
@@ -0,0 +1,104 @@
1
+ import { cloudUrlSchema, defaultCloudUrl } from "../cloud/url.js";
2
+ import { z } from "zod";
3
+ import { localInput } from "../deployment/configuration.js";
4
+ const path = z.string().min(1);
5
+ const resources = z.strictObject({
6
+ cpu: localInput.shape.cpu,
7
+ memory: localInput.shape.memory,
8
+ });
9
+ export const externalLiteLlmSchema = z.strictObject({
10
+ mode: z.literal("external"),
11
+ configurationFile: path,
12
+ credentialFile: path,
13
+ caFile: path.optional(),
14
+ });
15
+ export const bundledLiteLlmSchema = z.strictObject({
16
+ mode: z.literal("litellm"),
17
+ configurationFile: path,
18
+ upstreamEnvironmentFile: path,
19
+ });
20
+ /** Product contract: the whole team runtime is always protected by OpenShell. */
21
+ export const installationSchema = z
22
+ .strictObject({
23
+ schemaVersion: z.literal(1),
24
+ name: localInput.shape.name,
25
+ recipe: z
26
+ .strictObject({
27
+ id: z.string().min(1),
28
+ version: z.string().min(1).optional(),
29
+ release: z.string().min(1),
30
+ })
31
+ .optional(),
32
+ releaseFile: path,
33
+ stateDirectory: path,
34
+ exposure: z.discriminatedUnion("mode", [
35
+ z.strictObject({
36
+ mode: z.literal("local"),
37
+ applicationPort: z.number().int().min(1024).max(65535),
38
+ widgetPort: z.number().int().min(1024).max(65535),
39
+ }),
40
+ z.strictObject({
41
+ mode: z.literal("https"),
42
+ applicationOrigin: z.url(),
43
+ widgetOrigin: z.url(),
44
+ certificateFile: path,
45
+ keyFile: path,
46
+ }),
47
+ ]),
48
+ access: z.discriminatedUnion("mode", [
49
+ z.strictObject({
50
+ mode: z.literal("hosted"),
51
+ cloudUrl: cloudUrlSchema.default(defaultCloudUrl),
52
+ registrationFile: path.default("./secrets/hosted-login.json"),
53
+ administratorSubject: z.string().min(1).optional(),
54
+ administratorEmail: z.email().optional(),
55
+ administratorName: localInput.shape.administratorName,
56
+ }),
57
+ z.strictObject({
58
+ mode: z.literal("oidc"),
59
+ administratorName: localInput.shape.administratorName,
60
+ issuer: z.url(),
61
+ clientId: z.string().min(1),
62
+ clientSecretFile: path,
63
+ administratorSubject: z.string().min(1).optional(),
64
+ administratorEmail: z.email().optional(),
65
+ }),
66
+ ]),
67
+ resources: z.strictObject({ runtime: resources }),
68
+ browser: z.strictObject({ enabled: z.boolean() }),
69
+ models: z.discriminatedUnion("mode", [
70
+ externalLiteLlmSchema,
71
+ bundledLiteLlmSchema,
72
+ ]),
73
+ connections: z.strictObject({
74
+ mode: z.enum(["disabled", "hosted"]),
75
+ cloudUrl: cloudUrlSchema.default(defaultCloudUrl),
76
+ registrationFile: path.default("./secrets/connections-registration.json"),
77
+ }),
78
+ packOperator: z
79
+ .strictObject({
80
+ pythonExecutable: path,
81
+ experimentalClaws: z.literal(true),
82
+ })
83
+ .optional(),
84
+ packs: z
85
+ .array(z.strictObject({
86
+ directory: path,
87
+ members: z.array(z.string().regex(/^[a-z][a-z0-9-]{0,63}$/)).min(1),
88
+ bindingsFile: path.optional(),
89
+ }))
90
+ .max(32),
91
+ })
92
+ .superRefine((value, context) => {
93
+ if (Boolean(value.access.administratorSubject) !==
94
+ Boolean(value.access.administratorEmail))
95
+ context.addIssue({
96
+ code: "custom",
97
+ message: "Supply both administrator subject and email, or omit both to claim the server after login.",
98
+ });
99
+ if (value.packs.length > 0 && !value.packOperator)
100
+ context.addIssue({
101
+ code: "custom",
102
+ message: "Selected packs require a Python OpenShell SDK and explicit experimental Claws acknowledgement.",
103
+ });
104
+ });