@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,154 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, link, mkdir, mkdtemp, open, readFile, rename, rm, } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import lockfile from "proper-lockfile";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { parseLocalInput } from "./configuration.js";
|
|
8
|
+
import { LocalSetupError } from "./process.js";
|
|
9
|
+
const identitySchema = z.object({
|
|
10
|
+
schemaVersion: z.literal(1),
|
|
11
|
+
ownerId: z.uuid(),
|
|
12
|
+
});
|
|
13
|
+
const stateSchema = identitySchema
|
|
14
|
+
.extend({ input: z.unknown().transform(parseLocalInput) })
|
|
15
|
+
.strict();
|
|
16
|
+
export async function writePrivate(path, value) {
|
|
17
|
+
const temporary = path + "." + randomUUID();
|
|
18
|
+
try {
|
|
19
|
+
const file = await open(temporary, "wx", 0o600);
|
|
20
|
+
try {
|
|
21
|
+
await file.writeFile(value);
|
|
22
|
+
await file.sync();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
await file.close();
|
|
26
|
+
}
|
|
27
|
+
await rename(temporary, path);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await rm(temporary, { force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Publish complete initial state, or retain an identical existing file. Never replace edits. */
|
|
34
|
+
export async function ensurePrivateFile(path, value) {
|
|
35
|
+
try {
|
|
36
|
+
const metadata = await lstat(path);
|
|
37
|
+
if (!metadata.isFile() ||
|
|
38
|
+
(metadata.mode & 0o077) !== 0 ||
|
|
39
|
+
!Buffer.from(value).equals(await readFile(path)))
|
|
40
|
+
throw new LocalSetupError("configuration_changed", "An existing private configuration file differs from setup inputs. Setup will not overwrite it.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const temporary = path + "." + randomUUID();
|
|
48
|
+
try {
|
|
49
|
+
await writePrivate(temporary, value);
|
|
50
|
+
await link(temporary, path);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await rm(temporary, { force: true });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function readStateRecord(directory) {
|
|
57
|
+
const metadata = await lstat(directory);
|
|
58
|
+
if (!metadata.isDirectory() ||
|
|
59
|
+
(metadata.mode & 0o077) !== 0 ||
|
|
60
|
+
metadata.uid !== process.getuid?.())
|
|
61
|
+
throw new LocalSetupError("private_directory_required", "Use a private installation directory owned by the current user.");
|
|
62
|
+
return JSON.parse(await readFile(join(directory, "installation.json"), "utf8"));
|
|
63
|
+
}
|
|
64
|
+
/** Cleanup needs ownership, even when the execution configuration is incomplete. */
|
|
65
|
+
export async function readInstallationIdentity(directory) {
|
|
66
|
+
return identitySchema.parse(await readStateRecord(directory));
|
|
67
|
+
}
|
|
68
|
+
export async function readState(directory) {
|
|
69
|
+
return stateSchema.parse(await readStateRecord(directory));
|
|
70
|
+
}
|
|
71
|
+
export async function initializeState(path, input) {
|
|
72
|
+
const directory = resolve(path);
|
|
73
|
+
try {
|
|
74
|
+
const existing = await readState(directory);
|
|
75
|
+
if (!isDeepStrictEqual(existing.input, input))
|
|
76
|
+
throw new LocalSetupError("configuration_changed", "The supplied configuration differs from this installation. Setup never overwrites an existing installation.");
|
|
77
|
+
return existing;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
81
|
+
throw error;
|
|
82
|
+
try {
|
|
83
|
+
await lstat(directory);
|
|
84
|
+
}
|
|
85
|
+
catch (missing) {
|
|
86
|
+
if (!(missing instanceof Error &&
|
|
87
|
+
"code" in missing &&
|
|
88
|
+
missing.code === "ENOENT"))
|
|
89
|
+
throw missing;
|
|
90
|
+
await mkdir(dirname(directory), { recursive: true, mode: 0o700 });
|
|
91
|
+
const staging = await mkdtemp(join(dirname(directory), ".clawscarf-init-"));
|
|
92
|
+
try {
|
|
93
|
+
const state = {
|
|
94
|
+
schemaVersion: 1,
|
|
95
|
+
ownerId: randomUUID(),
|
|
96
|
+
input,
|
|
97
|
+
};
|
|
98
|
+
await writePrivate(join(staging, "installation.json"), JSON.stringify(state, null, 2) + "\n");
|
|
99
|
+
await mkdir(join(staging, "private"), { mode: 0o700 });
|
|
100
|
+
for (const name of [
|
|
101
|
+
"database-admin-password",
|
|
102
|
+
"database-runtime-password",
|
|
103
|
+
])
|
|
104
|
+
await writePrivate(join(staging, "private", name), randomBytes(32).toString("base64url"));
|
|
105
|
+
await writePrivate(join(staging, "private", "encryption.key"), randomBytes(32));
|
|
106
|
+
// mkdir reserves the destination; rename alone can replace an empty foreign directory.
|
|
107
|
+
await mkdir(directory, { mode: 0o700 });
|
|
108
|
+
await rename(staging, directory);
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await rm(staging, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new LocalSetupError("unowned_directory", "The destination exists without a ClawScarf installation identity; choose a new directory.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export async function withInstallationLock(directory, work) {
|
|
119
|
+
directory = resolve(directory);
|
|
120
|
+
await mkdir(dirname(directory), { recursive: true, mode: 0o700 });
|
|
121
|
+
let release;
|
|
122
|
+
try {
|
|
123
|
+
release = await lockfile.lock(directory, {
|
|
124
|
+
realpath: false,
|
|
125
|
+
retries: 0,
|
|
126
|
+
lockfilePath: directory + ".operator-lock",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (!(error instanceof Error &&
|
|
131
|
+
"code" in error &&
|
|
132
|
+
error.code === "ELOCKED"))
|
|
133
|
+
throw error;
|
|
134
|
+
throw new LocalSetupError("operation_busy", "Another installation operation is active. Wait for it to finish.");
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
return await work();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await release();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function resourceNames(state) {
|
|
144
|
+
const suffix = state.ownerId.replaceAll("-", "").slice(0, 12);
|
|
145
|
+
return {
|
|
146
|
+
project: `clawscarf-${suffix}`,
|
|
147
|
+
sandbox: `cs-${suffix}`,
|
|
148
|
+
volume: `clawscarf-${suffix}-home`,
|
|
149
|
+
databaseVolume: `clawscarf-${suffix}-postgres`,
|
|
150
|
+
browserVolume: `clawscarf-${suffix}-browser`,
|
|
151
|
+
browserNodeVolume: `clawscarf-${suffix}-browser-node`,
|
|
152
|
+
browserNodeConfigVolume: `clawscarf-${suffix}-browser-node-config`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { createPrivateKey, X509Certificate } from "node:crypto";
|
|
3
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
import { get } from "node:https";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { ensurePrivateFile } from "./state.js";
|
|
8
|
+
import { LocalSetupError } from "./process.js";
|
|
9
|
+
export async function readTeamMaterials(team) {
|
|
10
|
+
try {
|
|
11
|
+
for (const path of [
|
|
12
|
+
...(team.keyFile ? [team.keyFile] : []),
|
|
13
|
+
team.clientSecretFile,
|
|
14
|
+
]) {
|
|
15
|
+
const info = await lstat(path);
|
|
16
|
+
if (!info.isFile() ||
|
|
17
|
+
info.uid !== process.getuid?.() ||
|
|
18
|
+
(info.mode & 0o077) !== 0)
|
|
19
|
+
throw new OperatorError("Private operator file required.");
|
|
20
|
+
}
|
|
21
|
+
const secret = (await readFile(team.clientSecretFile, "utf8")).trim();
|
|
22
|
+
if (!secret)
|
|
23
|
+
throw new OperatorError("OIDC secret is empty.");
|
|
24
|
+
if (!team.certificateFile || !team.keyFile)
|
|
25
|
+
return { secret };
|
|
26
|
+
const certificate = await readFile(team.certificateFile);
|
|
27
|
+
const key = await readFile(team.keyFile);
|
|
28
|
+
const cert = new X509Certificate(certificate);
|
|
29
|
+
if (!secret ||
|
|
30
|
+
!cert.checkPrivateKey(createPrivateKey(key)) ||
|
|
31
|
+
Date.parse(cert.validFrom) > Date.now() ||
|
|
32
|
+
Date.parse(cert.validTo) <= Date.now())
|
|
33
|
+
throw new OperatorError("Invalid TLS or OIDC material.");
|
|
34
|
+
for (const origin of [team.origin, team.widgetOrigin]) {
|
|
35
|
+
const host = new URL(origin).hostname.replace(/^\[|\]$/g, "");
|
|
36
|
+
if (!(isIP(host) ? cert.checkIP(host) : cert.checkHost(host)))
|
|
37
|
+
throw new OperatorError("Certificate name mismatch.");
|
|
38
|
+
}
|
|
39
|
+
return { certificate, key, secret };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new LocalSetupError("invalid_team_configuration", "Team setup requires valid TLS for both public names and private, operator-owned key and OIDC secret files.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function prepareTeamFiles(directory, materials) {
|
|
46
|
+
if (materials.certificate) {
|
|
47
|
+
await ensurePrivateFile(join(directory, "application-cert.pem"), materials.certificate);
|
|
48
|
+
await ensurePrivateFile(join(directory, "application-key.pem"), materials.key);
|
|
49
|
+
}
|
|
50
|
+
await ensurePrivateFile(join(directory, "oidc-client-secret"), materials.secret);
|
|
51
|
+
}
|
|
52
|
+
/** Availability only: company login and native authority require the actual OIDC user. */
|
|
53
|
+
export async function probeTeamAccess(directory, input, signal) {
|
|
54
|
+
const origin = input.team.origin;
|
|
55
|
+
const ca = await readFile(join(directory, "private/management-ca.pem"));
|
|
56
|
+
await new Promise((resolve, reject) => {
|
|
57
|
+
get({
|
|
58
|
+
hostname: "127.0.0.1",
|
|
59
|
+
servername: "localhost",
|
|
60
|
+
port: input.ports.management,
|
|
61
|
+
path: "/_clawscarf/health",
|
|
62
|
+
headers: { host: new URL(origin).host },
|
|
63
|
+
ca,
|
|
64
|
+
signal,
|
|
65
|
+
}, (response) => {
|
|
66
|
+
response.resume();
|
|
67
|
+
response.on("error", reject);
|
|
68
|
+
response.on("end", () => {
|
|
69
|
+
if (response.statusCode === 200)
|
|
70
|
+
resolve();
|
|
71
|
+
else
|
|
72
|
+
reject(new LocalSetupError("native_unavailable", "Team access has not become healthy."));
|
|
73
|
+
});
|
|
74
|
+
}).on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Pinned public OpenShell protobuf RPCs absent from its high-level SandboxClient.
|
|
2
|
+
|
|
3
|
+
Operator-only: private mTLS material and snapshots never enter the runtime image.
|
|
4
|
+
The published SDK supplies generated messages; no private client attributes are used.
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import pathlib
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
import grpc
|
|
12
|
+
from openshell._proto import openshell_pb2 as api, openshell_pb2_grpc, sandbox_pb2 as policy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Refused(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def encode(message):
|
|
20
|
+
return base64.b64encode(message.SerializeToString(deterministic=True)).decode("ascii")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def decode(value, kind):
|
|
24
|
+
result = kind()
|
|
25
|
+
result.ParseFromString(base64.b64decode(value, validate=True))
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def current(stub, request):
|
|
30
|
+
result = stub.GetSandbox(api.GetSandboxRequest(name=request["name"], workspace="default"), timeout=15).sandbox
|
|
31
|
+
if result.metadata.id != request["id"] or result.metadata.labels.get("clawscarf.installation") != request["ownerId"]:
|
|
32
|
+
raise Refused("identity_changed")
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def config(stub, sandbox):
|
|
37
|
+
return stub.GetSandboxConfig(policy.GetSandboxConfigRequest(sandbox_id=sandbox.metadata.id), timeout=15)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def no_globals(stub, effective):
|
|
41
|
+
global_settings = stub.GetGatewayConfig(policy.GetGatewayConfigRequest(), timeout=15)
|
|
42
|
+
if any(value.WhichOneof("value") is not None for value in global_settings.settings.values()) or effective.policy_source != policy.POLICY_SOURCE_SANDBOX:
|
|
43
|
+
raise Refused("global_overrides_unsupported")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def snapshot(stub, request):
|
|
47
|
+
sandbox = current(stub, request)
|
|
48
|
+
if sandbox.status.phase != api.SANDBOX_PHASE_STOPPED:
|
|
49
|
+
raise Refused("runtime_not_stopped")
|
|
50
|
+
effective = config(stub, sandbox)
|
|
51
|
+
no_globals(stub, effective)
|
|
52
|
+
revision = stub.GetSandboxPolicyStatus(api.GetSandboxPolicyStatusRequest(name=request["name"], workspace="default"), timeout=15)
|
|
53
|
+
if revision.revision.status != api.POLICY_STATUS_LOADED or revision.active_version != revision.revision.version:
|
|
54
|
+
raise Refused("policy_not_loaded")
|
|
55
|
+
spec = api.SandboxSpec()
|
|
56
|
+
spec.CopyFrom(sandbox.spec)
|
|
57
|
+
spec.policy.CopyFrom(revision.revision.policy)
|
|
58
|
+
if list(spec.command) != ["/app/clawscarf/bin/openclaw", "gateway"]:
|
|
59
|
+
raise Refused("runtime_command_unsupported")
|
|
60
|
+
create = api.CreateSandboxRequest(spec=spec, name=request["name"], workspace="default",
|
|
61
|
+
labels=sandbox.metadata.labels, annotations=sandbox.metadata.annotations)
|
|
62
|
+
result = {"create": encode(create), "effective": encode(effective),
|
|
63
|
+
"resourceVersion": str(sandbox.metadata.resource_version)}
|
|
64
|
+
# Reads are not a transaction: reject a revision changing during the snapshot.
|
|
65
|
+
if current(stub, request).metadata.resource_version != sandbox.metadata.resource_version or encode(config(stub, sandbox)) != result["effective"]:
|
|
66
|
+
raise Refused("configuration_changed")
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def restored(stub, request, expected):
|
|
71
|
+
sandbox = current(stub, request)
|
|
72
|
+
actual = config(stub, sandbox)
|
|
73
|
+
no_globals(stub, actual)
|
|
74
|
+
if actual.policy != expected.policy or actual.settings != expected.settings:
|
|
75
|
+
raise Refused("restoration_unverified")
|
|
76
|
+
original = decode(request["snapshot"]["create"], api.CreateSandboxRequest)
|
|
77
|
+
if list(sandbox.spec.providers) != list(original.spec.providers):
|
|
78
|
+
raise Refused("configuration_changed")
|
|
79
|
+
return {"verified": True}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def perform(stub, request):
|
|
83
|
+
action = request["action"]
|
|
84
|
+
if action == "snapshot":
|
|
85
|
+
return snapshot(stub, request)
|
|
86
|
+
original = decode(request["snapshot"]["create"], api.CreateSandboxRequest)
|
|
87
|
+
expected = decode(request["snapshot"]["effective"], policy.GetSandboxConfigResponse)
|
|
88
|
+
if original.name != request["name"] or original.labels.get("clawscarf.installation") != request["ownerId"]:
|
|
89
|
+
raise Refused("identity_changed")
|
|
90
|
+
if action == "create":
|
|
91
|
+
no_globals(stub, expected)
|
|
92
|
+
original.spec.template.image = request["image"]
|
|
93
|
+
original.spec.environment["CLAWSCARF_START_GATE"] = request["generation"]
|
|
94
|
+
original.labels["clawscarf.upgrade"] = request["generation"]
|
|
95
|
+
result = stub.CreateSandbox(original, timeout=120).sandbox
|
|
96
|
+
return {"id": result.metadata.id}
|
|
97
|
+
if action == "verify":
|
|
98
|
+
return restored(stub, request, expected)
|
|
99
|
+
if action == "restore":
|
|
100
|
+
sandbox = current(stub, request)
|
|
101
|
+
no_globals(stub, config(stub, sandbox))
|
|
102
|
+
for key, setting in expected.settings.items():
|
|
103
|
+
if setting.scope != policy.SETTING_SCOPE_SANDBOX or setting.value.WhichOneof("value") is None:
|
|
104
|
+
continue
|
|
105
|
+
sandbox = current(stub, request)
|
|
106
|
+
observed = config(stub, sandbox).settings.get(key)
|
|
107
|
+
if observed is not None and observed == setting:
|
|
108
|
+
continue
|
|
109
|
+
if observed is not None and observed.value.WhichOneof("value") is not None:
|
|
110
|
+
raise Refused("configuration_changed")
|
|
111
|
+
# The pinned setting-update API has no atomic revision precondition.
|
|
112
|
+
# Explicit resume skips observed matches and reapplies only unset values;
|
|
113
|
+
# exclusive operator control prevents concurrent edits.
|
|
114
|
+
stub.UpdateConfig(api.UpdateConfigRequest(name=request["name"], workspace="default", setting_key=key,
|
|
115
|
+
setting_value=setting.value), timeout=15)
|
|
116
|
+
return restored(stub, request, expected)
|
|
117
|
+
raise Refused("invalid_request")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main():
|
|
121
|
+
try:
|
|
122
|
+
raw = sys.stdin.buffer.read(4 * 1024 * 1024 + 1)
|
|
123
|
+
if len(raw) > 4 * 1024 * 1024:
|
|
124
|
+
raise Refused("invalid_request")
|
|
125
|
+
request = json.loads(raw)
|
|
126
|
+
directory = pathlib.Path(request["controller"])
|
|
127
|
+
settings = json.loads((directory / "controller.json").read_text())
|
|
128
|
+
if settings["name"] != request["name"]:
|
|
129
|
+
raise Refused("identity_changed")
|
|
130
|
+
tls = directory / "tls"
|
|
131
|
+
credentials = grpc.ssl_channel_credentials((tls / "ca.crt").read_bytes(),
|
|
132
|
+
(tls / "client/tls.key").read_bytes(), (tls / "client/tls.crt").read_bytes())
|
|
133
|
+
with grpc.secure_channel(f"127.0.0.1:{settings['port']}", credentials) as channel:
|
|
134
|
+
result = perform(openshell_pb2_grpc.OpenShellStub(channel), request)
|
|
135
|
+
output = json.dumps({"ok": True, "value": result})
|
|
136
|
+
if len(output.encode()) > 4 * 1024 * 1024:
|
|
137
|
+
raise Refused("response_too_large")
|
|
138
|
+
print(output)
|
|
139
|
+
except Refused as error:
|
|
140
|
+
print(json.dumps({"ok": False, "code": str(error)}))
|
|
141
|
+
except grpc.RpcError as error:
|
|
142
|
+
print(json.dumps({"ok": False, "code": error.code().name}))
|
|
143
|
+
except Exception:
|
|
144
|
+
print(json.dumps({"ok": False, "code": "unavailable"}))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { LocalSetupError } from "./process.js";
|
|
5
|
+
export const snapshotSchema = z.strictObject({
|
|
6
|
+
create: z.string().min(1),
|
|
7
|
+
effective: z.string().min(1),
|
|
8
|
+
resourceVersion: z.string().regex(/^[0-9]+$/),
|
|
9
|
+
});
|
|
10
|
+
export const upgradeSchema = z.strictObject({
|
|
11
|
+
ownerId: z.uuid(),
|
|
12
|
+
generation: z.uuid(),
|
|
13
|
+
fromImage: z.string(),
|
|
14
|
+
toImage: z.string(),
|
|
15
|
+
oldId: z.uuid(),
|
|
16
|
+
oldContainerId: z.string().regex(/^[a-f0-9]{64}$/),
|
|
17
|
+
newId: z.uuid().optional(),
|
|
18
|
+
snapshot: snapshotSchema,
|
|
19
|
+
stage: z.enum([
|
|
20
|
+
"prepared",
|
|
21
|
+
"delete_sent",
|
|
22
|
+
"deleted",
|
|
23
|
+
"create_sent",
|
|
24
|
+
"created",
|
|
25
|
+
"restore_sent",
|
|
26
|
+
"restored",
|
|
27
|
+
"stopped",
|
|
28
|
+
"released",
|
|
29
|
+
"committing",
|
|
30
|
+
"complete",
|
|
31
|
+
]),
|
|
32
|
+
});
|
|
33
|
+
export async function readUpgrade(directory) {
|
|
34
|
+
try {
|
|
35
|
+
return upgradeSchema.parse(JSON.parse(await readFile(join(directory, "upgrade.json"), "utf8")));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
39
|
+
return undefined;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function requireNoUpgrade(directory) {
|
|
44
|
+
const upgrade = await readUpgrade(directory);
|
|
45
|
+
if (upgrade && upgrade.stage !== "complete")
|
|
46
|
+
throw new LocalSetupError("upgrade_pending", "An upgrade is unfinished. Resume that upgrade before preparing or starting this installation.");
|
|
47
|
+
}
|