@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/THIRD_PARTY_NOTICES.md +102 -0
- package/deploy/execution/browser/LICENSE.playwright +202 -0
- package/deploy/execution/browser/seccomp.json +640 -0
- package/deploy/execution/browser-node/configuration.js +85 -0
- package/deploy/execution/browser-node/operator.js +167 -0
- package/deploy/execution/network/node-ingress.cfg +35 -0
- package/deploy/models/catalog.json +275 -0
- package/deploy/openshell/policy.yaml +24 -0
- package/generated/http/LICENSE.md +21 -0
- package/generated/http/client/client.gen.d.ts +2 -0
- package/generated/http/client/client.gen.js +216 -0
- package/generated/http/client/index.d.ts +10 -0
- package/generated/http/client/index.js +6 -0
- package/generated/http/client/types.gen.d.ts +120 -0
- package/generated/http/client/types.gen.js +2 -0
- package/generated/http/client/utils.gen.d.ts +37 -0
- package/generated/http/client/utils.gen.js +228 -0
- package/generated/http/core/auth.gen.d.ts +25 -0
- package/generated/http/core/auth.gen.js +14 -0
- package/generated/http/core/bodySerializer.gen.d.ts +25 -0
- package/generated/http/core/bodySerializer.gen.js +57 -0
- package/generated/http/core/params.gen.d.ts +43 -0
- package/generated/http/core/params.gen.js +109 -0
- package/generated/http/core/pathSerializer.gen.d.ts +33 -0
- package/generated/http/core/pathSerializer.gen.js +106 -0
- package/generated/http/core/queryKeySerializer.gen.d.ts +18 -0
- package/generated/http/core/queryKeySerializer.gen.js +92 -0
- package/generated/http/core/serverSentEvents.gen.d.ts +71 -0
- package/generated/http/core/serverSentEvents.gen.js +132 -0
- package/generated/http/core/types.gen.d.ts +83 -0
- package/generated/http/core/types.gen.js +2 -0
- package/generated/http/core/utils.gen.d.ts +19 -0
- package/generated/http/core/utils.gen.js +87 -0
- package/package.json +37 -0
- package/packs/README.md +130 -0
- package/packs/research-team/pack.json +20 -0
- package/packs/research-team/researcher/CLAW.md +19 -0
- package/packs/research-team/researcher/package.json +8 -0
- package/packs/research-team/researcher/profiles/openclaw.yml +6 -0
- package/packs/research-team/reviewer/CLAW.md +19 -0
- package/packs/research-team/reviewer/package.json +8 -0
- package/packs/research-team/reviewer/profiles/openclaw.yml +6 -0
- package/pnpm-lock.yaml +7158 -0
- package/recipes/README.md +37 -0
- package/recipes/team-server/recipe.json +28 -0
- package/release/README.md +137 -0
- package/release/components.json +29 -0
- package/release/operator.md +63 -0
- package/runtime/configuration.js +135 -0
- package/runtime/connections-configuration.js +70 -0
- package/runtime/model-contract.js +92 -0
- package/runtime/releases/0.1.0-alpha.1.json +37 -0
- package/scripts/clawscarf.js +92 -0
- package/scripts/cloud/login.js +134 -0
- package/scripts/cloud/registration.js +232 -0
- package/scripts/cloud/url.js +9 -0
- package/scripts/connections.js +193 -0
- package/scripts/controller.js +143 -0
- package/scripts/deployment/browser-node-compose.js +81 -0
- package/scripts/deployment/browser-node-helper.js +9 -0
- package/scripts/deployment/browser-node-pairing.js +165 -0
- package/scripts/deployment/browser-node.js +154 -0
- package/scripts/deployment/browser.js +143 -0
- package/scripts/deployment/certificates.js +76 -0
- package/scripts/deployment/compose.js +191 -0
- package/scripts/deployment/configuration.js +275 -0
- package/scripts/deployment/connection-policy.js +80 -0
- package/scripts/deployment/connection-settings.js +60 -0
- package/scripts/deployment/connections-runtime.js +31 -0
- package/scripts/deployment/connections.js +213 -0
- package/scripts/deployment/controller-compose.js +59 -0
- package/scripts/deployment/database.js +199 -0
- package/scripts/deployment/entrypoint.js +7 -0
- package/scripts/deployment/images.js +17 -0
- package/scripts/deployment/initial-services.js +53 -0
- package/scripts/deployment/launch.js +177 -0
- package/scripts/deployment/logs.js +15 -0
- package/scripts/deployment/model-gateway-compose.js +74 -0
- package/scripts/deployment/model-gateway.js +125 -0
- package/scripts/deployment/models.js +82 -0
- package/scripts/deployment/networks.js +354 -0
- package/scripts/deployment/policy.js +56 -0
- package/scripts/deployment/preflight.js +124 -0
- package/scripts/deployment/prepare.js +205 -0
- package/scripts/deployment/process.js +70 -0
- package/scripts/deployment/relay.js +58 -0
- package/scripts/deployment/runtime-binding.js +100 -0
- package/scripts/deployment/runtime.js +232 -0
- package/scripts/deployment/state.js +154 -0
- package/scripts/deployment/team.js +76 -0
- package/scripts/deployment/upgrade-rpc.py +148 -0
- package/scripts/deployment/upgrade-state.js +47 -0
- package/scripts/deployment/upgrade.js +298 -0
- package/scripts/deployment/volumes.js +24 -0
- package/scripts/errors.js +9 -0
- package/scripts/installation/administrator.js +23 -0
- package/scripts/installation/command.js +176 -0
- package/scripts/installation/configuration.js +104 -0
- package/scripts/installation/configure.js +122 -0
- package/scripts/installation/delete.js +111 -0
- package/scripts/installation/doctor.js +8 -0
- package/scripts/installation/errors.js +8 -0
- package/scripts/installation/files.js +51 -0
- package/scripts/installation/installer/cloud.js +22 -0
- package/scripts/installation/installer/collect.js +245 -0
- package/scripts/installation/installer/inputs.js +59 -0
- package/scripts/installation/installer/menu.js +49 -0
- package/scripts/installation/installer/prompts.js +139 -0
- package/scripts/installation/installer/run.js +251 -0
- package/scripts/installation/installer/secrets.js +16 -0
- package/scripts/installation/installer/sections/access.js +69 -0
- package/scripts/installation/installer/sections/certificates.js +18 -0
- package/scripts/installation/installer/sections/connections.js +4 -0
- package/scripts/installation/installer/sections/external-models.js +35 -0
- package/scripts/installation/installer/sections/models.js +119 -0
- package/scripts/installation/installer/sections/packs.js +82 -0
- package/scripts/installation/installer/sections/resources.js +10 -0
- package/scripts/installation/installer/settings.js +192 -0
- package/scripts/installation/installer/summary.js +44 -0
- package/scripts/installation/lifecycle.js +127 -0
- package/scripts/installation/location.js +3 -0
- package/scripts/installation/models.js +42 -0
- package/scripts/installation/options.js +360 -0
- package/scripts/installation/packs.js +150 -0
- package/scripts/installation/plan.js +121 -0
- package/scripts/installation/prerequisites.js +170 -0
- package/scripts/installation/recipes/catalog.js +55 -0
- package/scripts/installation/recipes/definition.js +32 -0
- package/scripts/installation/reconfigure.js +255 -0
- package/scripts/installation/requirements.js +23 -0
- package/scripts/installation/resolve.js +217 -0
- package/scripts/installation/runtime.js +87 -0
- package/scripts/installation/save.js +95 -0
- package/scripts/installation/setup.js +91 -0
- package/scripts/models/catalog.js +9 -0
- package/scripts/models/configuration.js +184 -0
- package/scripts/models/credentials.js +132 -0
- package/scripts/models/runtime.js +87 -0
- package/scripts/output.js +34 -0
- package/scripts/packs/connections.js +95 -0
- package/scripts/packs/lifecycle.js +125 -0
- package/scripts/packs/model.js +66 -0
- package/scripts/packs/native.js +69 -0
- package/scripts/packs/openshell.js +148 -0
- package/scripts/packs/policy.js +189 -0
- package/scripts/packs/requirements.in +1 -0
- package/scripts/packs/requirements.js +11 -0
- package/scripts/packs/requirements.txt +116 -0
- package/scripts/packs/source.js +90 -0
- package/scripts/packs/transport.py +68 -0
- package/scripts/people.js +52 -0
- package/scripts/release/create.js +62 -0
- package/scripts/release/definition.js +51 -0
- package/scripts/session.js +27 -0
- package/services/access/generated/client.gen.d.ts +12 -0
- package/services/access/generated/client.gen.js +3 -0
- package/services/access/generated/fastify.gen.d.ts +49 -0
- package/services/access/generated/fastify.gen.js +2 -0
- package/services/access/generated/index.d.ts +2 -0
- package/services/access/generated/index.js +2 -0
- package/services/access/generated/sdk.gen.d.ts +28 -0
- package/services/access/generated/sdk.gen.js +107 -0
- package/services/access/generated/types.gen.d.ts +349 -0
- package/services/access/generated/types.gen.js +2 -0
- package/services/access/migrations/001_access.sql +39 -0
- package/services/access/migrations/002_administrator_setup.sql +16 -0
- package/services/access/migrations/003_local_login_completion.sql +5 -0
- package/services/access/migrations/004_invitations.sql +21 -0
- package/services/access/repo/postgres.js +341 -0
- package/services/access/repo/session-logout.js +28 -0
- package/services/access/types/credential.js +2 -0
- package/services/access/types/errors.js +7 -0
- package/services/access/types/ingress.js +1 -0
- package/services/access/types/model.js +1 -0
- package/services/access/types/native-errors.js +7 -0
- package/services/access/types/native.js +1 -0
- package/services/cloud/generated/client.gen.d.ts +12 -0
- package/services/cloud/generated/client.gen.js +3 -0
- package/services/cloud/generated/index.d.ts +2 -0
- package/services/cloud/generated/index.js +2 -0
- package/services/cloud/generated/sdk.gen.d.ts +145 -0
- package/services/cloud/generated/sdk.gen.js +488 -0
- package/services/cloud/generated/types.gen.d.ts +2034 -0
- package/services/cloud/generated/types.gen.js +2 -0
- package/services/connections/cloud/generated/client.gen.d.ts +12 -0
- package/services/connections/cloud/generated/client.gen.js +3 -0
- package/services/connections/cloud/generated/fastify.gen.d.ts +68 -0
- package/services/connections/cloud/generated/fastify.gen.js +2 -0
- package/services/connections/cloud/generated/index.d.ts +2 -0
- package/services/connections/cloud/generated/index.js +2 -0
- package/services/connections/cloud/generated/sdk.gen.d.ts +95 -0
- package/services/connections/cloud/generated/sdk.gen.js +210 -0
- package/services/connections/cloud/generated/types.gen.d.ts +1044 -0
- package/services/connections/cloud/generated/types.gen.js +2 -0
- package/services/connections/migrations/001_connections.sql +220 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { browserDefaults } from "./browser.js";
|
|
2
|
+
/** Compose a fresh native preset. Existing native state is never reconciled through this helper. */
|
|
3
|
+
export function withInitialServices(native, services) {
|
|
4
|
+
const browser = services.browserToken === undefined
|
|
5
|
+
? undefined
|
|
6
|
+
: browserDefaults(services.browserToken);
|
|
7
|
+
return {
|
|
8
|
+
...native,
|
|
9
|
+
...(services.connectionsBrokerUrl
|
|
10
|
+
? {
|
|
11
|
+
secrets: {
|
|
12
|
+
...("secrets" in native ? native.secrets : {}),
|
|
13
|
+
providers: {
|
|
14
|
+
...("secrets" in native ? native.secrets.providers : {}),
|
|
15
|
+
"clawscarf-connections": {
|
|
16
|
+
source: "env",
|
|
17
|
+
allowlist: ["CLAWSCARF_CONNECTIONS_TOKEN"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
plugins: {
|
|
22
|
+
...native.plugins,
|
|
23
|
+
entries: {
|
|
24
|
+
...native.plugins.entries,
|
|
25
|
+
"clawscarf-connections": {
|
|
26
|
+
enabled: true,
|
|
27
|
+
config: {
|
|
28
|
+
brokerUrl: services.connectionsBrokerUrl,
|
|
29
|
+
credential: {
|
|
30
|
+
source: "env",
|
|
31
|
+
provider: "clawscarf-connections",
|
|
32
|
+
id: "CLAWSCARF_CONNECTIONS_TOKEN",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
: {}),
|
|
40
|
+
...(browser && services.browserNode
|
|
41
|
+
? {
|
|
42
|
+
gateway: {
|
|
43
|
+
...native.gateway,
|
|
44
|
+
nodes: {
|
|
45
|
+
pairing: { autoApproveLocal: false },
|
|
46
|
+
browser: { mode: "manual", node: services.browserNode },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
: {}),
|
|
51
|
+
...(browser ? { browser: { ...native.browser, ...browser } } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { applyConnectionPolicy } from "./connection-policy.js";
|
|
2
|
+
import { verifyRuntimeImage } from "./images.js";
|
|
3
|
+
import { startBrowserNode } from "./browser-node-pairing.js";
|
|
4
|
+
import { verifyBrowserNode } from "./browser-node.js";
|
|
5
|
+
import { verifyRelayConfiguration } from "./relay.js";
|
|
6
|
+
import { verifyBrowserListener, verifyBrowserConfiguration, } from "./browser.js";
|
|
7
|
+
import { probeTeamAccess } from "./team.js";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { readState, resourceNames } from "./state.js";
|
|
13
|
+
import { verifyLocalNetworks } from "./networks.js";
|
|
14
|
+
import { compose } from "./compose.js";
|
|
15
|
+
import { ensureRuntime, stopRuntime } from "./runtime.js";
|
|
16
|
+
import { LocalSetupError, run } from "./process.js";
|
|
17
|
+
import { verifyLocalExecutables, verifyLocalPorts } from "./preflight.js";
|
|
18
|
+
import { verifyRuntimeBinding } from "./runtime-binding.js";
|
|
19
|
+
import { requireNoUpgrade } from "./upgrade-state.js";
|
|
20
|
+
/** Internal operation: the caller holds the installation lock for its full lifetime. */
|
|
21
|
+
export async function launchLocal(directoryInput, report, control = {}) {
|
|
22
|
+
const directory = resolve(directoryInput);
|
|
23
|
+
const state = await readState(directory);
|
|
24
|
+
z.strictObject({
|
|
25
|
+
ownerId: z.literal(state.ownerId),
|
|
26
|
+
settingsCandidate: z.string().optional(),
|
|
27
|
+
settingsReapply: z.enum(["models", "connections"]).optional(),
|
|
28
|
+
}).parse(JSON.parse(await readFile(join(directory, "prepared.json"), "utf8")));
|
|
29
|
+
if (Object.keys(process.env).some((key) => key.startsWith("OPENSHELL_")))
|
|
30
|
+
throw new LocalSetupError("configuration_changed", "Unset OPENSHELL_* overrides before starting this installation.");
|
|
31
|
+
const controller = join(directory, "controller");
|
|
32
|
+
const env = {
|
|
33
|
+
...process.env,
|
|
34
|
+
XDG_CONFIG_HOME: join(controller, "config"),
|
|
35
|
+
XDG_STATE_HOME: join(controller, "state"),
|
|
36
|
+
XDG_DATA_HOME: join(controller, "data"),
|
|
37
|
+
};
|
|
38
|
+
const name = resourceNames(state).sandbox;
|
|
39
|
+
await requireNoUpgrade(directory);
|
|
40
|
+
await verifyRuntimeImage(state.input.runtimeImage);
|
|
41
|
+
await verifyLocalExecutables(state);
|
|
42
|
+
await verifyLocalPorts(state);
|
|
43
|
+
await verifyLocalNetworks(directory, state);
|
|
44
|
+
await verifyBrowserConfiguration(directory, state);
|
|
45
|
+
await verifyBrowserNode(directory, state);
|
|
46
|
+
await verifyRelayConfiguration(directory, state);
|
|
47
|
+
const cancellation = new AbortController();
|
|
48
|
+
const stopSignal = () => {
|
|
49
|
+
cancellation.abort();
|
|
50
|
+
};
|
|
51
|
+
const check = () => {
|
|
52
|
+
if (cancellation.signal.aborted)
|
|
53
|
+
throw new LocalSetupError("startup_interrupted", "Startup interrupted. Services already started remain available; use stop to stop the installation.");
|
|
54
|
+
};
|
|
55
|
+
async function waitFor(probe) {
|
|
56
|
+
const deadline = Date.now() + 90_000;
|
|
57
|
+
let lastFailure;
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
check();
|
|
60
|
+
try {
|
|
61
|
+
await probe();
|
|
62
|
+
check();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
check();
|
|
67
|
+
lastFailure = error;
|
|
68
|
+
await delay(500);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
throw lastFailure instanceof Error
|
|
72
|
+
? lastFailure
|
|
73
|
+
: new LocalSetupError("startup_timed_out", "Local startup did not become ready before its deadline.");
|
|
74
|
+
}
|
|
75
|
+
control.signal?.addEventListener("abort", stopSignal, { once: true });
|
|
76
|
+
if (control.signal?.aborted)
|
|
77
|
+
stopSignal();
|
|
78
|
+
process.on("SIGINT", stopSignal);
|
|
79
|
+
process.on("SIGTERM", stopSignal);
|
|
80
|
+
try {
|
|
81
|
+
report("Starting controller…");
|
|
82
|
+
await compose(directory, ["up", "-d", "controller"]);
|
|
83
|
+
await waitFor(async () => {
|
|
84
|
+
await run(state.input.openshellCli, ["sandbox", "list", "--gateway", name, "--output", "json"], { env, timeout: 5000 });
|
|
85
|
+
});
|
|
86
|
+
await applyConnectionPolicy(directory, state, env);
|
|
87
|
+
if (state.input.modelGateway) {
|
|
88
|
+
report("Starting model gateway…");
|
|
89
|
+
await compose(directory, [
|
|
90
|
+
"up",
|
|
91
|
+
"-d",
|
|
92
|
+
"--wait",
|
|
93
|
+
"--wait-timeout",
|
|
94
|
+
"120",
|
|
95
|
+
"models",
|
|
96
|
+
]);
|
|
97
|
+
check();
|
|
98
|
+
}
|
|
99
|
+
if (state.input.browser) {
|
|
100
|
+
report("Starting browser relay…");
|
|
101
|
+
await compose(directory, ["up", "-d", "browser-relay"]);
|
|
102
|
+
const browserPort = state.input.browser.port;
|
|
103
|
+
await waitFor(() => verifyBrowserListener(directory, browserPort));
|
|
104
|
+
}
|
|
105
|
+
report("Starting OpenClaw…");
|
|
106
|
+
const runtime = await ensureRuntime(directory, state, env);
|
|
107
|
+
check();
|
|
108
|
+
await compose(directory, ["up", "-d", "application", "widgets"]);
|
|
109
|
+
await waitFor(async () => {
|
|
110
|
+
const response = await fetch(`http://127.0.0.1:${String(state.input.ports.native)}/healthz`, { signal: AbortSignal.timeout(3000) });
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
throw new LocalSetupError("native_unavailable", "OpenClaw has not become healthy.");
|
|
113
|
+
await response.body?.cancel();
|
|
114
|
+
});
|
|
115
|
+
await verifyRuntimeBinding(state, runtime);
|
|
116
|
+
await applyConnectionPolicy(directory, state, env, true);
|
|
117
|
+
if (state.input.browser) {
|
|
118
|
+
report("Starting native browser node…");
|
|
119
|
+
await startBrowserNode(directory, state, cancellation.signal);
|
|
120
|
+
}
|
|
121
|
+
report("Starting access…");
|
|
122
|
+
await compose(directory, ["up", "-d", "--wait", "companion"]);
|
|
123
|
+
await waitFor(() => probeTeamAccess(directory, state.input, AbortSignal.any([cancellation.signal, AbortSignal.timeout(3000)])));
|
|
124
|
+
check();
|
|
125
|
+
report(`Open ${state.input.team.origin}`);
|
|
126
|
+
await control.activate?.();
|
|
127
|
+
check();
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
control.signal?.removeEventListener("abort", stopSignal);
|
|
131
|
+
process.off("SIGINT", stopSignal);
|
|
132
|
+
process.off("SIGTERM", stopSignal);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Stop entry first, then the owned runtime, and only then its controller. */
|
|
136
|
+
export async function stopLocal(directory) {
|
|
137
|
+
const state = await readState(directory);
|
|
138
|
+
const controller = join(directory, "controller");
|
|
139
|
+
const env = {
|
|
140
|
+
...process.env,
|
|
141
|
+
XDG_CONFIG_HOME: join(controller, "config"),
|
|
142
|
+
XDG_STATE_HOME: join(controller, "state"),
|
|
143
|
+
XDG_DATA_HOME: join(controller, "data"),
|
|
144
|
+
};
|
|
145
|
+
await compose(directory, [
|
|
146
|
+
"stop",
|
|
147
|
+
"companion",
|
|
148
|
+
"application",
|
|
149
|
+
"widgets",
|
|
150
|
+
...(state.input.browser
|
|
151
|
+
? ["browser-node", "browser-node-ingress", "browser-node-dns"]
|
|
152
|
+
: []),
|
|
153
|
+
]);
|
|
154
|
+
// A stopped controller must be available to observe and stop its owned runtime.
|
|
155
|
+
await compose(directory, ["up", "-d", "controller"]);
|
|
156
|
+
const deadline = Date.now() + 30_000;
|
|
157
|
+
for (;;) {
|
|
158
|
+
try {
|
|
159
|
+
await run(state.input.openshellCli, [
|
|
160
|
+
"sandbox",
|
|
161
|
+
"list",
|
|
162
|
+
"--gateway",
|
|
163
|
+
resourceNames(state).sandbox,
|
|
164
|
+
"-o",
|
|
165
|
+
"json",
|
|
166
|
+
], { env, timeout: 5000 });
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (Date.now() >= deadline)
|
|
171
|
+
throw error;
|
|
172
|
+
await delay(500);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await stopRuntime(directory, state, env);
|
|
176
|
+
await compose(directory, ["stop"]);
|
|
177
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const localLogNames = [
|
|
2
|
+
"controller",
|
|
3
|
+
"application",
|
|
4
|
+
"widgets",
|
|
5
|
+
"companion",
|
|
6
|
+
"postgres",
|
|
7
|
+
"models",
|
|
8
|
+
"models-database",
|
|
9
|
+
"browser-relay",
|
|
10
|
+
"browser",
|
|
11
|
+
"browser-egress",
|
|
12
|
+
"browser-node",
|
|
13
|
+
"browser-node-ingress",
|
|
14
|
+
"browser-node-dns",
|
|
15
|
+
];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { postgresImage } from "./images.js";
|
|
3
|
+
export function modelGatewayServices(directory, state) {
|
|
4
|
+
const input = state.input.modelGateway;
|
|
5
|
+
if (!input)
|
|
6
|
+
return {};
|
|
7
|
+
const root = join(directory, "private/models");
|
|
8
|
+
const labels = { "clawscarf.installation": state.ownerId };
|
|
9
|
+
return {
|
|
10
|
+
"models-database": {
|
|
11
|
+
image: postgresImage,
|
|
12
|
+
environment: {
|
|
13
|
+
POSTGRES_DB: "models",
|
|
14
|
+
POSTGRES_USER: "models",
|
|
15
|
+
POSTGRES_PASSWORD_FILE: "/run/secrets/password",
|
|
16
|
+
},
|
|
17
|
+
volumes: [
|
|
18
|
+
"models-database:/var/lib/postgresql/data",
|
|
19
|
+
`${join(root, "database-password")}:/run/secrets/password:ro`,
|
|
20
|
+
],
|
|
21
|
+
healthcheck: {
|
|
22
|
+
test: ["CMD-SHELL", "pg_isready -U models -d models"],
|
|
23
|
+
interval: "2s",
|
|
24
|
+
timeout: "3s",
|
|
25
|
+
retries: 30,
|
|
26
|
+
},
|
|
27
|
+
labels,
|
|
28
|
+
},
|
|
29
|
+
models: {
|
|
30
|
+
image: input.image,
|
|
31
|
+
init: true,
|
|
32
|
+
read_only: true,
|
|
33
|
+
cap_drop: ["ALL"],
|
|
34
|
+
security_opt: ["no-new-privileges:true"],
|
|
35
|
+
tmpfs: ["/tmp:size=256m,mode=1777"],
|
|
36
|
+
command: [
|
|
37
|
+
"--config",
|
|
38
|
+
"/etc/clawscarf/models.json",
|
|
39
|
+
"--host",
|
|
40
|
+
"0.0.0.0",
|
|
41
|
+
"--port",
|
|
42
|
+
"4000",
|
|
43
|
+
"--ssl_keyfile_path",
|
|
44
|
+
"/run/clawscarf/key.pem",
|
|
45
|
+
"--ssl_certfile_path",
|
|
46
|
+
"/run/clawscarf/cert.pem",
|
|
47
|
+
],
|
|
48
|
+
env_file: [{ path: join(root, "gateway.env"), format: "raw" }],
|
|
49
|
+
volumes: [
|
|
50
|
+
`${join(root, "models.json")}:/etc/clawscarf/models.json:ro`,
|
|
51
|
+
`${join(directory, "private/management-key.pem")}:/run/clawscarf/key.pem:ro`,
|
|
52
|
+
`${join(directory, "private/management-cert.pem")}:/run/clawscarf/cert.pem:ro`,
|
|
53
|
+
],
|
|
54
|
+
ports: [`127.0.0.1:${String(input.port)}:4000`],
|
|
55
|
+
depends_on: { "models-database": { condition: "service_healthy" } },
|
|
56
|
+
healthcheck: {
|
|
57
|
+
test: [
|
|
58
|
+
"CMD",
|
|
59
|
+
"python",
|
|
60
|
+
"-c",
|
|
61
|
+
"import ssl,urllib.request; urllib.request.urlopen('https://127.0.0.1:4000/health/liveliness',context=ssl.create_default_context(cafile='/run/clawscarf/cert.pem'),timeout=3)",
|
|
62
|
+
],
|
|
63
|
+
interval: "3s",
|
|
64
|
+
timeout: "5s",
|
|
65
|
+
retries: 40,
|
|
66
|
+
},
|
|
67
|
+
logging: {
|
|
68
|
+
driver: "json-file",
|
|
69
|
+
options: { "max-size": "10m", "max-file": "3" },
|
|
70
|
+
},
|
|
71
|
+
labels,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
import { configurationSchema, gatewayRoutesSchema, liteLlmConfiguration, } from "../models/configuration.js";
|
|
7
|
+
import { issueRuntimeCredential } from "../models/credentials.js";
|
|
8
|
+
import { ensurePrivateFile, writePrivate } from "./state.js";
|
|
9
|
+
import { LocalSetupError } from "./process.js";
|
|
10
|
+
import { compose } from "./compose.js";
|
|
11
|
+
export async function loadGatewayConfiguration(configurationFile, environmentFile) {
|
|
12
|
+
const routes = gatewayRoutesSchema.parse(JSON.parse(await readFile(configurationFile, "utf8")));
|
|
13
|
+
const configuration = configurationSchema.parse({
|
|
14
|
+
...routes,
|
|
15
|
+
mode: "litellm",
|
|
16
|
+
baseUrl: "https://host.docker.internal/v1",
|
|
17
|
+
});
|
|
18
|
+
if (configuration.mode !== "litellm" || !configuration.defaultModel)
|
|
19
|
+
throw new LocalSetupError("invalid_model_setup", "Local models require LiteLLM routes and an enabled default.");
|
|
20
|
+
const environment = parseEnv(await readFile(environmentFile, "utf8"));
|
|
21
|
+
const required = new Set(configuration.models
|
|
22
|
+
.filter((m) => m.enabled)
|
|
23
|
+
.map((m) => m.route?.apiKeyEnv));
|
|
24
|
+
if (Object.keys(environment).some((k) => !required.has(k) ||
|
|
25
|
+
[
|
|
26
|
+
"DATABASE_URL",
|
|
27
|
+
"LITELLM_MASTER_KEY",
|
|
28
|
+
"STORE_MODEL_IN_DB",
|
|
29
|
+
"LITELLM_LOG",
|
|
30
|
+
].includes(k) ||
|
|
31
|
+
/[\r\n]/.test(environment[k] ?? "")) ||
|
|
32
|
+
[...required].some((k) => !k || !environment[k]))
|
|
33
|
+
throw new LocalSetupError("invalid_model_setup", "The upstream environment file must contain exactly the enabled routes' credential variables.");
|
|
34
|
+
return { configuration, environment };
|
|
35
|
+
}
|
|
36
|
+
/** Generate local service credentials once, outside the Gateway and its image. */
|
|
37
|
+
export async function prepareModelGateway(directory, state, options = {}) {
|
|
38
|
+
const input = state.input.modelGateway;
|
|
39
|
+
const publish = options.replaceConfiguration
|
|
40
|
+
? writePrivate
|
|
41
|
+
: ensurePrivateFile;
|
|
42
|
+
if (!input)
|
|
43
|
+
return;
|
|
44
|
+
const loaded = await loadGatewayConfiguration(input.configurationFile, input.upstreamEnvironmentFile);
|
|
45
|
+
const root = join(directory, "private/models");
|
|
46
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
47
|
+
for (const [name, prefix] of [
|
|
48
|
+
["master-key", "sk-"],
|
|
49
|
+
["database-password", ""],
|
|
50
|
+
]) {
|
|
51
|
+
try {
|
|
52
|
+
await readFile(join(root, name));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!(error instanceof Error &&
|
|
56
|
+
"code" in error &&
|
|
57
|
+
error.code === "ENOENT"))
|
|
58
|
+
throw error;
|
|
59
|
+
await ensurePrivateFile(join(root, name), prefix + randomBytes(32).toString("hex"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const password = (await readFile(join(root, "database-password"), "utf8")).trim();
|
|
63
|
+
const master = (await readFile(join(root, "master-key"), "utf8")).trim();
|
|
64
|
+
const environment = {
|
|
65
|
+
...loaded.environment,
|
|
66
|
+
LITELLM_MASTER_KEY: master,
|
|
67
|
+
DATABASE_URL: `postgresql://models:${password}@models-database:5432/models`,
|
|
68
|
+
STORE_MODEL_IN_DB: "False",
|
|
69
|
+
LITELLM_LOG: "ERROR",
|
|
70
|
+
};
|
|
71
|
+
await publish(join(root, "gateway.env"), Object.entries(environment)
|
|
72
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
73
|
+
.join("\n") + "\n");
|
|
74
|
+
await publish(join(root, "models.json"), JSON.stringify(liteLlmConfiguration(loaded.configuration)));
|
|
75
|
+
await publish(join(root, "native.json"), JSON.stringify({
|
|
76
|
+
...loaded.configuration,
|
|
77
|
+
baseUrl: `https://host.docker.internal:${String(input.port)}/v1`,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
export async function prepareModelCredential(directory, state) {
|
|
81
|
+
if (!state.input.modelGateway)
|
|
82
|
+
return;
|
|
83
|
+
const root = join(directory, "private/models");
|
|
84
|
+
const output = join(root, "runtime-key");
|
|
85
|
+
try {
|
|
86
|
+
const key = await readFile(output, "utf8");
|
|
87
|
+
z.string().startsWith("sk-").parse(key.trim());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
const intent = join(root, "issuance-started");
|
|
95
|
+
try {
|
|
96
|
+
await readFile(intent);
|
|
97
|
+
throw new LocalSetupError("model_credential_pending", "Model credential issuance is unconfirmed. Inspect LiteLLM before an explicit retry; installation will not issue another key.");
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
await compose(directory, [
|
|
105
|
+
"up",
|
|
106
|
+
"-d",
|
|
107
|
+
"--wait",
|
|
108
|
+
"--wait-timeout",
|
|
109
|
+
"120",
|
|
110
|
+
"models",
|
|
111
|
+
]);
|
|
112
|
+
const configuration = configurationSchema.parse(JSON.parse(await readFile(join(root, "native.json"), "utf8")));
|
|
113
|
+
await ensurePrivateFile(intent, state.ownerId);
|
|
114
|
+
await issueRuntimeCredential({
|
|
115
|
+
origin: `https://127.0.0.1:${String(state.input.modelGateway.port)}`,
|
|
116
|
+
masterKeyFile: join(root, "master-key"),
|
|
117
|
+
output,
|
|
118
|
+
configuration,
|
|
119
|
+
caFile: join(directory, "private/management-ca.pem"),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await compose(directory, ["stop", "models", "models-database"]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { X509Certificate } from "node:crypto";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { configurationSchema, nativeModelProvider, nativeModelDefaults, } from "../models/configuration.js";
|
|
7
|
+
import { networkRequirementSchema } from "../packs/policy.js";
|
|
8
|
+
import { LocalSetupError } from "./process.js";
|
|
9
|
+
import { ensurePrivateFile } from "./state.js";
|
|
10
|
+
const enabledConfiguration = configurationSchema.refine((value) => value.mode !== "disabled" && value.defaultModel !== null, "Initial model setup requires an enabled gateway and default model.");
|
|
11
|
+
const tokenSchema = z.string().trim().min(1).max(65536);
|
|
12
|
+
/** Validate all optional inputs before allocating resources; never include their values in errors. */
|
|
13
|
+
export async function loadInitialModels(input) {
|
|
14
|
+
if (!input)
|
|
15
|
+
return undefined;
|
|
16
|
+
try {
|
|
17
|
+
const config = enabledConfiguration.parse(JSON.parse(await readFile(input.configurationFile, "utf8")));
|
|
18
|
+
if (config.mode === "disabled" || config.defaultModel === null)
|
|
19
|
+
throw new OperatorError("An initial default is required.");
|
|
20
|
+
const endpoint = new URL(config.baseUrl);
|
|
21
|
+
if (endpoint.protocol !== "https:")
|
|
22
|
+
throw new OperatorError("The runtime requires an HTTPS model gateway.");
|
|
23
|
+
const network = networkRequirementSchema.parse({
|
|
24
|
+
host: endpoint.hostname,
|
|
25
|
+
port: Number(endpoint.port || "443"),
|
|
26
|
+
protocol: "tcp",
|
|
27
|
+
binary: "/usr/local/bin/node",
|
|
28
|
+
});
|
|
29
|
+
const token = tokenSchema.parse(await readFile(input.runtimeKeyFile, "utf8"));
|
|
30
|
+
const ca = input.caFile ? await readFile(input.caFile, "utf8") : undefined;
|
|
31
|
+
// An explicitly trusted self-signed server certificate is also a valid trust anchor.
|
|
32
|
+
if (ca !== undefined)
|
|
33
|
+
new X509Certificate(ca);
|
|
34
|
+
return {
|
|
35
|
+
configuration: { ...config, defaultModel: config.defaultModel },
|
|
36
|
+
credential: { token, ...(ca === undefined ? {} : { ca }) },
|
|
37
|
+
network,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new LocalSetupError("invalid_model_setup", "Initial models require a valid enabled model configuration with a default, an HTTPS DNS endpoint reachable from the runtime, a nonempty scoped key file and, if supplied, a valid trust certificate. Use host.docker.internal for a gateway on this workstation.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Only used to construct a fresh native preset, never to reconcile mutable native state. */
|
|
45
|
+
export function withInitialModels(native, models) {
|
|
46
|
+
if (!models)
|
|
47
|
+
return native;
|
|
48
|
+
return {
|
|
49
|
+
...native,
|
|
50
|
+
secrets: {
|
|
51
|
+
providers: {
|
|
52
|
+
"clawscarf-models": {
|
|
53
|
+
source: "file",
|
|
54
|
+
path: "/home/node/.openclaw/clawscarf-models/initial.json",
|
|
55
|
+
mode: "json",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
models: {
|
|
60
|
+
...native.models,
|
|
61
|
+
providers: {
|
|
62
|
+
clawscarf: {
|
|
63
|
+
...nativeModelProvider(models.configuration),
|
|
64
|
+
apiKey: {
|
|
65
|
+
source: "file",
|
|
66
|
+
provider: "clawscarf-models",
|
|
67
|
+
id: "/token",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
agents: {
|
|
73
|
+
defaults: nativeModelDefaults(models.configuration),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export async function prepareInitialModels(directory, input) {
|
|
78
|
+
const models = await loadInitialModels(input);
|
|
79
|
+
if (models)
|
|
80
|
+
await ensurePrivateFile(join(directory, "private/model-bootstrap.json"), JSON.stringify(models));
|
|
81
|
+
return models;
|
|
82
|
+
}
|