@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,205 @@
|
|
|
1
|
+
import { openshellGatewayImage, verifyRuntimeImage } from "./images.js";
|
|
2
|
+
import { ensureOwnedVolume } from "./volumes.js";
|
|
3
|
+
import { prepareModelGateway, prepareModelCredential, } from "./model-gateway.js";
|
|
4
|
+
import { prepareBrowserNode, browserNodeName } from "./browser-node.js";
|
|
5
|
+
import { withInitialServices } from "./initial-services.js";
|
|
6
|
+
import { loadInitialConnections, prepareInitialConnections, readInitialConnectionToken, } from "./connections.js";
|
|
7
|
+
import { prepareRelay } from "./relay.js";
|
|
8
|
+
import { prepareRuntimePolicy } from "./policy.js";
|
|
9
|
+
import { prepareBrowser, initializeBrowserVolume } from "./browser.js";
|
|
10
|
+
import { readTeamMaterials, prepareTeamFiles } from "./team.js";
|
|
11
|
+
import { readFile, lstat } from "node:fs/promises";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import { prepareInitialModels, withInitialModels, } from "./models.js";
|
|
14
|
+
import { ensureLocalNetworks } from "./networks.js";
|
|
15
|
+
import { nodeEntrypoint } from "./entrypoint.js";
|
|
16
|
+
import { ensureCertificates } from "./certificates.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { PostgresAccessStore } from "../../services/access/repo/postgres.js";
|
|
19
|
+
import { parseLocalInput, generateLocalConfiguration, } from "./configuration.js";
|
|
20
|
+
import { withPreparedDatabase } from "./database.js";
|
|
21
|
+
import { initializeState, writePrivate, ensurePrivateFile, resourceNames, } from "./state.js";
|
|
22
|
+
import { composeConfiguration } from "./compose.js";
|
|
23
|
+
import { run, LocalSetupError } from "./process.js";
|
|
24
|
+
import { verifyLocalExecutables, verifyLocalPorts } from "./preflight.js";
|
|
25
|
+
import { requireNoUpgrade } from "./upgrade-state.js";
|
|
26
|
+
/** Internal operation: the caller holds the installation lock for its full lifetime. */
|
|
27
|
+
export async function prepareLocal(directoryInput, inputValue, capabilities) {
|
|
28
|
+
const directory = resolve(directoryInput);
|
|
29
|
+
const input = parseLocalInput(inputValue);
|
|
30
|
+
const connections = await loadInitialConnections(input.connections);
|
|
31
|
+
const teamMaterials = await readTeamMaterials(input.team);
|
|
32
|
+
if (process.platform !== "darwin" || process.arch !== "arm64")
|
|
33
|
+
throw new LocalSetupError("platform_unqualified", "This local assembly currently requires macOS arm64 with Docker Desktop; other platforms need qualification.");
|
|
34
|
+
for (const image of [
|
|
35
|
+
input.runtimeImage,
|
|
36
|
+
input.openshellClientImage,
|
|
37
|
+
openshellGatewayImage,
|
|
38
|
+
input.companionImage,
|
|
39
|
+
...(input.modelGateway ? [input.modelGateway.image] : []),
|
|
40
|
+
...(input.relayImage ? [input.relayImage] : []),
|
|
41
|
+
...(input.browser
|
|
42
|
+
? [
|
|
43
|
+
input.browser.image,
|
|
44
|
+
input.browser.egressImage,
|
|
45
|
+
input.browser.nodeImage,
|
|
46
|
+
input.browser.dnsImage,
|
|
47
|
+
]
|
|
48
|
+
: []),
|
|
49
|
+
])
|
|
50
|
+
await run("docker", ["image", "inspect", image]);
|
|
51
|
+
await verifyRuntimeImage(input.runtimeImage);
|
|
52
|
+
const state = await initializeState(directory, input);
|
|
53
|
+
try {
|
|
54
|
+
z.strictObject({
|
|
55
|
+
ownerId: z.literal(state.ownerId),
|
|
56
|
+
settingsCandidate: z.string().optional(),
|
|
57
|
+
settingsReapply: z.enum(["models", "connections"]).optional(),
|
|
58
|
+
}).parse(JSON.parse(await readFile(join(directory, "prepared.json"), "utf8")));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
62
|
+
throw new LocalSetupError("configuration_changed", "A retained settings change is unconfirmed. Inspect it before preparing or starting this installation.");
|
|
63
|
+
}
|
|
64
|
+
await requireNoUpgrade(directory);
|
|
65
|
+
if (capabilities)
|
|
66
|
+
await ensurePrivateFile(join(directory, "inputs.sha256"), capabilities.inputFingerprint);
|
|
67
|
+
await verifyLocalExecutables(state);
|
|
68
|
+
await verifyLocalPorts(state);
|
|
69
|
+
const privateDirectory = join(directory, "private");
|
|
70
|
+
await ensureCertificates(privateDirectory);
|
|
71
|
+
const connectionsEndpoint = await prepareInitialConnections(directory, connections);
|
|
72
|
+
await prepareModelGateway(directory, state);
|
|
73
|
+
await ensureLocalNetworks(directory, state);
|
|
74
|
+
const browser = await prepareBrowser(directory, state);
|
|
75
|
+
const browserMachine = browser
|
|
76
|
+
? await prepareBrowserNode(directory, state, browser.token)
|
|
77
|
+
: undefined;
|
|
78
|
+
await prepareRelay(directory, state);
|
|
79
|
+
const names = resourceNames(state);
|
|
80
|
+
await prepareTeamFiles(privateDirectory, teamMaterials);
|
|
81
|
+
await ensureOwnedVolume(names.databaseVolume, state.ownerId);
|
|
82
|
+
await ensureOwnedVolume(names.volume, state.ownerId);
|
|
83
|
+
if (input.modelGateway)
|
|
84
|
+
await ensureOwnedVolume(`${names.project}-models`, state.ownerId);
|
|
85
|
+
if (browser) {
|
|
86
|
+
await ensureOwnedVolume(names.browserVolume, state.ownerId);
|
|
87
|
+
await initializeBrowserVolume(state, browser.token);
|
|
88
|
+
}
|
|
89
|
+
await ensurePrivateFile(join(directory, "compose.json"), JSON.stringify(composeConfiguration(state, directory, browser?.address, browserMachine), null, 2));
|
|
90
|
+
await prepareModelCredential(directory, state);
|
|
91
|
+
const models = await prepareInitialModels(directory, input.models);
|
|
92
|
+
await prepareRuntimePolicy(directory, models, connectionsEndpoint);
|
|
93
|
+
await withPreparedDatabase(directory, state, async (pool, runtimeUrl) => {
|
|
94
|
+
const store = new PostgresAccessStore(pool, await readFile(join(privateDirectory, "encryption.key")), {
|
|
95
|
+
issuer: input.team.issuer,
|
|
96
|
+
subject: input.team.administratorSubject ?? "urn:clawscarf:unclaimed",
|
|
97
|
+
email: input.team.administratorEmail ?? "",
|
|
98
|
+
claimRequired: !input.team.administratorSubject,
|
|
99
|
+
name: input.administratorName,
|
|
100
|
+
});
|
|
101
|
+
const identity = await store.initialize();
|
|
102
|
+
const connectionCredential = capabilities?.connections && connectionsEndpoint
|
|
103
|
+
? {
|
|
104
|
+
...connectionsEndpoint,
|
|
105
|
+
token: await readInitialConnectionToken(capabilities.connections.credentialFile ?? ""),
|
|
106
|
+
}
|
|
107
|
+
: undefined;
|
|
108
|
+
const insideDatabase = new URL(runtimeUrl);
|
|
109
|
+
insideDatabase.hostname = "postgres";
|
|
110
|
+
insideDatabase.port = "5432";
|
|
111
|
+
const generated = generateLocalConfiguration({
|
|
112
|
+
input,
|
|
113
|
+
directory: privateDirectory,
|
|
114
|
+
encryptionKeyPath: join(privateDirectory, "encryption.key"),
|
|
115
|
+
managementCertificatePath: join(privateDirectory, "management-cert.pem"),
|
|
116
|
+
managementKeyPath: join(privateDirectory, "management-key.pem"),
|
|
117
|
+
runtimeDatabaseUrl: insideDatabase.toString(),
|
|
118
|
+
administratorIdentity: identity.administrator.identity,
|
|
119
|
+
});
|
|
120
|
+
// Native configuration is an initialization input; it is never re-applied on resume.
|
|
121
|
+
await ensurePrivateFile(join(privateDirectory, "access.json"), JSON.stringify(generated.access, null, 2));
|
|
122
|
+
await ensurePrivateFile(join(privateDirectory, "companion.json"), JSON.stringify(generated.companion, null, 2));
|
|
123
|
+
const configured = withInitialModels(generated.native, models);
|
|
124
|
+
const native = JSON.stringify(withInitialServices(configured, {
|
|
125
|
+
...(connectionCredential
|
|
126
|
+
? { connectionsBrokerUrl: connectionCredential.brokerUrl }
|
|
127
|
+
: {}),
|
|
128
|
+
...(browser
|
|
129
|
+
? {
|
|
130
|
+
browserToken: browser.token,
|
|
131
|
+
browserNode: browserNodeName(state),
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
}), null, 2);
|
|
135
|
+
await initializeNativeVolume(state, native, identity.serverId, models?.credential, connectionCredential);
|
|
136
|
+
await writePrivate(join(directory, "identity.json"), JSON.stringify(identity, null, 2));
|
|
137
|
+
});
|
|
138
|
+
const controller = join(directory, "controller");
|
|
139
|
+
try {
|
|
140
|
+
await lstat(controller);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
144
|
+
throw error;
|
|
145
|
+
await run(process.execPath, [
|
|
146
|
+
...nodeEntrypoint("../controller"),
|
|
147
|
+
"init",
|
|
148
|
+
"--directory",
|
|
149
|
+
controller,
|
|
150
|
+
"--gateway",
|
|
151
|
+
input.openshellGateway,
|
|
152
|
+
"--cli",
|
|
153
|
+
input.openshellCli,
|
|
154
|
+
"--name",
|
|
155
|
+
names.sandbox,
|
|
156
|
+
"--port",
|
|
157
|
+
String(input.ports.controller),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
// The existing helper owns controller configuration; partial initialization must be explicit.
|
|
161
|
+
z.object({
|
|
162
|
+
name: z.literal(names.sandbox),
|
|
163
|
+
port: z.literal(input.ports.controller),
|
|
164
|
+
cli: z.literal(input.openshellCli),
|
|
165
|
+
gateway: z.literal(input.openshellGateway),
|
|
166
|
+
}).parse(JSON.parse(await readFile(join(controller, "controller.json"), "utf8")));
|
|
167
|
+
await writePrivate(join(directory, "prepared.json"), JSON.stringify({ ownerId: state.ownerId }));
|
|
168
|
+
return state;
|
|
169
|
+
}
|
|
170
|
+
async function initializeNativeVolume(state, configuration, serverId, modelCredential, connectionsCredential) {
|
|
171
|
+
await run("docker", [
|
|
172
|
+
"run",
|
|
173
|
+
"--rm",
|
|
174
|
+
"-i",
|
|
175
|
+
"--pull",
|
|
176
|
+
"never",
|
|
177
|
+
"--network",
|
|
178
|
+
"none",
|
|
179
|
+
"--user",
|
|
180
|
+
"root",
|
|
181
|
+
"--mount",
|
|
182
|
+
`type=volume,source=${resourceNames(state).volume},target=/home/node,volume-nocopy`,
|
|
183
|
+
"--entrypoint",
|
|
184
|
+
"node",
|
|
185
|
+
state.input.runtimeImage,
|
|
186
|
+
"/app/clawscarf/initialize-main.js",
|
|
187
|
+
], {
|
|
188
|
+
input: JSON.stringify({
|
|
189
|
+
ownerId: state.ownerId,
|
|
190
|
+
serverId,
|
|
191
|
+
configuration,
|
|
192
|
+
...(modelCredential ? { modelCredential } : {}),
|
|
193
|
+
...(connectionsCredential
|
|
194
|
+
? {
|
|
195
|
+
connectionsCredential: {
|
|
196
|
+
token: connectionsCredential.token,
|
|
197
|
+
...(connectionsCredential.ca
|
|
198
|
+
? { ca: connectionsCredential.ca }
|
|
199
|
+
: {}),
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { constants } from "node:os";
|
|
4
|
+
const execute = promisify(execFile);
|
|
5
|
+
export class LocalSetupError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
commandFailure;
|
|
8
|
+
constructor(code, message, commandFailure) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.commandFailure = commandFailure;
|
|
12
|
+
this.name = "LocalSetupError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function commandFailure(error) {
|
|
16
|
+
if (!(error instanceof Error))
|
|
17
|
+
return { reason: "unavailable" };
|
|
18
|
+
const code = "code" in error ? error.code : undefined;
|
|
19
|
+
if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
|
|
20
|
+
return { reason: "output_limit" };
|
|
21
|
+
if ("killed" in error && error.killed === true)
|
|
22
|
+
return { reason: "timeout" };
|
|
23
|
+
if (typeof code === "number" && Number.isSafeInteger(code))
|
|
24
|
+
return { reason: "exit", exitCode: code };
|
|
25
|
+
if ("signal" in error) {
|
|
26
|
+
for (const signal of Object.keys(constants.signals))
|
|
27
|
+
if (error.signal === signal)
|
|
28
|
+
return { reason: "signal", signal };
|
|
29
|
+
}
|
|
30
|
+
if (typeof code === "string" &&
|
|
31
|
+
[
|
|
32
|
+
"ENOENT",
|
|
33
|
+
"EACCES",
|
|
34
|
+
"ENOEXEC",
|
|
35
|
+
"ENOTDIR",
|
|
36
|
+
"EAGAIN",
|
|
37
|
+
"EMFILE",
|
|
38
|
+
"ENFILE",
|
|
39
|
+
].includes(code))
|
|
40
|
+
return { reason: "spawn", systemCode: code };
|
|
41
|
+
return { reason: "unavailable" };
|
|
42
|
+
}
|
|
43
|
+
function failureDetail(failure) {
|
|
44
|
+
switch (failure.reason) {
|
|
45
|
+
case "exit":
|
|
46
|
+
return `exit ${String(failure.exitCode)}`;
|
|
47
|
+
case "signal":
|
|
48
|
+
return failure.signal;
|
|
49
|
+
case "spawn":
|
|
50
|
+
return failure.systemCode;
|
|
51
|
+
default:
|
|
52
|
+
return failure.reason;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Keep subprocess diagnostic payloads out of operator errors: they can contain secrets. */
|
|
56
|
+
export async function run(executable, args, options = {}) {
|
|
57
|
+
try {
|
|
58
|
+
const child = execute(executable, [...args], {
|
|
59
|
+
timeout: options.timeout ?? 60_000,
|
|
60
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
61
|
+
...(options.env ? { env: options.env } : {}),
|
|
62
|
+
});
|
|
63
|
+
child.child.stdin?.end(options.input);
|
|
64
|
+
return (await child).stdout;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const failure = commandFailure(error);
|
|
68
|
+
throw new LocalSetupError("command_failed", `An external command did not confirm success (${failureDetail(failure)}). Inspect this installation before retrying.`, failure);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { chmod, lstat, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { LocalSetupError } from "./process.js";
|
|
4
|
+
export const runtimeRelayHost = "runtime.clawscarf.internal";
|
|
5
|
+
/** Fixed browser TCP destination; authentication remains end-to-end. */
|
|
6
|
+
export const browserRelayConfiguration = `global
|
|
7
|
+
maxconn 256
|
|
8
|
+
nbthread 1
|
|
9
|
+
|
|
10
|
+
defaults
|
|
11
|
+
mode tcp
|
|
12
|
+
timeout connect 5s
|
|
13
|
+
timeout client 0
|
|
14
|
+
timeout server 0
|
|
15
|
+
option clitcpka
|
|
16
|
+
option srvtcpka
|
|
17
|
+
|
|
18
|
+
resolvers docker
|
|
19
|
+
nameserver dns 127.0.0.11:53
|
|
20
|
+
resolve_retries 3
|
|
21
|
+
timeout resolve 1s
|
|
22
|
+
timeout retry 1s
|
|
23
|
+
hold valid 10s
|
|
24
|
+
|
|
25
|
+
listen browser
|
|
26
|
+
bind :9223
|
|
27
|
+
server browser browser:9223 resolvers docker init-addr libc,none
|
|
28
|
+
`;
|
|
29
|
+
async function verifyFile(path, expected) {
|
|
30
|
+
const metadata = await lstat(path);
|
|
31
|
+
if (!metadata.isFile() ||
|
|
32
|
+
metadata.nlink !== 1 ||
|
|
33
|
+
metadata.uid !== process.getuid?.() ||
|
|
34
|
+
(metadata.mode & 0o777) !== 0o444 ||
|
|
35
|
+
(await readFile(path, "utf8")) !== expected)
|
|
36
|
+
throw new LocalSetupError("configuration_changed", "The prepared browser relay differs from its fixed destination.");
|
|
37
|
+
}
|
|
38
|
+
export async function prepareRelay(directory, state) {
|
|
39
|
+
if (!state.input.relayImage)
|
|
40
|
+
return undefined;
|
|
41
|
+
const path = join(directory, "private/browser-relay.cfg");
|
|
42
|
+
const content = browserRelayConfiguration;
|
|
43
|
+
try {
|
|
44
|
+
await verifyFile(path, content);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
48
|
+
throw error;
|
|
49
|
+
await writeFile(path, content, { flag: "wx", mode: 0o444 });
|
|
50
|
+
await chmod(path, 0o444);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Starting a deployment observes its policy; it never repairs changed configuration. */
|
|
54
|
+
export async function verifyRelayConfiguration(directory, state) {
|
|
55
|
+
if (!state.input.relayImage)
|
|
56
|
+
return;
|
|
57
|
+
await verifyFile(join(directory, "private/browser-relay.cfg"), browserRelayConfiguration);
|
|
58
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resourceNames } from "./state.js";
|
|
3
|
+
import { LocalSetupError, run } from "./process.js";
|
|
4
|
+
const imageId = z.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
5
|
+
const containerId = z.string().regex(/^[a-f0-9]{64}$/);
|
|
6
|
+
/** Read actual Docker-driver identity/image/storage before admitting application access. */
|
|
7
|
+
export async function verifyRuntimeBinding(state, target, command = run) {
|
|
8
|
+
const names = resourceNames(state);
|
|
9
|
+
if (target.name !== names.sandbox || !z.uuid().safeParse(target.id).success)
|
|
10
|
+
throw new LocalSetupError("runtime_binding_changed", "The selected runtime does not belong to this installation.");
|
|
11
|
+
try {
|
|
12
|
+
const ids = (await command("docker", [
|
|
13
|
+
"container",
|
|
14
|
+
"ls",
|
|
15
|
+
"--all",
|
|
16
|
+
"--quiet",
|
|
17
|
+
"--no-trunc",
|
|
18
|
+
"--filter",
|
|
19
|
+
`label=openshell.ai/sandbox-id=${target.id}`,
|
|
20
|
+
]))
|
|
21
|
+
.trim()
|
|
22
|
+
.split("\n")
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
const parsed = z.array(containerId).parse(ids);
|
|
25
|
+
if (parsed.length !== 1)
|
|
26
|
+
changed();
|
|
27
|
+
const id = parsed[0];
|
|
28
|
+
if (!id)
|
|
29
|
+
changed();
|
|
30
|
+
const actual = z
|
|
31
|
+
.object({
|
|
32
|
+
Id: containerId,
|
|
33
|
+
Image: imageId,
|
|
34
|
+
Labels: z.record(z.string(), z.string()),
|
|
35
|
+
Mounts: z.array(z.object({
|
|
36
|
+
Type: z.string(),
|
|
37
|
+
Name: z.string().optional(),
|
|
38
|
+
Destination: z.string(),
|
|
39
|
+
RW: z.boolean(),
|
|
40
|
+
})),
|
|
41
|
+
})
|
|
42
|
+
.parse(JSON.parse(await command("docker", [
|
|
43
|
+
"container",
|
|
44
|
+
"inspect",
|
|
45
|
+
"--format",
|
|
46
|
+
'{"Id":{{json .Id}},"Image":{{json .Image}},"Labels":{{json .Config.Labels}},"Mounts":{{json .Mounts}}}',
|
|
47
|
+
id,
|
|
48
|
+
])));
|
|
49
|
+
const expectedImage = imageId.parse((await command("docker", [
|
|
50
|
+
"image",
|
|
51
|
+
"inspect",
|
|
52
|
+
"--format",
|
|
53
|
+
"{{.Id}}",
|
|
54
|
+
state.input.runtimeImage,
|
|
55
|
+
])).trim());
|
|
56
|
+
const labels = {
|
|
57
|
+
"openshell.ai/managed-by": "openshell",
|
|
58
|
+
"openshell.ai/sandbox-id": target.id,
|
|
59
|
+
"openshell.ai/sandbox-name": target.name,
|
|
60
|
+
"openshell.ai/sandbox-namespace": names.sandbox,
|
|
61
|
+
"openshell.ai/sandbox-workspace": "default",
|
|
62
|
+
};
|
|
63
|
+
const homes = actual.Mounts.filter((mount) => mount.Destination === "/home/node");
|
|
64
|
+
const home = homes[0];
|
|
65
|
+
if (actual.Id !== id ||
|
|
66
|
+
actual.Image !== expectedImage ||
|
|
67
|
+
Object.entries(labels).some(([key, value]) => actual.Labels[key] !== value) ||
|
|
68
|
+
homes.length !== 1 ||
|
|
69
|
+
actual.Mounts.some((mount) => mount.Destination.startsWith("/home/node/")) ||
|
|
70
|
+
home?.Type !== "volume" ||
|
|
71
|
+
home.Name !== names.volume ||
|
|
72
|
+
!home.RW)
|
|
73
|
+
changed();
|
|
74
|
+
const volume = z
|
|
75
|
+
.object({
|
|
76
|
+
Name: z.string(),
|
|
77
|
+
Labels: z.record(z.string(), z.string()).nullable(),
|
|
78
|
+
})
|
|
79
|
+
.parse(JSON.parse(await command("docker", [
|
|
80
|
+
"volume",
|
|
81
|
+
"inspect",
|
|
82
|
+
"--format",
|
|
83
|
+
'{"Name":{{json .Name}},"Labels":{{json .Labels}}}',
|
|
84
|
+
names.volume,
|
|
85
|
+
])));
|
|
86
|
+
if (volume.Name !== names.volume ||
|
|
87
|
+
volume.Labels?.["clawscarf.installation"] !== state.ownerId)
|
|
88
|
+
changed();
|
|
89
|
+
return { containerId: actual.Id, imageId: actual.Image };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof LocalSetupError &&
|
|
93
|
+
error.code === "runtime_binding_changed")
|
|
94
|
+
throw error;
|
|
95
|
+
throw new LocalSetupError("runtime_binding_unavailable", "The runtime image and storage could not be verified. Inspect Docker before continuing.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function changed() {
|
|
99
|
+
throw new LocalSetupError("runtime_binding_changed", "The runtime container, image or home volume differs from this installation. Inspect its ownership before continuing.");
|
|
100
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { run, LocalSetupError } from "./process.js";
|
|
5
|
+
import { ensurePrivateFile, writePrivate, resourceNames, } from "./state.js";
|
|
6
|
+
const targetSchema = z.object({
|
|
7
|
+
id: z.uuid(),
|
|
8
|
+
name: z.string(),
|
|
9
|
+
phase: z.string(),
|
|
10
|
+
workspace: z.literal("default"),
|
|
11
|
+
labels: z.record(z.string(), z.string()),
|
|
12
|
+
});
|
|
13
|
+
const intentSchema = z.strictObject({
|
|
14
|
+
ownerId: z.uuid(),
|
|
15
|
+
name: z.string(),
|
|
16
|
+
image: z.string(),
|
|
17
|
+
});
|
|
18
|
+
const receiptSchema = intentSchema.extend({ id: z.uuid() });
|
|
19
|
+
async function readOptional(path) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
25
|
+
return undefined;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function uncertain() {
|
|
30
|
+
throw new LocalSetupError("runtime_outcome_unknown", "Runtime creation was attempted but its owned target is absent. Inspect this installation; setup will not create another runtime automatically.");
|
|
31
|
+
}
|
|
32
|
+
function changed() {
|
|
33
|
+
throw new LocalSetupError("runtime_identity_changed", "The runtime identity or ownership differs from this installation. No further runtime command was sent.");
|
|
34
|
+
}
|
|
35
|
+
export function runtimeManager(directory, state, env, command) {
|
|
36
|
+
const name = resourceNames(state).sandbox;
|
|
37
|
+
const intent = {
|
|
38
|
+
ownerId: state.ownerId,
|
|
39
|
+
name,
|
|
40
|
+
image: state.input.runtimeImage,
|
|
41
|
+
};
|
|
42
|
+
const receiptPath = join(directory, "runtime.json");
|
|
43
|
+
const intentPath = join(directory, "runtime-create.json");
|
|
44
|
+
const shell = (args) => command(state.input.openshellCli, [
|
|
45
|
+
...args,
|
|
46
|
+
"--gateway",
|
|
47
|
+
resourceNames(state).sandbox,
|
|
48
|
+
"--workspace",
|
|
49
|
+
"default",
|
|
50
|
+
], { env, timeout: 120000 });
|
|
51
|
+
async function observe() {
|
|
52
|
+
const matching = [];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const limit = 100;
|
|
55
|
+
for (let offset = 0;; offset += limit) {
|
|
56
|
+
if (offset > 0xffffffff - limit)
|
|
57
|
+
throw new LocalSetupError("runtime_lookup_incomplete", "Runtime lookup did not complete. No absence or creation decision was made.");
|
|
58
|
+
const rows = z
|
|
59
|
+
.array(targetSchema)
|
|
60
|
+
.max(limit)
|
|
61
|
+
.parse(JSON.parse(await shell([
|
|
62
|
+
"sandbox",
|
|
63
|
+
"list",
|
|
64
|
+
"--limit",
|
|
65
|
+
String(limit),
|
|
66
|
+
"--offset",
|
|
67
|
+
String(offset),
|
|
68
|
+
"-o",
|
|
69
|
+
"json",
|
|
70
|
+
])));
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
if (seen.has(row.id))
|
|
73
|
+
throw new LocalSetupError("runtime_lookup_incomplete", "The runtime list changed or repeated while loading. Retry observation before making changes.");
|
|
74
|
+
seen.add(row.id);
|
|
75
|
+
if (row.name === name)
|
|
76
|
+
matching.push(row);
|
|
77
|
+
}
|
|
78
|
+
if (rows.length < limit)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (matching.length > 1)
|
|
82
|
+
changed();
|
|
83
|
+
const row = matching[0];
|
|
84
|
+
if (!row)
|
|
85
|
+
return undefined;
|
|
86
|
+
if (row.labels["clawscarf.installation"] !== state.ownerId)
|
|
87
|
+
changed();
|
|
88
|
+
const current = targetSchema.parse(JSON.parse(await shell(["sandbox", "get", name, "-o", "json"])));
|
|
89
|
+
if (current.name !== name ||
|
|
90
|
+
current.id !== row.id ||
|
|
91
|
+
current.labels["clawscarf.installation"] !== state.ownerId)
|
|
92
|
+
changed();
|
|
93
|
+
return current;
|
|
94
|
+
}
|
|
95
|
+
async function recorded() {
|
|
96
|
+
const pending = await readOptional(intentPath);
|
|
97
|
+
if (pending !== undefined) {
|
|
98
|
+
const value = intentSchema.parse(pending);
|
|
99
|
+
if (value.ownerId !== intent.ownerId ||
|
|
100
|
+
value.name !== name ||
|
|
101
|
+
value.image !== intent.image)
|
|
102
|
+
changed();
|
|
103
|
+
}
|
|
104
|
+
const raw = await readOptional(receiptPath);
|
|
105
|
+
const receipt = raw === undefined ? undefined : receiptSchema.parse(raw);
|
|
106
|
+
if (receipt &&
|
|
107
|
+
(receipt.ownerId !== intent.ownerId ||
|
|
108
|
+
receipt.name !== name ||
|
|
109
|
+
receipt.image !== intent.image))
|
|
110
|
+
changed();
|
|
111
|
+
if (receipt && pending === undefined)
|
|
112
|
+
changed();
|
|
113
|
+
return { pending: pending !== undefined, receipt };
|
|
114
|
+
}
|
|
115
|
+
async function confirm(id) {
|
|
116
|
+
const result = await observe();
|
|
117
|
+
if (!result || result.id !== id)
|
|
118
|
+
changed();
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
name,
|
|
123
|
+
intent,
|
|
124
|
+
intentPath,
|
|
125
|
+
receiptPath,
|
|
126
|
+
shell,
|
|
127
|
+
observe,
|
|
128
|
+
recorded,
|
|
129
|
+
confirm,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Caller holds the installation lock. A persisted create intent is never blindly replayed. */
|
|
133
|
+
export async function ensureRuntime(directory, state, env, command = run) {
|
|
134
|
+
const control = runtimeManager(directory, state, env, command);
|
|
135
|
+
const record = await control.recorded();
|
|
136
|
+
let target = await control.observe();
|
|
137
|
+
if (record.receipt && target?.id !== record.receipt.id)
|
|
138
|
+
changed();
|
|
139
|
+
if (!target) {
|
|
140
|
+
if (record.pending)
|
|
141
|
+
uncertain();
|
|
142
|
+
await ensurePrivateFile(control.intentPath, JSON.stringify(control.intent) + "\n");
|
|
143
|
+
const args = [
|
|
144
|
+
"sandbox",
|
|
145
|
+
"create",
|
|
146
|
+
"--name",
|
|
147
|
+
control.name,
|
|
148
|
+
"--from",
|
|
149
|
+
state.input.runtimeImage,
|
|
150
|
+
"--policy",
|
|
151
|
+
join(directory, "private/runtime-policy.json"),
|
|
152
|
+
"--cpu",
|
|
153
|
+
state.input.cpu,
|
|
154
|
+
"--memory",
|
|
155
|
+
state.input.memory,
|
|
156
|
+
"--driver-config-json",
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
docker: {
|
|
159
|
+
mounts: [
|
|
160
|
+
{
|
|
161
|
+
type: "volume",
|
|
162
|
+
source: resourceNames(state).volume,
|
|
163
|
+
target: "/home/node",
|
|
164
|
+
read_only: false,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
}),
|
|
169
|
+
"--label",
|
|
170
|
+
`clawscarf.installation=${state.ownerId}`,
|
|
171
|
+
"--no-auto-providers",
|
|
172
|
+
"--detach",
|
|
173
|
+
"--no-tty",
|
|
174
|
+
"--gateway",
|
|
175
|
+
resourceNames(state).sandbox,
|
|
176
|
+
"--workspace",
|
|
177
|
+
"default",
|
|
178
|
+
"--",
|
|
179
|
+
"/app/clawscarf/bin/openclaw",
|
|
180
|
+
"gateway",
|
|
181
|
+
];
|
|
182
|
+
// A failed response may follow successful allocation: reconcile observed identity only.
|
|
183
|
+
try {
|
|
184
|
+
await command(state.input.openshellCli, args, { env, timeout: 180000 });
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
target = await control.observe();
|
|
188
|
+
if (!target)
|
|
189
|
+
uncertain();
|
|
190
|
+
}
|
|
191
|
+
target ??= await control.observe();
|
|
192
|
+
if (!target)
|
|
193
|
+
uncertain();
|
|
194
|
+
}
|
|
195
|
+
else if (!record.pending) {
|
|
196
|
+
changed();
|
|
197
|
+
}
|
|
198
|
+
if (!record.receipt)
|
|
199
|
+
await writePrivate(control.receiptPath, JSON.stringify({ ...control.intent, id: target.id }) + "\n");
|
|
200
|
+
if (target.phase === "Error")
|
|
201
|
+
throw new LocalSetupError("runtime_failed", "The owned runtime is in Error. Inspect it before retrying; setup will not replace it.");
|
|
202
|
+
if (target.phase === "Stopped") {
|
|
203
|
+
await control.confirm(target.id);
|
|
204
|
+
await control.shell(["sandbox", "start", target.name]);
|
|
205
|
+
target = await control.confirm(target.id);
|
|
206
|
+
if (target.phase === "Error" || target.phase === "Stopped")
|
|
207
|
+
throw new LocalSetupError("runtime_start_failed", "The owned runtime did not confirm startup. Inspect it before retrying.");
|
|
208
|
+
}
|
|
209
|
+
return { id: target.id, name: target.name, phase: target.phase };
|
|
210
|
+
}
|
|
211
|
+
/** Native start/stop resolve names; identity checks detect replacement, not an atomic UUID precondition. */
|
|
212
|
+
export async function stopRuntime(directory, state, env, command = run) {
|
|
213
|
+
const control = runtimeManager(directory, state, env, command);
|
|
214
|
+
const record = await control.recorded();
|
|
215
|
+
const target = await control.observe();
|
|
216
|
+
if (!target) {
|
|
217
|
+
if (record.pending || record.receipt)
|
|
218
|
+
uncertain();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!record.receipt || record.receipt.id !== target.id)
|
|
222
|
+
changed();
|
|
223
|
+
if (target.phase === "Stopped")
|
|
224
|
+
return;
|
|
225
|
+
if (target.phase === "Error")
|
|
226
|
+
throw new LocalSetupError("runtime_failed", "The owned runtime is in Error. Inspect it before continuing.");
|
|
227
|
+
await control.confirm(target.id);
|
|
228
|
+
await control.shell(["sandbox", "stop", target.name]);
|
|
229
|
+
const stopped = await control.confirm(target.id);
|
|
230
|
+
if (stopped.phase !== "Stopped")
|
|
231
|
+
throw new LocalSetupError("runtime_stop_pending", "The runtime has not confirmed Stopped. Keep its controller running and inspect its state.");
|
|
232
|
+
}
|