@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,132 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { open, readFile, unlink } from "node:fs/promises";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { Agent, fetch } from "undici";
|
|
7
|
+
export async function issueRuntimeCredential(input) {
|
|
8
|
+
if (input.configuration.mode !== "litellm")
|
|
9
|
+
throw new OperatorError("Key provisioning requires bundled LiteLLM mode.");
|
|
10
|
+
const client = await managementClient(input);
|
|
11
|
+
const output = await open(input.output, "wx", 0o600).catch(async (error) => {
|
|
12
|
+
await client.close();
|
|
13
|
+
throw error;
|
|
14
|
+
});
|
|
15
|
+
let stored = false;
|
|
16
|
+
let cleanup;
|
|
17
|
+
try {
|
|
18
|
+
const response = await client.request("/key/generate", {
|
|
19
|
+
method: "POST",
|
|
20
|
+
redirect: "error",
|
|
21
|
+
signal: AbortSignal.timeout(30000),
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${client.masterKey}`,
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
models: input.configuration.models
|
|
28
|
+
.filter((model) => model.enabled)
|
|
29
|
+
.map((model) => model.id),
|
|
30
|
+
key_type: "llm_api",
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
await response.body?.cancel();
|
|
35
|
+
throw new OperatorError("LiteLLM credential provisioning was rejected.");
|
|
36
|
+
}
|
|
37
|
+
const body = await response.json();
|
|
38
|
+
const result = z.object({ key: z.string().startsWith("sk-") }).parse(body);
|
|
39
|
+
await output.writeFile(`${result.key}\n`);
|
|
40
|
+
await output.sync();
|
|
41
|
+
stored = true;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
cleanup = await Promise.allSettled([
|
|
45
|
+
output.close(),
|
|
46
|
+
client.close(),
|
|
47
|
+
...(!stored ? [unlink(input.output)] : []),
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
if (cleanup.some((result) => result.status === "rejected"))
|
|
51
|
+
throw new OperatorError("Runtime key was stored, but local credential cleanup failed.");
|
|
52
|
+
}
|
|
53
|
+
async function managementClient(input) {
|
|
54
|
+
const origin = new URL(input.origin);
|
|
55
|
+
if (origin.origin !== input.origin ||
|
|
56
|
+
origin.username ||
|
|
57
|
+
origin.password ||
|
|
58
|
+
!["http:", "https:"].includes(origin.protocol))
|
|
59
|
+
throw new OperatorError("Use the private LiteLLM management origin.");
|
|
60
|
+
const masterKey = (await readFile(input.masterKeyFile, "utf8")).trim();
|
|
61
|
+
if (!masterKey.startsWith("sk-"))
|
|
62
|
+
throw new OperatorError("Invalid LiteLLM management credential.");
|
|
63
|
+
const dispatcher = input.caFile
|
|
64
|
+
? new Agent({ connect: { ca: await readFile(input.caFile, "utf8") } })
|
|
65
|
+
: undefined;
|
|
66
|
+
return {
|
|
67
|
+
masterKey,
|
|
68
|
+
request: (path, options) => fetch(new URL(path, origin), {
|
|
69
|
+
...options,
|
|
70
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
71
|
+
}),
|
|
72
|
+
close: async () => {
|
|
73
|
+
await dispatcher?.close();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Change only this existing key's model permissions; never create, renew or unblock it. */
|
|
78
|
+
export async function setRuntimeCredentialModels(input) {
|
|
79
|
+
const key = (await readFile(input.keyFile, "utf8")).trim();
|
|
80
|
+
const models = z.array(z.string().min(1)).min(1).parse(input.models).sort();
|
|
81
|
+
const client = await managementClient(input);
|
|
82
|
+
try {
|
|
83
|
+
if (!key.startsWith("sk-") || key === client.masterKey)
|
|
84
|
+
throw new OperatorError("Supply distinct administrator and runtime credentials.");
|
|
85
|
+
const headers = {
|
|
86
|
+
Authorization: `Bearer ${client.masterKey}`,
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
};
|
|
89
|
+
const observe = async () => {
|
|
90
|
+
const response = await client.request(`/key/info?key=${createHash("sha256").update(key).digest("hex")}`, {
|
|
91
|
+
headers,
|
|
92
|
+
redirect: "error",
|
|
93
|
+
signal: AbortSignal.timeout(30_000),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
await response.body?.cancel();
|
|
97
|
+
throw new OperatorError("The retained model credential could not be verified. It will not be replaced.");
|
|
98
|
+
}
|
|
99
|
+
const value = z
|
|
100
|
+
.object({
|
|
101
|
+
info: z.object({
|
|
102
|
+
models: z.array(z.string()),
|
|
103
|
+
blocked: z.boolean().nullable(),
|
|
104
|
+
expires: z.iso.datetime({ offset: true }).nullable(),
|
|
105
|
+
}),
|
|
106
|
+
})
|
|
107
|
+
.parse(await response.json());
|
|
108
|
+
return { ...value.info, models: value.info.models.sort() };
|
|
109
|
+
};
|
|
110
|
+
const previous = await observe();
|
|
111
|
+
if (isDeepStrictEqual(previous.models, models))
|
|
112
|
+
return;
|
|
113
|
+
const response = await client.request("/key/update", {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers,
|
|
116
|
+
redirect: "error",
|
|
117
|
+
signal: AbortSignal.timeout(30_000),
|
|
118
|
+
body: JSON.stringify({ key, models }),
|
|
119
|
+
});
|
|
120
|
+
await response.body?.cancel();
|
|
121
|
+
if (!response.ok)
|
|
122
|
+
throw new OperatorError("LiteLLM rejected the model permission change.");
|
|
123
|
+
const after = await observe();
|
|
124
|
+
if (!isDeepStrictEqual(after.models, models) ||
|
|
125
|
+
after.blocked !== previous.blocked ||
|
|
126
|
+
after.expires !== previous.expires)
|
|
127
|
+
throw new OperatorError("The model permission change was not confirmed. Observe the key before another explicit apply.");
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await client.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { modelResultSchema, ModelConfigurationError, } from "../../runtime/model-contract.js";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { nativeAssignments } from "./configuration.js";
|
|
4
|
+
/** Use the same packaged native helper while its owned home volume is stopped. */
|
|
5
|
+
export async function configureStoppedRuntimeModels(options) {
|
|
6
|
+
return runModelHelper("docker", [
|
|
7
|
+
"run",
|
|
8
|
+
"--rm",
|
|
9
|
+
"-i",
|
|
10
|
+
"--pull",
|
|
11
|
+
"never",
|
|
12
|
+
"--network",
|
|
13
|
+
"none",
|
|
14
|
+
"--read-only",
|
|
15
|
+
"--cap-drop",
|
|
16
|
+
"ALL",
|
|
17
|
+
"--security-opt",
|
|
18
|
+
"no-new-privileges:true",
|
|
19
|
+
"--user",
|
|
20
|
+
"1000:1000",
|
|
21
|
+
"--tmpfs",
|
|
22
|
+
"/tmp:rw,nosuid,nodev,size=64m",
|
|
23
|
+
"--mount",
|
|
24
|
+
`type=volume,source=${options.volume},target=/home/node,volume-nocopy`,
|
|
25
|
+
"--entrypoint",
|
|
26
|
+
"node",
|
|
27
|
+
options.image,
|
|
28
|
+
"/app/clawscarf/models-main.js",
|
|
29
|
+
], {
|
|
30
|
+
assignments: nativeAssignments(options.configuration),
|
|
31
|
+
...options.credential,
|
|
32
|
+
ca: options.credential.ca ?? null,
|
|
33
|
+
apply: options.apply,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Preserve structured helper failures and never replay an uncertain apply. */
|
|
37
|
+
function runModelHelper(executable, args, input) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let output = "";
|
|
40
|
+
const child = spawn(executable, args, {
|
|
41
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
42
|
+
signal: AbortSignal.timeout(60_000),
|
|
43
|
+
});
|
|
44
|
+
let spawned = false;
|
|
45
|
+
const transportFailure = () => new ModelConfigurationError(input.apply && spawned ? "outcome_unknown" : "unavailable");
|
|
46
|
+
child.once("spawn", () => {
|
|
47
|
+
spawned = true;
|
|
48
|
+
});
|
|
49
|
+
child.stdout.setEncoding("utf8");
|
|
50
|
+
child.stdout.on("data", (chunk) => {
|
|
51
|
+
output += chunk;
|
|
52
|
+
if (output.length > 4096) {
|
|
53
|
+
child.kill();
|
|
54
|
+
reject(transportFailure());
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
child.once("error", () => {
|
|
58
|
+
reject(transportFailure());
|
|
59
|
+
});
|
|
60
|
+
child.stdin.on("error", () => {
|
|
61
|
+
reject(transportFailure());
|
|
62
|
+
});
|
|
63
|
+
child.once("close", (code) => {
|
|
64
|
+
try {
|
|
65
|
+
const value = JSON.parse(output);
|
|
66
|
+
const result = modelResultSchema.safeParse(value);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
reject(transportFailure());
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!result.data.ok) {
|
|
72
|
+
reject(new ModelConfigurationError(result.data.error));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (code !== 0) {
|
|
76
|
+
reject(transportFailure());
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
resolve(result.data.state);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
reject(transportFailure());
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
child.stdin.end(JSON.stringify(input));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Human output is deliberately not a scripting contract; use --json for that. */
|
|
2
|
+
export function writeResult(command, value, human) {
|
|
3
|
+
const json = command.optsWithGlobals().json;
|
|
4
|
+
process.stdout.write((json ? JSON.stringify(value, null, 2) : (human ?? describe(value))) + "\n");
|
|
5
|
+
}
|
|
6
|
+
function describe(value, indent = "") {
|
|
7
|
+
if (value === null || value === undefined)
|
|
8
|
+
return "None";
|
|
9
|
+
if (typeof value === "boolean")
|
|
10
|
+
return value ? "Yes" : "No";
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.length
|
|
13
|
+
? value
|
|
14
|
+
.map((item) => `${indent}- ${describe(item, indent + " ").trimStart()}`)
|
|
15
|
+
.join("\n")
|
|
16
|
+
: "None";
|
|
17
|
+
if (typeof value === "string" ||
|
|
18
|
+
typeof value === "number" ||
|
|
19
|
+
typeof value === "bigint")
|
|
20
|
+
return String(value);
|
|
21
|
+
if (typeof value !== "object")
|
|
22
|
+
return "Unavailable";
|
|
23
|
+
return Object.entries(value)
|
|
24
|
+
.map(([key, item]) => {
|
|
25
|
+
const label = key
|
|
26
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
27
|
+
.replaceAll("_", " ");
|
|
28
|
+
const nested = item !== null &&
|
|
29
|
+
typeof item === "object" &&
|
|
30
|
+
(!Array.isArray(item) || item.length > 0);
|
|
31
|
+
return `${indent}${label.charAt(0).toUpperCase()}${label.slice(1)}:${nested ? "\n" : " "}${describe(item, nested ? indent + " " : "")}`;
|
|
32
|
+
})
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { sep } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createClient } from "../../generated/http/client/index.js";
|
|
6
|
+
import { getConnection } from "../../services/connections/cloud/generated/sdk.gen.js";
|
|
7
|
+
const bindingsSchema = z.strictObject({
|
|
8
|
+
origin: z.url(),
|
|
9
|
+
brokerUrl: z.url(),
|
|
10
|
+
sessionFile: z.string(),
|
|
11
|
+
connections: z.record(z.string(), z.uuid()),
|
|
12
|
+
});
|
|
13
|
+
export async function verifyConnections(member, native, sourceRoot, bindingsFile) {
|
|
14
|
+
const connections = [];
|
|
15
|
+
if (member.requirements.connections.length) {
|
|
16
|
+
if (!member.connectionFile)
|
|
17
|
+
throw new OperatorError("A connection-dependent member must declare its native-owned connectionFile.");
|
|
18
|
+
if (!native.supportsConnectionBindings)
|
|
19
|
+
throw new OperatorError("Connection-dependent packs require operator-side verification; do not copy administrator sessions into this runtime.");
|
|
20
|
+
if (!bindingsFile)
|
|
21
|
+
throw new OperatorError("This pack requires explicit connection bindings.");
|
|
22
|
+
const bindings = bindingsSchema.parse(JSON.parse(await readFile(bindingsFile, "utf8")));
|
|
23
|
+
const origin = new URL(bindings.origin);
|
|
24
|
+
if (origin.username ||
|
|
25
|
+
origin.password ||
|
|
26
|
+
origin.search ||
|
|
27
|
+
origin.hash ||
|
|
28
|
+
origin.pathname !== "/")
|
|
29
|
+
throw new OperatorError("Management origin must not contain credentials, path, query or fragment.");
|
|
30
|
+
if (origin.protocol !== "https:" &&
|
|
31
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname))
|
|
32
|
+
throw new OperatorError("Connection bindings require HTTPS or a loopback origin.");
|
|
33
|
+
const broker = await native.brokerUrl();
|
|
34
|
+
if (broker !== bindings.brokerUrl.replace(/\/$/, ""))
|
|
35
|
+
throw new OperatorError("Connection bindings do not match the target runtime broker.");
|
|
36
|
+
for (const privatePath of [bindingsFile, bindings.sessionFile]) {
|
|
37
|
+
const path = await realpath(privatePath);
|
|
38
|
+
if (path === sourceRoot || path.startsWith(sourceRoot + sep))
|
|
39
|
+
throw new OperatorError("Keep connection bindings and administrator sessions outside the pack source.");
|
|
40
|
+
}
|
|
41
|
+
const cookie = (await readFile(bindings.sessionFile, "utf8")).trim();
|
|
42
|
+
if (!/^[A-Za-z0-9_-]+$/.test(cookie))
|
|
43
|
+
throw new OperatorError("Invalid session file.");
|
|
44
|
+
const client = createClient({
|
|
45
|
+
baseUrl: origin.origin,
|
|
46
|
+
redirect: "error",
|
|
47
|
+
headers: { Cookie: `clawscarf_session=${cookie}` },
|
|
48
|
+
});
|
|
49
|
+
for (const required of member.requirements.connections) {
|
|
50
|
+
const id = bindings.connections[required.slot];
|
|
51
|
+
if (!id)
|
|
52
|
+
throw new OperatorError(`Missing connection binding: ${required.slot}`);
|
|
53
|
+
const { data } = await getConnection({
|
|
54
|
+
client,
|
|
55
|
+
path: { connectionId: id },
|
|
56
|
+
throwOnError: true,
|
|
57
|
+
signal: AbortSignal.timeout(10_000),
|
|
58
|
+
});
|
|
59
|
+
const account = z
|
|
60
|
+
.object({
|
|
61
|
+
id: z.uuid(),
|
|
62
|
+
serverId: z.uuid(),
|
|
63
|
+
connectorId: z.string(),
|
|
64
|
+
name: z.string(),
|
|
65
|
+
state: z.string(),
|
|
66
|
+
revision: z.number().int(),
|
|
67
|
+
grant: z.discriminatedUnion("mode", [
|
|
68
|
+
z.object({ mode: z.literal("all") }),
|
|
69
|
+
z.object({
|
|
70
|
+
mode: z.literal("selected"),
|
|
71
|
+
agentIds: z.array(z.string()),
|
|
72
|
+
}),
|
|
73
|
+
]),
|
|
74
|
+
})
|
|
75
|
+
.parse(data);
|
|
76
|
+
if (account.id !== id ||
|
|
77
|
+
account.connectorId !== required.connectorId ||
|
|
78
|
+
account.state !== "connected" ||
|
|
79
|
+
(account.grant.mode === "selected" &&
|
|
80
|
+
!account.grant.agentIds.includes(member.id)))
|
|
81
|
+
throw new OperatorError(`Connection binding is unavailable for ${member.id}: ${required.slot}`);
|
|
82
|
+
if (connections.some((connection) => connection.serverId !== account.serverId))
|
|
83
|
+
throw new OperatorError("Pack account bindings must belong to one server.");
|
|
84
|
+
connections.push({
|
|
85
|
+
slot: required.slot,
|
|
86
|
+
serverId: account.serverId,
|
|
87
|
+
connectionId: id,
|
|
88
|
+
revision: account.revision,
|
|
89
|
+
name: account.name,
|
|
90
|
+
connectorId: account.connectorId,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return connections;
|
|
95
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { openPack, prepareSource } from "./source.js";
|
|
6
|
+
import { requirements } from "./requirements.js";
|
|
7
|
+
import { planSchema } from "./model.js";
|
|
8
|
+
function argumentsFor(operation, root, member, workspace) {
|
|
9
|
+
return operation === "add"
|
|
10
|
+
? [
|
|
11
|
+
"claws",
|
|
12
|
+
"add",
|
|
13
|
+
join(root, member.source),
|
|
14
|
+
"--agent-id",
|
|
15
|
+
member.id,
|
|
16
|
+
"--workspace",
|
|
17
|
+
workspace,
|
|
18
|
+
]
|
|
19
|
+
: operation === "update"
|
|
20
|
+
? ["claws", "update", member.id, "--from", join(root, member.source)]
|
|
21
|
+
: ["claws", "remove", member.id];
|
|
22
|
+
}
|
|
23
|
+
export async function inspectPack(directory, native) {
|
|
24
|
+
await native.version();
|
|
25
|
+
const source = await openPack(directory);
|
|
26
|
+
const targetRoot = await native.source(source.root, source.digest);
|
|
27
|
+
const members = [];
|
|
28
|
+
for (const member of source.manifest.members)
|
|
29
|
+
members.push({
|
|
30
|
+
id: member.id,
|
|
31
|
+
native: await native.run([
|
|
32
|
+
"claws",
|
|
33
|
+
"inspect",
|
|
34
|
+
join(targetRoot, member.source),
|
|
35
|
+
"--json",
|
|
36
|
+
]),
|
|
37
|
+
});
|
|
38
|
+
return { pack: source.manifest, digest: source.digest, members };
|
|
39
|
+
}
|
|
40
|
+
export async function planPack(input, native) {
|
|
41
|
+
await native.version();
|
|
42
|
+
const source = await openPack(input.directory);
|
|
43
|
+
const member = source.manifest.members.find((item) => item.id === input.member);
|
|
44
|
+
if (!member)
|
|
45
|
+
throw new OperatorError("Unknown pack member.");
|
|
46
|
+
const verified = input.operation === "remove"
|
|
47
|
+
? { model: null, connections: [], network: null }
|
|
48
|
+
: await requirements(source.manifest, member, native, input.bindingsFile, source.root);
|
|
49
|
+
const target = await native.target();
|
|
50
|
+
const targetRoot = input.operation === "remove"
|
|
51
|
+
? (input.targetPack ?? source.root)
|
|
52
|
+
: await prepareSource(source, member, verified.connections, native, input.targetPack);
|
|
53
|
+
const workspace = resolve(input.workspace);
|
|
54
|
+
const nativePlan = await native.run([
|
|
55
|
+
...argumentsFor(input.operation, targetRoot, member, workspace),
|
|
56
|
+
"--dry-run",
|
|
57
|
+
"--json",
|
|
58
|
+
]);
|
|
59
|
+
const validated = z
|
|
60
|
+
.object({
|
|
61
|
+
schemaVersion: z.literal({
|
|
62
|
+
add: "openclaw.clawAddPlan.v1",
|
|
63
|
+
update: "openclaw.clawUpdatePlan.v1",
|
|
64
|
+
remove: "openclaw.clawRemovePlan.v1",
|
|
65
|
+
}[input.operation]),
|
|
66
|
+
stability: z.literal("experimental"),
|
|
67
|
+
planIntegrity: z.string(),
|
|
68
|
+
diagnostics: z.array(z.object({ level: z.string() })).optional(),
|
|
69
|
+
summary: z
|
|
70
|
+
.object({
|
|
71
|
+
blockedActions: z.number().optional(),
|
|
72
|
+
blocked: z.number().optional(),
|
|
73
|
+
})
|
|
74
|
+
.optional(),
|
|
75
|
+
})
|
|
76
|
+
.parse(nativePlan);
|
|
77
|
+
if (validated.summary?.blockedActions ||
|
|
78
|
+
validated.summary?.blocked ||
|
|
79
|
+
validated.diagnostics?.some((item) => item.level === "error"))
|
|
80
|
+
throw new OperatorError("Native Claw plan is blocked; inspect it with openclaw claws before applying.");
|
|
81
|
+
return planSchema.parse({
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
pack: source.root,
|
|
84
|
+
packDigest: source.digest,
|
|
85
|
+
target,
|
|
86
|
+
targetPack: targetRoot,
|
|
87
|
+
member: member.id,
|
|
88
|
+
operation: input.operation,
|
|
89
|
+
workspace,
|
|
90
|
+
nativePlan,
|
|
91
|
+
planIntegrity: validated.planIntegrity,
|
|
92
|
+
requirements: verified,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export async function applyPack(value, native, bindingsFile) {
|
|
96
|
+
const plan = planSchema.parse(value);
|
|
97
|
+
if (!isDeepStrictEqual(await native.target(), plan.target))
|
|
98
|
+
throw new OperatorError("Pack target changed; review a new plan.");
|
|
99
|
+
const source = await openPack(plan.pack);
|
|
100
|
+
if (source.digest !== plan.packDigest)
|
|
101
|
+
throw new OperatorError("Pack sources changed; review a new plan.");
|
|
102
|
+
const fresh = await planPack({
|
|
103
|
+
directory: source.root,
|
|
104
|
+
member: plan.member,
|
|
105
|
+
operation: plan.operation,
|
|
106
|
+
workspace: plan.workspace,
|
|
107
|
+
targetPack: plan.targetPack,
|
|
108
|
+
...(bindingsFile ? { bindingsFile } : {}),
|
|
109
|
+
}, native);
|
|
110
|
+
if (fresh.targetPack !== plan.targetPack ||
|
|
111
|
+
!isDeepStrictEqual(fresh.target, plan.target) ||
|
|
112
|
+
fresh.planIntegrity !== plan.planIntegrity ||
|
|
113
|
+
!isDeepStrictEqual(fresh.requirements, plan.requirements))
|
|
114
|
+
throw new OperatorError("Native state or requirements changed; review a new plan.");
|
|
115
|
+
const member = source.manifest.members.find((item) => item.id === plan.member);
|
|
116
|
+
if (!member)
|
|
117
|
+
throw new OperatorError("Pack member disappeared.");
|
|
118
|
+
return native.run([
|
|
119
|
+
...argumentsFor(plan.operation, plan.targetPack, member, plan.workspace),
|
|
120
|
+
"--yes",
|
|
121
|
+
"--plan-integrity",
|
|
122
|
+
plan.planIntegrity,
|
|
123
|
+
"--json",
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { networkRequirementSchema, policyProofSchema } from "./policy.js";
|
|
3
|
+
const id = z.string().regex(/^[a-z][a-z0-9-]{0,63}$/);
|
|
4
|
+
export const packSchema = z.strictObject({
|
|
5
|
+
schemaVersion: z.literal(1),
|
|
6
|
+
id,
|
|
7
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
8
|
+
openclaw: z.literal("2026.9.4"),
|
|
9
|
+
description: z.string().min(1),
|
|
10
|
+
members: z
|
|
11
|
+
.array(z.strictObject({
|
|
12
|
+
id,
|
|
13
|
+
source: z.string().regex(/^[a-z][a-z0-9/-]*$/),
|
|
14
|
+
connectionFile: z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^[a-z][a-z0-9_-]*\.json$/)
|
|
17
|
+
.optional(),
|
|
18
|
+
requirements: z.strictObject({
|
|
19
|
+
model: z.enum(["configured-default", "none"]),
|
|
20
|
+
connections: z.array(z.strictObject({
|
|
21
|
+
slot: id,
|
|
22
|
+
connectorId: z.string().regex(/^[a-z][a-z0-9_]{0,99}$/),
|
|
23
|
+
})),
|
|
24
|
+
}),
|
|
25
|
+
}))
|
|
26
|
+
.min(1)
|
|
27
|
+
.max(32),
|
|
28
|
+
execution: z.strictObject({
|
|
29
|
+
location: z.literal("native-tools"),
|
|
30
|
+
binaries: z.array(z.string().regex(/^[a-zA-Z0-9._+-]+$/)),
|
|
31
|
+
network: z.array(networkRequirementSchema),
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
export const targetSchema = z.discriminatedUnion("kind", [
|
|
35
|
+
z.strictObject({ kind: z.literal("local") }),
|
|
36
|
+
z.strictObject({
|
|
37
|
+
kind: z.literal("openshell"),
|
|
38
|
+
gateway: z.string().min(1),
|
|
39
|
+
sandbox: z.string().min(1),
|
|
40
|
+
sandboxId: z.uuid(),
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
export const planSchema = z.strictObject({
|
|
44
|
+
schemaVersion: z.literal(1),
|
|
45
|
+
pack: z.string(),
|
|
46
|
+
packDigest: z.string(),
|
|
47
|
+
target: targetSchema,
|
|
48
|
+
targetPack: z.string(),
|
|
49
|
+
member: id,
|
|
50
|
+
operation: z.enum(["add", "update", "remove"]),
|
|
51
|
+
workspace: z.string(),
|
|
52
|
+
nativePlan: z.unknown(),
|
|
53
|
+
planIntegrity: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
54
|
+
requirements: z.object({
|
|
55
|
+
model: z.string().nullable(),
|
|
56
|
+
network: policyProofSchema.nullable(),
|
|
57
|
+
connections: z.array(z.object({
|
|
58
|
+
slot: z.string(),
|
|
59
|
+
connectionId: z.string(),
|
|
60
|
+
serverId: z.uuid(),
|
|
61
|
+
revision: z.number().int(),
|
|
62
|
+
name: z.string(),
|
|
63
|
+
connectorId: z.string(),
|
|
64
|
+
})),
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
export class NativeClaws {
|
|
7
|
+
executable;
|
|
8
|
+
supportsConnectionBindings;
|
|
9
|
+
constructor(executable, supportsConnectionBindings = true) {
|
|
10
|
+
this.executable = executable;
|
|
11
|
+
this.supportsConnectionBindings = supportsConnectionBindings;
|
|
12
|
+
}
|
|
13
|
+
target() {
|
|
14
|
+
return Promise.resolve({ kind: "local" });
|
|
15
|
+
}
|
|
16
|
+
source(root, _digest, _existing) {
|
|
17
|
+
return Promise.resolve(root);
|
|
18
|
+
}
|
|
19
|
+
binary(name) {
|
|
20
|
+
return Promise.reject(new Error(`Required binary ${name} needs operator-side verification on the team runtime.`));
|
|
21
|
+
}
|
|
22
|
+
async run(args) {
|
|
23
|
+
if (process.env.OPENCLAW_EXPERIMENTAL_CLAWS !== "1")
|
|
24
|
+
throw new OperatorError("Set OPENCLAW_EXPERIMENTAL_CLAWS=1 to acknowledge the experimental native Claws contract.");
|
|
25
|
+
const { stdout } = await execute(this.executable, [...args], {
|
|
26
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
27
|
+
timeout: 120000,
|
|
28
|
+
});
|
|
29
|
+
const value = JSON.parse(stdout);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
async version() {
|
|
33
|
+
const { stdout } = await execute(this.executable, ["--version"], {
|
|
34
|
+
maxBuffer: 8192,
|
|
35
|
+
timeout: 10000,
|
|
36
|
+
});
|
|
37
|
+
if (!/\b2026\.9\.4\b/.test(stdout))
|
|
38
|
+
throw new OperatorError("Packs require the pinned OpenClaw 2026.9.4 release.");
|
|
39
|
+
}
|
|
40
|
+
network(requirements) {
|
|
41
|
+
if (requirements.length)
|
|
42
|
+
throw new OperatorError("Network-dependent packs require a verified OpenShell target policy.");
|
|
43
|
+
return Promise.resolve(null);
|
|
44
|
+
}
|
|
45
|
+
async brokerUrl() {
|
|
46
|
+
const enabled = await this.run([
|
|
47
|
+
"config",
|
|
48
|
+
"get",
|
|
49
|
+
"plugins.entries.clawscarf-connections.enabled",
|
|
50
|
+
"--json",
|
|
51
|
+
]);
|
|
52
|
+
if (enabled !== true)
|
|
53
|
+
throw new OperatorError("The target Connections plugin is not enabled.");
|
|
54
|
+
return z
|
|
55
|
+
.url()
|
|
56
|
+
.parse(await this.run([
|
|
57
|
+
"config",
|
|
58
|
+
"get",
|
|
59
|
+
"plugins.entries.clawscarf-connections.config.brokerUrl",
|
|
60
|
+
"--json",
|
|
61
|
+
]))
|
|
62
|
+
.replace(/\/$/, "");
|
|
63
|
+
}
|
|
64
|
+
async defaultModel() {
|
|
65
|
+
const status = await this.run(["models", "status", "--json", "--check"]);
|
|
66
|
+
return z.object({ resolvedDefault: z.string().min(1) }).parse(status)
|
|
67
|
+
.resolvedDefault;
|
|
68
|
+
}
|
|
69
|
+
}
|