@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,354 @@
1
+ import { lstat, readFile } from "node:fs/promises";
2
+ import { isIP } from "node:net";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import ipaddr from "ipaddr.js";
6
+ import { isDeepStrictEqual } from "node:util";
7
+ import { LocalSetupError, run } from "./process.js";
8
+ import { ensurePrivateFile, resourceNames } from "./state.js";
9
+ const networkId = z.string().regex(/^[a-f0-9]{64}$/);
10
+ const intentSchema = z.strictObject({
11
+ ownerId: z.uuid(),
12
+ purpose: z.enum(["companion", "runtime", "browser", "machine"]),
13
+ name: z.string().min(1),
14
+ });
15
+ const browserAddressSchema = z.strictObject({
16
+ subnet: z.string(),
17
+ address: z.ipv4(),
18
+ gateway: z.string().optional(),
19
+ });
20
+ const receiptSchema = intentSchema
21
+ .extend({
22
+ id: networkId,
23
+ isolated: browserAddressSchema.optional(),
24
+ })
25
+ .refine((receipt) => ["browser", "machine"].includes(receipt.purpose) ===
26
+ (receipt.isolated !== undefined));
27
+ const emptyOptions = z.record(z.string(), z.never()).nullable();
28
+ const bridgeOptions = z
29
+ .strictObject({
30
+ "com.docker.network.enable_ipv4": z.literal("true").optional(),
31
+ "com.docker.network.enable_ipv6": z.literal("false").optional(),
32
+ })
33
+ .nullable();
34
+ const inspectedSchema = z.object({
35
+ Id: networkId,
36
+ Name: z.string(),
37
+ Driver: z.literal("bridge"),
38
+ Scope: z.literal("local"),
39
+ Internal: z.literal(false),
40
+ Ingress: z.literal(false),
41
+ Attachable: z.boolean(),
42
+ EnableIPv6: z.literal(false),
43
+ Options: bridgeOptions,
44
+ Labels: z.record(z.string(), z.string()),
45
+ IPAM: z.object({
46
+ Driver: z.literal("default"),
47
+ Options: emptyOptions,
48
+ Config: z
49
+ .array(z.object({ Gateway: z.string().optional() }))
50
+ .refine((configs) => configs.some((config) => config.Gateway && isIP(config.Gateway) === 4)),
51
+ }),
52
+ });
53
+ const browserInspectedSchema = inspectedSchema.extend({
54
+ Internal: z.literal(true),
55
+ Attachable: z.literal(false),
56
+ Options: z.strictObject({
57
+ "com.docker.network.enable_ipv4": z.literal("true").optional(),
58
+ "com.docker.network.enable_ipv6": z.literal("false").optional(),
59
+ "com.docker.network.bridge.gateway_mode_ipv4": z.literal("isolated"),
60
+ }),
61
+ IPAM: z.object({
62
+ Driver: z.literal("default"),
63
+ Options: emptyOptions,
64
+ Config: z.tuple([
65
+ z.strictObject({
66
+ Subnet: z.string(),
67
+ Gateway: z.string().optional(),
68
+ IPRange: z.literal("").optional(),
69
+ AuxiliaryAddresses: z.record(z.string(), z.never()).optional(),
70
+ }),
71
+ ]),
72
+ }),
73
+ });
74
+ function reservedAddress(config) {
75
+ try {
76
+ if (!ipaddr.IPv4.isValidCIDRFourPartDecimal(config.Subnet))
77
+ changed();
78
+ const [base, prefix] = ipaddr.IPv4.parseCIDR(config.Subnet);
79
+ const first = ipaddr.IPv4.networkAddressFromCIDR(config.Subnet);
80
+ const last = ipaddr.IPv4.broadcastAddressFromCIDR(config.Subnet);
81
+ if (base.toString() !== first.toString() ||
82
+ prefix > 29 ||
83
+ first.range() !== "private" ||
84
+ last.range() !== "private")
85
+ changed();
86
+ // The final usable host avoids Docker's usual low-address dynamic allocation.
87
+ const bytes = last.toByteArray();
88
+ const end = bytes[3];
89
+ if (end === undefined || end < 1)
90
+ changed();
91
+ bytes[3] = end - 1;
92
+ const address = ipaddr.fromByteArray(bytes).toString();
93
+ if (config.Gateway) {
94
+ if (!ipaddr.IPv4.isValidFourPartDecimal(config.Gateway))
95
+ changed();
96
+ const gateway = ipaddr.IPv4.parse(config.Gateway);
97
+ if (!gateway.match([first, prefix]) ||
98
+ [first.toString(), last.toString(), address].includes(gateway.toString()))
99
+ changed();
100
+ }
101
+ return {
102
+ subnet: config.Subnet,
103
+ address,
104
+ ...(config.Gateway !== undefined ? { gateway: config.Gateway } : {}),
105
+ };
106
+ }
107
+ catch {
108
+ changed();
109
+ }
110
+ }
111
+ function changed() {
112
+ throw new LocalSetupError("network_identity_changed", "A required network or its private ownership record differs from this installation. Inspect it before continuing; setup will not adopt or replace it.");
113
+ }
114
+ function uncertain(purpose) {
115
+ throw new LocalSetupError("network_outcome_unknown", `Creation of this installation's ${purpose} network was attempted but its owned network is absent. Inspect Docker network capacity and this installation before continuing; setup will not automatically allocate it again.`);
116
+ }
117
+ async function record(path) {
118
+ try {
119
+ const metadata = await lstat(path);
120
+ if (!metadata.isFile() ||
121
+ (metadata.mode & 0o077) !== 0 ||
122
+ metadata.uid !== process.getuid?.())
123
+ changed();
124
+ return JSON.parse(await readFile(path, "utf8"));
125
+ }
126
+ catch (error) {
127
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
128
+ return undefined;
129
+ if (error instanceof LocalSetupError)
130
+ throw error;
131
+ changed();
132
+ }
133
+ }
134
+ function equalIntent(actual, expected) {
135
+ return (actual.ownerId === expected.ownerId &&
136
+ actual.purpose === expected.purpose &&
137
+ actual.name === expected.name);
138
+ }
139
+ async function records(directory, intent) {
140
+ const base = join(directory, "private", `network-${intent.purpose}`);
141
+ const intentPath = `${base}-create.json`;
142
+ const receiptPath = `${base}-receipt.json`;
143
+ const pending = await record(intentPath);
144
+ const rawReceipt = await record(receiptPath);
145
+ if (pending !== undefined) {
146
+ const parsed = intentSchema.safeParse(pending);
147
+ if (!parsed.success || !equalIntent(parsed.data, intent))
148
+ changed();
149
+ }
150
+ let receipt;
151
+ if (rawReceipt !== undefined) {
152
+ const parsed = receiptSchema.safeParse(rawReceipt);
153
+ if (!parsed.success ||
154
+ !equalIntent(parsed.data, intent) ||
155
+ pending === undefined)
156
+ changed();
157
+ receipt = parsed.data;
158
+ }
159
+ return { pending: pending !== undefined, receipt, intentPath, receiptPath };
160
+ }
161
+ async function observe(intent, command) {
162
+ try {
163
+ const output = await command("docker", [
164
+ "network",
165
+ "ls",
166
+ "--no-trunc",
167
+ "--format",
168
+ '{"id":{{json .ID}},"name":{{json .Name}}}',
169
+ ]);
170
+ const rows = output.trim()
171
+ ? output
172
+ .trim()
173
+ .split("\n")
174
+ .map((line) => z
175
+ .object({ id: networkId, name: z.string() })
176
+ .parse(JSON.parse(line)))
177
+ : [];
178
+ const matching = rows.filter((row) => row.name === intent.name);
179
+ if (matching.length > 1)
180
+ changed();
181
+ const found = matching[0];
182
+ if (!found)
183
+ return undefined;
184
+ const format = '{"Id":{{json .Id}},"Name":{{json .Name}},"Driver":{{json .Driver}},"Scope":{{json .Scope}},"Internal":{{json .Internal}},"Ingress":{{json .Ingress}},"Attachable":{{json .Attachable}},"EnableIPv6":{{json .EnableIPv6}},"Options":{{json .Options}},"Labels":{{json .Labels}},"IPAM":{{json .IPAM}}}';
185
+ const raw = JSON.parse(await command("docker", [
186
+ "network",
187
+ "inspect",
188
+ "--format",
189
+ format,
190
+ found.id,
191
+ ]));
192
+ const browser = ["browser", "machine"].includes(intent.purpose)
193
+ ? browserInspectedSchema.safeParse(raw)
194
+ : undefined;
195
+ const result = browser ?? inspectedSchema.safeParse(raw);
196
+ if (!result.success)
197
+ changed();
198
+ const network = result.data;
199
+ if (network.Id !== found.id ||
200
+ network.Name !== intent.name ||
201
+ network.Labels["clawscarf.installation"] !== intent.ownerId ||
202
+ network.Labels["clawscarf.network-purpose"] !== intent.purpose ||
203
+ network.Attachable !== (intent.purpose === "runtime"))
204
+ changed();
205
+ return {
206
+ id: network.Id,
207
+ ...(browser?.success
208
+ ? { isolated: reservedAddress(browser.data.IPAM.Config[0]) }
209
+ : {}),
210
+ };
211
+ }
212
+ catch (error) {
213
+ if (error instanceof LocalSetupError &&
214
+ error.code === "network_identity_changed")
215
+ throw error;
216
+ throw new LocalSetupError("network_lookup_incomplete", "Docker network ownership could not be completely observed. No absence or allocation decision was made; inspect Docker before retrying.");
217
+ }
218
+ }
219
+ function intents(state) {
220
+ const names = resourceNames(state);
221
+ return [
222
+ {
223
+ ownerId: state.ownerId,
224
+ purpose: "companion",
225
+ name: `${names.project}_default`,
226
+ },
227
+ { ownerId: state.ownerId, purpose: "runtime", name: names.sandbox },
228
+ ...(state.input.browser
229
+ ? [
230
+ {
231
+ ownerId: state.ownerId,
232
+ purpose: "browser",
233
+ name: `${names.project}_browser`,
234
+ },
235
+ {
236
+ ownerId: state.ownerId,
237
+ purpose: "machine",
238
+ name: `${names.project}_machine`,
239
+ },
240
+ ]
241
+ : []),
242
+ ];
243
+ }
244
+ /** Caller holds the installation lock. Reserve actual networks before volumes or services. */
245
+ export async function ensureLocalNetworks(directory, state, command = run) {
246
+ for (const intent of intents(state)) {
247
+ const saved = await records(directory, intent);
248
+ let observed = await observe(intent, command);
249
+ let id = observed?.id;
250
+ if (!id && saved.pending)
251
+ uncertain(intent.purpose);
252
+ if (saved.receipt &&
253
+ (id !== saved.receipt.id ||
254
+ !isDeepStrictEqual(observed?.isolated, saved.receipt.isolated)))
255
+ changed();
256
+ if (id && !saved.pending)
257
+ changed();
258
+ if (!id) {
259
+ await ensurePrivateFile(saved.intentPath, JSON.stringify(intent) + "\n");
260
+ let returnedId;
261
+ try {
262
+ returnedId = (await command("docker", [
263
+ "network",
264
+ "create",
265
+ "--driver",
266
+ "bridge",
267
+ ...(intent.purpose === "runtime" ? ["--attachable"] : []),
268
+ ...(["browser", "machine"].includes(intent.purpose)
269
+ ? [
270
+ "--internal",
271
+ "--opt",
272
+ "com.docker.network.bridge.gateway_mode_ipv4=isolated",
273
+ ]
274
+ : []),
275
+ "--label",
276
+ `clawscarf.installation=${intent.ownerId}`,
277
+ "--label",
278
+ `clawscarf.network-purpose=${intent.purpose}`,
279
+ intent.name,
280
+ ])).trim();
281
+ }
282
+ catch {
283
+ // A lost response may follow allocation. Reconcile exact observed ownership only.
284
+ }
285
+ observed = await observe(intent, command);
286
+ id = observed?.id;
287
+ if (!id)
288
+ uncertain(intent.purpose);
289
+ if (returnedId !== undefined && returnedId !== id)
290
+ changed();
291
+ }
292
+ if (!saved.receipt)
293
+ await ensurePrivateFile(saved.receiptPath, JSON.stringify({
294
+ ...intent,
295
+ id,
296
+ ...(observed?.isolated ? { isolated: observed.isolated } : {}),
297
+ }) + "\n");
298
+ }
299
+ }
300
+ /** Read-only startup check: never allocate or repair a missing reservation. */
301
+ export async function verifyLocalNetworks(directory, state, command = run) {
302
+ for (const intent of intents(state)) {
303
+ const saved = await records(directory, intent);
304
+ if (!saved.receipt)
305
+ throw new LocalSetupError("network_unprepared", "Required network reservations are not confirmed. Inspect and resume preparation before starting this installation.");
306
+ const observed = await observe(intent, command);
307
+ if (!observed)
308
+ uncertain(intent.purpose);
309
+ if (observed.id !== saved.receipt.id ||
310
+ !isDeepStrictEqual(observed.isolated, saved.receipt.isolated))
311
+ changed();
312
+ }
313
+ }
314
+ /** Re-observe the isolated reservation before generating its static address and source ACL. */
315
+ export async function observedBrowserAddress(directory, state, command = run) {
316
+ const intent = intents(state).find((value) => value.purpose === "browser");
317
+ if (!intent)
318
+ return undefined;
319
+ const saved = await records(directory, intent);
320
+ if (!saved.receipt)
321
+ throw new LocalSetupError("network_unprepared", "The browser network reservation is not confirmed. Resume preparation before configuring browser access.");
322
+ const observed = await observe(intent, command);
323
+ if (!observed)
324
+ uncertain(intent.purpose);
325
+ if (observed.id !== saved.receipt.id ||
326
+ !observed.isolated ||
327
+ !isDeepStrictEqual(observed.isolated, saved.receipt.isolated))
328
+ changed();
329
+ return observed.isolated.address;
330
+ }
331
+ /** Separate private bridge: only the browser node, its ingress and DNS attach. */
332
+ export async function observedBrowserMachineAddresses(directory, state, command = run) {
333
+ const intent = intents(state).find((value) => value.purpose === "machine");
334
+ if (!intent)
335
+ return undefined;
336
+ const saved = await records(directory, intent);
337
+ if (!saved.receipt)
338
+ throw new LocalSetupError("network_unprepared", "The private browser-node network is not prepared.");
339
+ const observed = await observe(intent, command);
340
+ if (!observed ||
341
+ observed.id !== saved.receipt.id ||
342
+ !observed.isolated ||
343
+ !isDeepStrictEqual(observed.isolated, saved.receipt.isolated))
344
+ changed();
345
+ const bytes = ipaddr.IPv4.parse(observed.isolated.address).toByteArray();
346
+ const last = bytes[3];
347
+ if (last === undefined || last < 3)
348
+ changed();
349
+ const ingress = [...bytes.slice(0, 3), last - 1].join(".");
350
+ const dns = [...bytes.slice(0, 3), last - 2].join(".");
351
+ if ([ingress, dns].includes(observed.isolated.gateway ?? ""))
352
+ changed();
353
+ return { node: observed.isolated.address, ingress, dns };
354
+ }
@@ -0,0 +1,56 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { parseDocument } from "yaml";
4
+ import { z } from "zod";
5
+ import { LocalSetupError } from "./process.js";
6
+ import { ensurePrivateFile } from "./state.js";
7
+ export function initialRuntimePolicy(source, models, connections) {
8
+ const document = parseDocument(source);
9
+ if (document.errors.length || document.warnings.length)
10
+ throw new LocalSetupError("invalid_runtime_policy", "The shipped runtime policy is invalid.");
11
+ const raw = document.toJS();
12
+ const parsed = z
13
+ .looseObject({ network_policies: z.record(z.string(), z.never()) })
14
+ .safeParse(raw);
15
+ if (!parsed.success)
16
+ throw new LocalSetupError("invalid_runtime_policy", "Initial model setup requires the shipped deny-by-default runtime policy.");
17
+ return {
18
+ ...parsed.data,
19
+ network_policies: {
20
+ ...(connections
21
+ ? {
22
+ connections_broker: {
23
+ name: "Connections broker",
24
+ endpoints: [
25
+ {
26
+ host: connections.network.host,
27
+ port: connections.network.port,
28
+ tls: "skip",
29
+ },
30
+ ],
31
+ binaries: [{ path: connections.network.binary }],
32
+ },
33
+ }
34
+ : {}),
35
+ ...(models
36
+ ? {
37
+ model_gateway: {
38
+ name: "Model gateway",
39
+ endpoints: [
40
+ {
41
+ host: models.network.host,
42
+ port: models.network.port,
43
+ tls: "skip",
44
+ },
45
+ ],
46
+ binaries: [{ path: models.network.binary }],
47
+ },
48
+ }
49
+ : {}),
50
+ },
51
+ };
52
+ }
53
+ export async function prepareRuntimePolicy(directory, models, connections) {
54
+ const policy = initialRuntimePolicy(await readFile(new URL("../../deploy/openshell/policy.yaml", import.meta.url), "utf8"), models, connections);
55
+ await ensurePrivateFile(join(directory, "private/runtime-policy.json"), JSON.stringify(policy));
56
+ }
@@ -0,0 +1,124 @@
1
+ import { constants } from "node:fs";
2
+ import { access, stat } from "node:fs/promises";
3
+ import { createServer } from "node:net";
4
+ import { z } from "zod";
5
+ import { resourceNames } from "./state.js";
6
+ import { LocalSetupError, run } from "./process.js";
7
+ /** A bind probe detects current conflicts; it does not reserve a port for startup. */
8
+ export async function portAvailable(port, host) {
9
+ const server = createServer((socket) => {
10
+ socket.destroy();
11
+ });
12
+ try {
13
+ await new Promise((resolve, reject) => {
14
+ server.once("error", reject);
15
+ server.listen({ host, port, exclusive: true }, resolve);
16
+ });
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ if (error instanceof Error &&
21
+ "code" in error &&
22
+ error.code === "EADDRINUSE")
23
+ return false;
24
+ throw new LocalSetupError("port_check_failed", `Cannot check local port ${String(port)}. Check local network permissions before retrying.`);
25
+ }
26
+ finally {
27
+ if (server.listening)
28
+ await new Promise((resolve, reject) => server.close((error) => {
29
+ if (error)
30
+ reject(error);
31
+ else
32
+ resolve();
33
+ }));
34
+ }
35
+ }
36
+ async function ownedListener(state, name, port, host, command) {
37
+ const services = {
38
+ database: "postgres",
39
+ native: "application",
40
+ nativeWidgets: "widgets",
41
+ application: "companion",
42
+ widgets: "companion",
43
+ management: "companion",
44
+ browser: "browser-relay",
45
+ };
46
+ const service = services[name] ?? name;
47
+ const project = resourceNames(state).project;
48
+ const ids = (await command("docker", [
49
+ "container",
50
+ "ls",
51
+ "--quiet",
52
+ "--no-trunc",
53
+ "--filter",
54
+ `label=clawscarf.installation=${state.ownerId}`,
55
+ "--filter",
56
+ `label=com.docker.compose.project=${project}`,
57
+ "--filter",
58
+ `label=com.docker.compose.service=${service}`,
59
+ ]))
60
+ .trim()
61
+ .split("\n")
62
+ .filter(Boolean);
63
+ if (ids.length !== 1 || !/^[a-f0-9]{64}$/.test(ids[0] ?? ""))
64
+ return false;
65
+ const id = ids[0];
66
+ if (!id)
67
+ return false;
68
+ const format = '{"running":{{json .State.Running}},"owner":{{json (index .Config.Labels "clawscarf.installation")}},"project":{{json (index .Config.Labels "com.docker.compose.project")}},"service":{{json (index .Config.Labels "com.docker.compose.service")}},"ports":{{json .NetworkSettings.Ports}}}';
69
+ const result = z
70
+ .object({
71
+ running: z.literal(true),
72
+ owner: z.literal(state.ownerId),
73
+ project: z.literal(project),
74
+ service: z.literal(service),
75
+ ports: z.record(z.string(), z
76
+ .array(z.object({ HostIp: z.string(), HostPort: z.string() }))
77
+ .nullable()),
78
+ })
79
+ .safeParse(JSON.parse(await command("docker", [
80
+ "container",
81
+ "inspect",
82
+ "--format",
83
+ format,
84
+ id,
85
+ ])));
86
+ return (result.success &&
87
+ Object.values(result.data.ports).some((bindings) => bindings?.some((binding) => binding.HostIp === host && binding.HostPort === String(port))));
88
+ }
89
+ /** Repeated starts may reuse listeners belonging to this installation only. */
90
+ export async function verifyLocalPorts(state, command = run) {
91
+ const ports = {
92
+ ...state.input.ports,
93
+ ...(state.input.modelGateway
94
+ ? { models: state.input.modelGateway.port }
95
+ : {}),
96
+ ...(state.input.browser ? { browser: state.input.browser.port } : {}),
97
+ };
98
+ for (const [name, port] of Object.entries(ports)) {
99
+ const host = state.input.team.certificateFile &&
100
+ ["application", "widgets"].includes(name)
101
+ ? "0.0.0.0"
102
+ : "127.0.0.1";
103
+ if (await portAvailable(port, host))
104
+ continue;
105
+ if (await ownedListener(state, name, port, host, command))
106
+ continue;
107
+ throw new LocalSetupError("port_in_use", `The ${name} port ${host}:${String(port)} is already in use. Stop its current owner or select different ports for a new installation before retrying.`);
108
+ }
109
+ }
110
+ export async function verifyLocalExecutables(state) {
111
+ for (const [name, path] of [
112
+ ["OpenShell CLI", state.input.openshellCli],
113
+ ["OpenShell gateway", state.input.openshellGateway],
114
+ ]) {
115
+ try {
116
+ await access(path, constants.X_OK);
117
+ if (!(await stat(path)).isFile())
118
+ throw Error("Not an executable file.");
119
+ }
120
+ catch {
121
+ throw new LocalSetupError("executable_unavailable", `The configured ${name} executable is unavailable. Install the pinned executable and check its permissions before retrying.`);
122
+ }
123
+ }
124
+ }