@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,143 @@
1
+ import { runtimeRelayHost } from "./relay.js";
2
+ import { randomBytes } from "node:crypto";
3
+ import { chmod, lstat, readFile, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { observedBrowserAddress } from "./networks.js";
7
+ import { LocalSetupError, run } from "./process.js";
8
+ import { ensurePrivateFile, resourceNames } from "./state.js";
9
+ const tokenSchema = z.string().regex(/^[a-f0-9]{64}$/);
10
+ /** Optional browser credentials are independent of model, account and controller keys. */
11
+ export async function prepareBrowser(directory, state, command = run) {
12
+ if (!state.input.browser)
13
+ return undefined;
14
+ const path = join(directory, "private/browser-token");
15
+ let token;
16
+ try {
17
+ const metadata = await lstat(path);
18
+ if (!metadata.isFile() ||
19
+ (metadata.mode & 0o077) !== 0 ||
20
+ metadata.uid !== process.getuid?.())
21
+ throw new LocalSetupError("configuration_changed", "The browser credential must remain private and owned by this operator.");
22
+ token = tokenSchema.parse(await readFile(path, "utf8"));
23
+ }
24
+ catch (error) {
25
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
26
+ throw error;
27
+ token = randomBytes(32).toString("hex");
28
+ await ensurePrivateFile(path, token);
29
+ }
30
+ const address = await observedBrowserAddress(directory, state, command);
31
+ if (!address)
32
+ throw new LocalSetupError("network_unprepared", "The isolated browser network is not prepared.");
33
+ const acl = join(directory, "private/browser-source.acl");
34
+ const content = `${address}/32\n`;
35
+ // Public network data, within the operator's private directory; Squid's UID must read its bind mount.
36
+ try {
37
+ const metadata = await lstat(acl);
38
+ if (!metadata.isFile() ||
39
+ (metadata.mode & 0o777) !== 0o444 ||
40
+ metadata.uid !== process.getuid?.() ||
41
+ (await readFile(acl, "utf8")) !== content)
42
+ throw new LocalSetupError("configuration_changed", "The browser source rule differs from its reserved network address.");
43
+ }
44
+ catch (error) {
45
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
46
+ throw error;
47
+ await writeFile(acl, content, { flag: "wx", mode: 0o444 });
48
+ await chmod(acl, 0o444);
49
+ }
50
+ await ensurePrivateFile(join(directory, "private/browser-seccomp.json"), await readFile(new URL("../../deploy/execution/browser/seccomp.json", import.meta.url)));
51
+ return { token, address };
52
+ }
53
+ /** Startup never repairs changed network constraints or substitutes browser credentials. */
54
+ export async function verifyBrowserConfiguration(directory, state, command = run) {
55
+ if (!state.input.browser)
56
+ return;
57
+ const address = await observedBrowserAddress(directory, state, command);
58
+ if (!address)
59
+ throw new LocalSetupError("network_unprepared", "The isolated browser network is not prepared.");
60
+ const expected = [
61
+ {
62
+ name: "browser-source.acl",
63
+ mode: 0o444,
64
+ content: Buffer.from(`${address}/32\n`),
65
+ },
66
+ {
67
+ name: "browser-seccomp.json",
68
+ mode: 0o600,
69
+ content: await readFile(new URL("../../deploy/execution/browser/seccomp.json", import.meta.url)),
70
+ },
71
+ ];
72
+ for (const file of expected) {
73
+ const path = join(directory, "private", file.name);
74
+ const metadata = await lstat(path);
75
+ if (!metadata.isFile() ||
76
+ metadata.nlink !== 1 ||
77
+ metadata.uid !== process.getuid?.() ||
78
+ (metadata.mode & 0o777) !== file.mode ||
79
+ !file.content.equals(await readFile(path)))
80
+ throw new LocalSetupError("configuration_changed", "The prepared browser isolation settings have changed. Inspect them before starting.");
81
+ }
82
+ const path = join(directory, "private/browser-token");
83
+ const metadata = await lstat(path);
84
+ if (!metadata.isFile() ||
85
+ metadata.nlink !== 1 ||
86
+ metadata.uid !== process.getuid?.() ||
87
+ (metadata.mode & 0o077) !== 0)
88
+ throw new LocalSetupError("configuration_changed", "The browser credential must remain private and owned by this operator.");
89
+ tokenSchema.parse(await readFile(path, "utf8"));
90
+ }
91
+ export function browserDefaults(token) {
92
+ tokenSchema.parse(token);
93
+ return {
94
+ enabled: true,
95
+ allowSystemProfileImport: false,
96
+ defaultProfile: "team",
97
+ ssrfPolicy: { allowedHostnames: [runtimeRelayHost] },
98
+ profiles: {
99
+ team: {
100
+ cdpUrl: `http://openclaw:${token}@${runtimeRelayHost}:9223`,
101
+ attachOnly: true,
102
+ },
103
+ },
104
+ };
105
+ }
106
+ export async function initializeBrowserVolume(state, token) {
107
+ await run("docker", [
108
+ "run",
109
+ "--rm",
110
+ "-i",
111
+ "--pull",
112
+ "never",
113
+ "--network",
114
+ "none",
115
+ "--user",
116
+ "root",
117
+ "--mount",
118
+ `type=volume,source=${resourceNames(state).browserVolume},target=/state,volume-nocopy`,
119
+ "--entrypoint",
120
+ "node",
121
+ state.input.runtimeImage,
122
+ "/app/clawscarf/initialize-browser-main.js",
123
+ ], {
124
+ input: JSON.stringify({
125
+ ownerId: state.ownerId,
126
+ token: tokenSchema.parse(token),
127
+ }),
128
+ });
129
+ }
130
+ /** Readiness observes the authenticated CDP service without opening a page or changing state. */
131
+ export async function verifyBrowserListener(directory, port) {
132
+ const token = tokenSchema.parse(await readFile(join(directory, "private/browser-token"), "utf8"));
133
+ const response = await fetch(`http://127.0.0.1:${String(port)}/json/version`, {
134
+ headers: { Authorization: `Bearer ${token}` },
135
+ redirect: "error",
136
+ signal: AbortSignal.timeout(3000),
137
+ });
138
+ if (!response.ok) {
139
+ await response.body?.cancel();
140
+ throw new LocalSetupError("browser_unavailable", "The shared browser has not become ready.");
141
+ }
142
+ z.object({ Browser: z.string().min(1), webSocketDebuggerUrl: z.url() }).parse(await response.json());
143
+ }
@@ -0,0 +1,76 @@
1
+ import { createPrivateKey, X509Certificate } from "node:crypto";
2
+ import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { LocalSetupError, run } from "./process.js";
5
+ import { ensurePrivateFile } from "./state.js";
6
+ const names = [
7
+ "management-cert.pem",
8
+ "management-key.pem",
9
+ "management-ca.pem",
10
+ ];
11
+ export async function ensureCertificates(directory) {
12
+ const present = await Promise.all(names.map(async (name) => {
13
+ try {
14
+ const info = await lstat(join(directory, name));
15
+ if (!info.isFile() || (info.mode & 0o077) !== 0)
16
+ throw new LocalSetupError("invalid_certificate", "Management TLS files must be private regular files.");
17
+ return true;
18
+ }
19
+ catch (error) {
20
+ if (error instanceof Error &&
21
+ "code" in error &&
22
+ error.code === "ENOENT")
23
+ return false;
24
+ throw error;
25
+ }
26
+ }));
27
+ if (present.some(Boolean)) {
28
+ if (!present.every(Boolean))
29
+ throw new LocalSetupError("incomplete_certificate", "Management TLS initialization is incomplete. Inspect the private TLS files; setup will not replace existing keys.");
30
+ const cert = new X509Certificate(await readFile(join(directory, "management-cert.pem")));
31
+ const ca = new X509Certificate(await readFile(join(directory, "management-ca.pem")));
32
+ const key = createPrivateKey(await readFile(join(directory, "management-key.pem")));
33
+ if (!cert.checkPrivateKey(key) ||
34
+ !cert.checkHost("host.docker.internal") ||
35
+ !cert.checkIP("127.0.0.1") ||
36
+ !cert.raw.equals(ca.raw) ||
37
+ !cert.ca ||
38
+ !cert.verify(cert.publicKey) ||
39
+ Date.parse(cert.validFrom) > Date.now() ||
40
+ Date.parse(cert.validTo) <= Date.now())
41
+ throw new LocalSetupError("invalid_certificate", "Management TLS material is invalid or expired; setup will not replace it.");
42
+ return;
43
+ }
44
+ const staging = join(directory, "management-tls");
45
+ // A retained staging directory makes an interrupted generation explicit.
46
+ await mkdir(staging, { mode: 0o700 });
47
+ // An explicit config avoids inheriting host-specific certificate extensions.
48
+ const requestConfig = join(staging, "request.cnf");
49
+ await writeFile(requestConfig, "[req]\ndistinguished_name=dn\n[dn]\n");
50
+ await run("openssl", [
51
+ "req",
52
+ "-config",
53
+ requestConfig,
54
+ "-x509",
55
+ "-newkey",
56
+ "rsa:3072",
57
+ "-nodes",
58
+ "-keyout",
59
+ join(staging, "key.pem"),
60
+ "-out",
61
+ join(staging, "cert.pem"),
62
+ "-days",
63
+ "365",
64
+ "-subj",
65
+ "/CN=ClawScarf local management",
66
+ "-addext",
67
+ "subjectAltName=DNS:localhost,DNS:host.docker.internal,IP:127.0.0.1",
68
+ "-addext",
69
+ "basicConstraints=critical,CA:TRUE",
70
+ ]);
71
+ const certificate = await readFile(join(staging, "cert.pem"));
72
+ await ensurePrivateFile(join(directory, "management-key.pem"), await readFile(join(staging, "key.pem")));
73
+ await ensurePrivateFile(join(directory, "management-ca.pem"), certificate);
74
+ await ensurePrivateFile(join(directory, "management-cert.pem"), certificate);
75
+ await rm(staging, { recursive: true });
76
+ }
@@ -0,0 +1,191 @@
1
+ import { OperatorError } from "../errors.js";
2
+ import { controllerServices } from "./controller-compose.js";
3
+ import { browserNodeServices, browserNodeVolumes, } from "./browser-node-compose.js";
4
+ import { join } from "node:path";
5
+ import { resourceNames } from "./state.js";
6
+ import { run } from "./process.js";
7
+ import { modelGatewayServices } from "./model-gateway-compose.js";
8
+ import { postgresImage } from "./images.js";
9
+ export function composeConfiguration(state, directory, browserAddress, browserMachine) {
10
+ const { input, ownerId } = state;
11
+ const names = resourceNames(state);
12
+ const privateDirectory = join(directory, "private");
13
+ const labels = { "clawscarf.installation": ownerId };
14
+ if (input.browser && (!browserAddress || !browserMachine))
15
+ throw new OperatorError("The isolated browser address and prepared native node are required.");
16
+ const constrained = {
17
+ read_only: true,
18
+ cap_drop: ["ALL"],
19
+ security_opt: ["no-new-privileges:true"],
20
+ init: true,
21
+ labels,
22
+ };
23
+ const configuration = {
24
+ name: names.project,
25
+ services: {
26
+ ...controllerServices(directory, state),
27
+ ...modelGatewayServices(directory, state),
28
+ ...(input.browser && browserAddress && browserMachine
29
+ ? browserNodeServices(state, directory, browserAddress, browserMachine)
30
+ : {}),
31
+ ...(input.browser
32
+ ? {
33
+ browser: {
34
+ ...constrained,
35
+ image: input.browser.image,
36
+ user: "1000:1000",
37
+ networks: {
38
+ browser: { ipv4_address: browserAddress, aliases: ["browser"] },
39
+ },
40
+ security_opt: [
41
+ ...constrained.security_opt,
42
+ `seccomp:${join(privateDirectory, "browser-seccomp.json")}`,
43
+ ],
44
+ environment: {
45
+ CLAWSCARF_BROWSER_TOKEN_FILE: "/state/.clawscarf-browser/token",
46
+ CLAWSCARF_BROWSER_PROXY_SERVER: "http://browser-egress:3128",
47
+ },
48
+ volumes: ["browser:/state"],
49
+ tmpfs: ["/tmp:rw,nosuid,nodev,size=512m"],
50
+ shm_size: "256m",
51
+ mem_limit: "1g",
52
+ pids_limit: 256,
53
+ stop_grace_period: "15s",
54
+ depends_on: ["browser-egress"],
55
+ },
56
+ "browser-egress": {
57
+ ...constrained,
58
+ image: input.browser.egressImage,
59
+ networks: {
60
+ default: {},
61
+ browser: { aliases: ["browser-egress"] },
62
+ },
63
+ volumes: [
64
+ `${join(privateDirectory, "browser-source.acl")}:/etc/squid/browser-source.acl:ro`,
65
+ ],
66
+ tmpfs: ["/tmp"],
67
+ mem_limit: "128m",
68
+ pids_limit: 64,
69
+ },
70
+ }
71
+ : {}),
72
+ ...(input.relayImage
73
+ ? {
74
+ "browser-relay": {
75
+ ...constrained,
76
+ image: input.relayImage,
77
+ user: "1000:1000",
78
+ networks: {
79
+ runtime: {
80
+ aliases: ["runtime.clawscarf.internal"],
81
+ },
82
+ ...(input.browser ? { browser: {} } : {}),
83
+ },
84
+ volumes: [
85
+ `${join(privateDirectory, "browser-relay.cfg")}:/usr/local/etc/haproxy/haproxy.cfg:ro`,
86
+ ],
87
+ ...(input.browser
88
+ ? {
89
+ ports: [`127.0.0.1:${String(input.browser.port)}:9223`],
90
+ depends_on: ["browser"],
91
+ }
92
+ : {}),
93
+ mem_limit: "64m",
94
+ pids_limit: 64,
95
+ },
96
+ }
97
+ : {}),
98
+ postgres: {
99
+ image: postgresImage,
100
+ environment: {
101
+ POSTGRES_DB: "clawscarf",
102
+ POSTGRES_USER: "postgres",
103
+ POSTGRES_PASSWORD_FILE: "/run/secrets/database_password",
104
+ },
105
+ secrets: ["database_password"],
106
+ volumes: ["database:/var/lib/postgresql/data"],
107
+ ports: [`127.0.0.1:${String(input.ports.database)}:5432`],
108
+ healthcheck: {
109
+ test: ["CMD-SHELL", "pg_isready -U postgres -d clawscarf"],
110
+ interval: "2s",
111
+ timeout: "3s",
112
+ retries: 30,
113
+ },
114
+ labels,
115
+ },
116
+ companion: {
117
+ image: input.companionImage,
118
+ user: `${String(process.getuid?.() ?? 1000)}:${String(process.getgid?.() ?? 1000)}`,
119
+ environment: {
120
+ NODE_EXTRA_CA_CERTS: "/run/clawscarf/management-ca.pem",
121
+ CLAWSCARF_COMPANION_CONFIG: "/run/clawscarf/companion.json",
122
+ },
123
+ volumes: [
124
+ "access.json",
125
+ "companion.json",
126
+ "encryption.key",
127
+ "management-ca.pem",
128
+ "management-cert.pem",
129
+ "management-key.pem",
130
+ ...(input.connections?.managementKeyFile ? ["connections"] : []),
131
+ "oidc-client-secret",
132
+ ...(input.team.certificateFile
133
+ ? ["application-cert.pem", "application-key.pem"]
134
+ : []),
135
+ ].map((name) => `${join(privateDirectory, name)}:/run/clawscarf/${name}:ro`),
136
+ ports: [
137
+ `${input.team.certificateFile ? "0.0.0.0" : "127.0.0.1"}:${String(input.ports.application)}:18800`,
138
+ `127.0.0.1:${String(input.ports.management)}:18801`,
139
+ `${input.team.certificateFile ? "0.0.0.0" : "127.0.0.1"}:${String(input.ports.widgets)}:18800`,
140
+ ],
141
+ read_only: true,
142
+ tmpfs: ["/tmp"],
143
+ cap_drop: ["ALL"],
144
+ security_opt: ["no-new-privileges:true"],
145
+ init: true,
146
+ depends_on: { postgres: { condition: "service_healthy" } },
147
+ labels,
148
+ },
149
+ },
150
+ secrets: {
151
+ database_password: {
152
+ file: join(privateDirectory, "database-admin-password"),
153
+ },
154
+ },
155
+ networks: {
156
+ ...(input.relayImage
157
+ ? { runtime: { external: true, name: names.sandbox } }
158
+ : {}),
159
+ default: { external: true, name: `${names.project}_default` },
160
+ ...(input.browser
161
+ ? {
162
+ browser: { external: true, name: `${names.project}_browser` },
163
+ machine: { external: true, name: `${names.project}_machine` },
164
+ }
165
+ : {}),
166
+ },
167
+ volumes: {
168
+ ...(input.modelGateway
169
+ ? {
170
+ "models-database": {
171
+ external: true,
172
+ name: `${names.project}-models`,
173
+ },
174
+ }
175
+ : {}),
176
+ database: { external: true, name: names.databaseVolume },
177
+ ...(input.browser
178
+ ? {
179
+ browser: { external: true, name: names.browserVolume },
180
+ ...browserNodeVolumes(state),
181
+ }
182
+ : {}),
183
+ },
184
+ };
185
+ for (const service of Object.values(configuration.services))
186
+ Object.assign(service, { restart: "unless-stopped", labels });
187
+ return configuration;
188
+ }
189
+ export function compose(directory, args, timeout = 120_000) {
190
+ return run("docker", ["compose", "-f", join(directory, "compose.json"), ...args], { timeout });
191
+ }
@@ -0,0 +1,275 @@
1
+ import { OperatorError } from "../errors.js";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { z } from "zod";
4
+ import { initialConfiguration } from "../../runtime/configuration.js";
5
+ import { networkRequirementSchema } from "../packs/policy.js";
6
+ const absolutePath = z
7
+ .string()
8
+ .min(1)
9
+ .refine(isAbsolute, "Use an absolute path.");
10
+ const image = z
11
+ .string()
12
+ .regex(/^(?:sha256:[a-f0-9]{64}|[a-zA-Z0-9][a-zA-Z0-9._:/-]*@sha256:[a-f0-9]{64})$/, "Use an exact image ID or image reference with a SHA-256 digest.");
13
+ const port = z.number().int().min(1024).max(65535);
14
+ const cpu = z
15
+ .string()
16
+ .regex(/^(?:[1-9]\d*m|(?:[1-9]\d*(?:\.\d+)?|0\.\d*[1-9]\d*))$/, "Use a positive CPU quantity, such as 2 or 500m.");
17
+ const memory = z
18
+ .string()
19
+ .regex(/^[1-9]\d*(?:Ki|Mi|Gi|Ti|K|M|G|T)$/, "Use a positive memory quantity, such as 512Mi or 2Gi.");
20
+ const ports = z
21
+ .strictObject({
22
+ controller: port,
23
+ application: z.number().int().min(1).max(65535),
24
+ widgets: z.number().int().min(1).max(65535),
25
+ management: port,
26
+ native: port,
27
+ nativeWidgets: port,
28
+ database: port,
29
+ })
30
+ .refine((value) => new Set(Object.values(value)).size === Object.keys(value).length, "Every local listener needs a distinct port.");
31
+ const applicationOrigin = z.url().refine((value) => {
32
+ const u = new URL(value);
33
+ return ((u.protocol === "https:" ||
34
+ (u.protocol === "http:" &&
35
+ ["127.0.0.1", "localhost"].includes(u.hostname))) &&
36
+ u.origin === value &&
37
+ !u.username &&
38
+ !u.password);
39
+ }, "Use an exact HTTPS origin, or HTTP on localhost.");
40
+ export const connectionsBrokerUrlSchema = z
41
+ .string()
42
+ .min(1)
43
+ .max(2048)
44
+ .refine((value) => {
45
+ let endpoint;
46
+ try {
47
+ endpoint = new URL(value);
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ return (/^https:\/\//iu.test(value) &&
53
+ value.trim() === value &&
54
+ Array.from(value).every((character) => character.charCodeAt(0) > 32 &&
55
+ character.charCodeAt(0) !== 127 &&
56
+ character !== "\\") &&
57
+ endpoint.protocol === "https:" &&
58
+ !endpoint.username &&
59
+ !endpoint.password &&
60
+ !endpoint.search &&
61
+ !endpoint.hash &&
62
+ !value.includes("?") &&
63
+ !value.includes("#") &&
64
+ networkRequirementSchema.safeParse({
65
+ host: endpoint.hostname,
66
+ port: Number(endpoint.port || "443"),
67
+ protocol: "tcp",
68
+ binary: "/usr/local/bin/node",
69
+ }).success);
70
+ }, "Use an HTTPS DNS endpoint without credentials, query or fragment.");
71
+ export const connectionsInputSchema = z.strictObject({
72
+ mode: z.literal("external"),
73
+ brokerUrl: connectionsBrokerUrlSchema,
74
+ managementKeyFile: absolutePath.optional(),
75
+ caFile: absolutePath.optional(),
76
+ });
77
+ const teamInput = z
78
+ .strictObject({
79
+ origin: applicationOrigin,
80
+ widgetOrigin: applicationOrigin,
81
+ certificateFile: absolutePath.optional(),
82
+ keyFile: absolutePath.optional(),
83
+ issuer: z.url().refine((value) => {
84
+ const u = new URL(value);
85
+ return (u.protocol === "https:" &&
86
+ !u.username &&
87
+ !u.password &&
88
+ !u.search &&
89
+ !u.hash);
90
+ }, "Use an HTTPS OIDC issuer without credentials, query or fragment."),
91
+ clientId: z.string().min(1),
92
+ clientSecretFile: absolutePath,
93
+ administratorSubject: z.string().min(1).optional(),
94
+ administratorEmail: z.email().optional(),
95
+ })
96
+ .refine((value) => Boolean(value.administratorSubject) === Boolean(value.administratorEmail), "Provide both administrator subject and email, or neither for a setup link.")
97
+ .refine((value) => value.origin !== value.widgetOrigin, "Widgets need a separate origin.")
98
+ .refine((value) => {
99
+ const tls = new URL(value.origin).protocol === "https:";
100
+ return (tls === (new URL(value.widgetOrigin).protocol === "https:") &&
101
+ (tls
102
+ ? !!value.certificateFile && !!value.keyFile
103
+ : !value.certificateFile && !value.keyFile));
104
+ }, "HTTPS requires both TLS files; loopback HTTP must not supply TLS files.");
105
+ export const localInput = z
106
+ .strictObject({
107
+ team: teamInput,
108
+ name: z
109
+ .string()
110
+ .max(30)
111
+ .regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/),
112
+ administratorName: z
113
+ .string()
114
+ .min(1)
115
+ .max(256)
116
+ .refine((value) => value.trim().length > 0 &&
117
+ Array.from(value).every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127), "Use a display name without control characters."),
118
+ runtimeImage: image,
119
+ companionImage: image,
120
+ openshellCli: absolutePath,
121
+ openshellGateway: absolutePath,
122
+ openshellClientImage: image,
123
+ ports,
124
+ models: z
125
+ .strictObject({
126
+ configurationFile: absolutePath,
127
+ runtimeKeyFile: absolutePath,
128
+ caFile: absolutePath.optional(),
129
+ })
130
+ .optional(),
131
+ connections: connectionsInputSchema.optional(),
132
+ modelGateway: z
133
+ .strictObject({
134
+ configurationFile: absolutePath,
135
+ upstreamEnvironmentFile: absolutePath,
136
+ image,
137
+ port,
138
+ })
139
+ .optional(),
140
+ cpu,
141
+ memory,
142
+ relayImage: image.optional(),
143
+ browser: z
144
+ .strictObject({
145
+ image,
146
+ egressImage: image,
147
+ nodeImage: image,
148
+ dnsImage: image,
149
+ port,
150
+ })
151
+ .optional(),
152
+ })
153
+ .superRefine((value, ctx) => {
154
+ if (value.modelGateway &&
155
+ (!value.models ||
156
+ [...Object.values(value.ports), value.browser?.port].includes(value.modelGateway.port)))
157
+ ctx.addIssue({
158
+ code: "custom",
159
+ message: "The model gateway requires native model configuration and a distinct port.",
160
+ });
161
+ if (Boolean(value.relayImage) !== Boolean(value.browser))
162
+ ctx.addIssue({
163
+ code: "custom",
164
+ path: ["relayImage"],
165
+ message: "Supply the relay image exactly when a browser is configured.",
166
+ });
167
+ if (value.browser &&
168
+ Object.values(value.ports).includes(value.browser.port))
169
+ ctx.addIssue({
170
+ code: "custom",
171
+ path: ["browser", "port"],
172
+ message: "The browser relay needs a distinct port.",
173
+ });
174
+ for (const [key, port] of [
175
+ ["origin", value.ports.application],
176
+ ["widgetOrigin", value.ports.widgets],
177
+ ]) {
178
+ if (Number(new URL(value.team[key]).port || "443") !== port)
179
+ ctx.addIssue({
180
+ code: "custom",
181
+ path: ["team", key],
182
+ message: "The public origin must use its configured published port.",
183
+ });
184
+ }
185
+ });
186
+ /** Hosted login and custom identity providers both use the same OIDC team profile. */
187
+ export function parseLocalInput(value) {
188
+ return localInput.parse(value);
189
+ }
190
+ export function managementOrigin(input) {
191
+ return `https://host.docker.internal:${String(input.ports.management)}`;
192
+ }
193
+ function mountedPrivateFile(directory, path, name) {
194
+ absolutePath.parse(path);
195
+ if (resolve(path) !== join(directory, name))
196
+ throw new OperatorError(`The local configuration requires ${name} in its private directory.`);
197
+ return `/run/clawscarf/${name}`;
198
+ }
199
+ export function generateLocalConfiguration(options) {
200
+ const input = parseLocalInput(options.input);
201
+ const directory = resolve(absolutePath.parse(options.directory));
202
+ const identity = z
203
+ .string()
204
+ .refine((value) => value.startsWith("clawscarf:") &&
205
+ z.uuid().safeParse(value.slice("clawscarf:".length)).success, "Use the local Access service's exact ClawScarf administrator identity.")
206
+ .parse(options.administratorIdentity);
207
+ const database = z
208
+ .url()
209
+ .refine((value) => ["postgres:", "postgresql:"].includes(new URL(value).protocol), "Use a PostgreSQL connection URL.")
210
+ .parse(options.runtimeDatabaseUrl);
211
+ const publicOrigin = input.team.origin;
212
+ const widgetOrigin = input.team.widgetOrigin;
213
+ const access = {
214
+ origin: publicOrigin,
215
+ host: "0.0.0.0",
216
+ port: 18800,
217
+ containerLoopbackPublication: !input.team.certificateFile,
218
+ databaseUrl: database,
219
+ encryptionKeyFile: mountedPrivateFile(directory, options.encryptionKeyPath, "encryption.key"),
220
+ managementTls: {
221
+ certificateFile: mountedPrivateFile(directory, options.managementCertificatePath, "management-cert.pem"),
222
+ keyFile: mountedPrivateFile(directory, options.managementKeyPath, "management-key.pem"),
223
+ host: "0.0.0.0",
224
+ port: 18801,
225
+ },
226
+ runtime: {
227
+ origin: `http://host.docker.internal:${String(input.ports.native)}`,
228
+ managementOrigin: managementOrigin(input),
229
+ widgetOrigin,
230
+ widgetUpstream: `http://host.docker.internal:${String(input.ports.nativeWidgets)}`,
231
+ },
232
+ ...(input.team.certificateFile
233
+ ? {
234
+ applicationTls: {
235
+ certificateFile: "/run/clawscarf/application-cert.pem",
236
+ keyFile: "/run/clawscarf/application-key.pem",
237
+ },
238
+ }
239
+ : {}),
240
+ identity: {
241
+ mode: "oidc",
242
+ issuer: input.team.issuer,
243
+ clientId: input.team.clientId,
244
+ clientSecretFile: "/run/clawscarf/oidc-client-secret",
245
+ administratorSubject: input.team.administratorSubject,
246
+ administratorEmail: input.team.administratorEmail,
247
+ },
248
+ };
249
+ const native = initialConfiguration({
250
+ publicOrigin,
251
+ widgetOrigin,
252
+ administratorIdentity: identity,
253
+ });
254
+ native.gateway.port = input.ports.native;
255
+ native.mcp.apps.sandboxPort = input.ports.nativeWidgets;
256
+ return {
257
+ access,
258
+ native,
259
+ companion: companionConfiguration(input),
260
+ };
261
+ }
262
+ export function companionConfiguration(input) {
263
+ return {
264
+ accessConfigurationFile: "/run/clawscarf/access.json",
265
+ ...(input.connections?.mode === "external" &&
266
+ input.connections.managementKeyFile
267
+ ? {
268
+ cloudConnections: {
269
+ url: new URL(input.connections.brokerUrl).origin,
270
+ managementKeyFile: "/run/clawscarf/connections/management-key",
271
+ },
272
+ }
273
+ : {}),
274
+ };
275
+ }