@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,80 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { readFile, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { run } from "./process.js";
|
|
6
|
+
import { resourceNames, writePrivate } from "./state.js";
|
|
7
|
+
import { runtimeManager } from "./runtime.js";
|
|
8
|
+
const ruleSchema = z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
endpoints: z.array(z.object({ host: z.string(), port: z.number(), tls: z.string() })),
|
|
11
|
+
binaries: z.array(z.object({ path: z.string() })),
|
|
12
|
+
});
|
|
13
|
+
const policySchema = z.looseObject({
|
|
14
|
+
network_policies: z.record(z.string(), z.unknown()),
|
|
15
|
+
});
|
|
16
|
+
/** Apply only the explicitly selected broker rule; never regenerate an operator's other policies. */
|
|
17
|
+
export async function applyConnectionPolicy(directory, state, env, verify = false) {
|
|
18
|
+
const pending = join(directory, "private/connection-policy-change.json");
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = await readFile(pending, "utf8");
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
25
|
+
return;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const change = z
|
|
29
|
+
.strictObject({
|
|
30
|
+
ownerId: z.literal(state.ownerId),
|
|
31
|
+
rule: ruleSchema.nullable(),
|
|
32
|
+
})
|
|
33
|
+
.parse(JSON.parse(text));
|
|
34
|
+
const base = join(directory, "private/runtime-policy.json");
|
|
35
|
+
const update = (policy) => {
|
|
36
|
+
if (change.rule)
|
|
37
|
+
policy.network_policies.connections_broker = change.rule;
|
|
38
|
+
else
|
|
39
|
+
delete policy.network_policies.connections_broker;
|
|
40
|
+
return policy;
|
|
41
|
+
};
|
|
42
|
+
const matches = (rule) => {
|
|
43
|
+
if (change.rule === null)
|
|
44
|
+
return rule === undefined;
|
|
45
|
+
const parsed = ruleSchema.safeParse(rule);
|
|
46
|
+
return parsed.success && isDeepStrictEqual(parsed.data, change.rule);
|
|
47
|
+
};
|
|
48
|
+
const name = resourceNames(state).sandbox;
|
|
49
|
+
if (!(await runtimeManager(directory, state, env, run).recorded()).receipt) {
|
|
50
|
+
if (verify)
|
|
51
|
+
throw Error("The configured runtime has not been recorded.");
|
|
52
|
+
await writePrivate(base, JSON.stringify(update(policySchema.parse(JSON.parse(await readFile(base, "utf8"))))));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const observed = z
|
|
56
|
+
.object({
|
|
57
|
+
sandbox: z.literal(name),
|
|
58
|
+
policy_source: z.literal("sandbox"),
|
|
59
|
+
policy: policySchema,
|
|
60
|
+
})
|
|
61
|
+
.parse(JSON.parse(await run(state.input.openshellCli, [
|
|
62
|
+
"policy",
|
|
63
|
+
"get",
|
|
64
|
+
name,
|
|
65
|
+
"--gateway",
|
|
66
|
+
name,
|
|
67
|
+
"--base",
|
|
68
|
+
"--output",
|
|
69
|
+
"json",
|
|
70
|
+
], { env })));
|
|
71
|
+
if (matches(observed.policy.network_policies.connections_broker)) {
|
|
72
|
+
if (verify)
|
|
73
|
+
await rm(pending);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (verify)
|
|
77
|
+
throw Error("The Connections network permission was not confirmed. The installation remains unavailable.");
|
|
78
|
+
await writePrivate(base, JSON.stringify(update(observed.policy)));
|
|
79
|
+
await run(state.input.openshellCli, ["policy", "set", name, "--gateway", name, "--policy", base], { env });
|
|
80
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { companionConfiguration } from "./configuration.js";
|
|
5
|
+
import { loadInitialConnections, prepareInitialConnections, readInitialConnectionToken, } from "./connections.js";
|
|
6
|
+
import { configureConnectionsVolume } from "./connections-runtime.js";
|
|
7
|
+
import { writePrivate } from "./state.js";
|
|
8
|
+
import { initialRuntimePolicy } from "./policy.js";
|
|
9
|
+
/** Explicit capability edit, with the installation stopped and its operator lock held. */
|
|
10
|
+
export async function applyConnectionSettings(directory, state, previous, credentialFile) {
|
|
11
|
+
const loaded = await loadInitialConnections(state.input.connections);
|
|
12
|
+
const endpoint = await prepareInitialConnections(directory, loaded, {
|
|
13
|
+
state,
|
|
14
|
+
apply: true,
|
|
15
|
+
});
|
|
16
|
+
const serverId = z
|
|
17
|
+
.object({ serverId: z.uuid() })
|
|
18
|
+
.parse(JSON.parse(await readFile(join(directory, "identity.json"), "utf8"))).serverId;
|
|
19
|
+
const credential = endpoint
|
|
20
|
+
? {
|
|
21
|
+
token: await readInitialConnectionToken(credentialFile ?? ""),
|
|
22
|
+
...(endpoint.ca ? { ca: endpoint.ca } : {}),
|
|
23
|
+
}
|
|
24
|
+
: undefined;
|
|
25
|
+
const target = endpoint ?? previous;
|
|
26
|
+
if (target) {
|
|
27
|
+
const result = await configureConnectionsVolume(state, {
|
|
28
|
+
kind: endpoint ? "configure" : "disable",
|
|
29
|
+
ownerId: state.ownerId,
|
|
30
|
+
serverId,
|
|
31
|
+
brokerUrl: target.brokerUrl,
|
|
32
|
+
...(endpoint ? { enable: true } : {}),
|
|
33
|
+
...(credential ? { credential } : {}),
|
|
34
|
+
});
|
|
35
|
+
if (endpoint
|
|
36
|
+
? result.state !== "configured" ||
|
|
37
|
+
!result.enabled ||
|
|
38
|
+
result.credentialMatches !== true
|
|
39
|
+
: result.state !== "not_installed" && result.enabled)
|
|
40
|
+
throw Error("Connections settings were not confirmed by OpenClaw.");
|
|
41
|
+
}
|
|
42
|
+
await writePrivate(join(directory, "private/companion.json"), JSON.stringify(companionConfiguration(state.input)));
|
|
43
|
+
// The only Compose mount changed here belongs to Connections. Preserve other services and their addresses.
|
|
44
|
+
const file = join(directory, "compose.json");
|
|
45
|
+
const config = z
|
|
46
|
+
.looseObject({
|
|
47
|
+
services: z.looseObject({
|
|
48
|
+
companion: z.looseObject({ volumes: z.array(z.string()) }),
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
.parse(JSON.parse(await readFile(file, "utf8")));
|
|
52
|
+
const mount = `${join(directory, "private/connections")}:/run/clawscarf/connections:ro`;
|
|
53
|
+
config.services.companion.volumes = config.services.companion.volumes.filter((value) => value !== mount);
|
|
54
|
+
if (loaded?.managementKey)
|
|
55
|
+
config.services.companion.volumes.push(mount);
|
|
56
|
+
await writePrivate(file, JSON.stringify(config));
|
|
57
|
+
const rule = initialRuntimePolicy("network_policies: {}", undefined, endpoint)
|
|
58
|
+
.network_policies.connections_broker ?? null;
|
|
59
|
+
await writePrivate(join(directory, "private/connection-policy-change.json"), JSON.stringify({ ownerId: state.ownerId, rule }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { connectionsConfigurationResultSchema } from "../../runtime/connections-configuration.js";
|
|
2
|
+
import { resourceNames } from "./state.js";
|
|
3
|
+
import { run } from "./process.js";
|
|
4
|
+
/** Caller holds the installation lock and has verified the stopped, owned native volume. */
|
|
5
|
+
export async function configureConnectionsVolume(state, request, command = run) {
|
|
6
|
+
const names = resourceNames(state);
|
|
7
|
+
return connectionsConfigurationResultSchema.parse(JSON.parse(await command("docker", [
|
|
8
|
+
"run",
|
|
9
|
+
"--rm",
|
|
10
|
+
"-i",
|
|
11
|
+
"--pull",
|
|
12
|
+
"never",
|
|
13
|
+
"--network",
|
|
14
|
+
"none",
|
|
15
|
+
"--read-only",
|
|
16
|
+
"--cap-drop",
|
|
17
|
+
"ALL",
|
|
18
|
+
"--security-opt",
|
|
19
|
+
"no-new-privileges:true",
|
|
20
|
+
"--user",
|
|
21
|
+
"1000:1000",
|
|
22
|
+
"--tmpfs",
|
|
23
|
+
"/tmp:rw,nosuid,nodev,size=64m",
|
|
24
|
+
"--mount",
|
|
25
|
+
`type=volume,source=${names.volume},target=/home/node,volume-nocopy${request.kind === "observe" ? ",readonly" : ""}`,
|
|
26
|
+
"--entrypoint",
|
|
27
|
+
"node",
|
|
28
|
+
state.input.runtimeImage,
|
|
29
|
+
"/app/clawscarf/configure-connections-main.js",
|
|
30
|
+
], { input: JSON.stringify(request), timeout: 60_000 })));
|
|
31
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { OperatorError } from "../errors.js";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, readdir, rename, rm, } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { networkRequirementSchema } from "../packs/policy.js";
|
|
8
|
+
import { connectionsBrokerUrlSchema, connectionsInputSchema, } from "./configuration.js";
|
|
9
|
+
import { LocalSetupError } from "./process.js";
|
|
10
|
+
import { ensurePrivateFile, readState } from "./state.js";
|
|
11
|
+
const maximumCertificateBytes = 64 * 1024;
|
|
12
|
+
const endpointSchema = z.strictObject({
|
|
13
|
+
brokerUrl: connectionsBrokerUrlSchema,
|
|
14
|
+
network: networkRequirementSchema,
|
|
15
|
+
ca: z.string().min(1).max(maximumCertificateBytes).optional(),
|
|
16
|
+
});
|
|
17
|
+
export async function readInitialConnectionToken(path) {
|
|
18
|
+
return z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(512)
|
|
22
|
+
.regex(/^[\x21-\x7e]+$/u)
|
|
23
|
+
.parse((await readRegular(path, true, 4096)).toString("utf8").trim());
|
|
24
|
+
}
|
|
25
|
+
const ownerSchema = z.strictObject({
|
|
26
|
+
ownerId: z.uuid(),
|
|
27
|
+
mode: z.literal("external"),
|
|
28
|
+
endpointSha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
29
|
+
});
|
|
30
|
+
function endpoint(brokerUrl, ca) {
|
|
31
|
+
const url = new URL(connectionsBrokerUrlSchema.parse(brokerUrl));
|
|
32
|
+
if (ca !== undefined)
|
|
33
|
+
new X509Certificate(ca);
|
|
34
|
+
return endpointSchema.parse({
|
|
35
|
+
brokerUrl: url.href.replace(/\/$/, ""),
|
|
36
|
+
network: {
|
|
37
|
+
host: url.hostname,
|
|
38
|
+
port: Number(url.port || "443"),
|
|
39
|
+
protocol: "tcp",
|
|
40
|
+
binary: "/usr/local/bin/node",
|
|
41
|
+
},
|
|
42
|
+
...(ca === undefined ? {} : { ca }),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function changed() {
|
|
46
|
+
throw new LocalSetupError("configuration_changed", "The retained Connections configuration differs from this installation's initial inputs. Setup will not replace endpoints, trust certificates or management credentials.");
|
|
47
|
+
}
|
|
48
|
+
async function readRegular(path, privateFile, maximumBytes) {
|
|
49
|
+
if (!(await lstat(path)).isFile())
|
|
50
|
+
throw new OperatorError("Configuration must be a regular file");
|
|
51
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
52
|
+
try {
|
|
53
|
+
const metadata = await file.stat();
|
|
54
|
+
if (!metadata.isFile() ||
|
|
55
|
+
metadata.size > maximumBytes ||
|
|
56
|
+
(privateFile &&
|
|
57
|
+
(metadata.uid !== process.getuid?.() ||
|
|
58
|
+
metadata.nlink !== 1 ||
|
|
59
|
+
(metadata.mode & 0o077) !== 0)))
|
|
60
|
+
throw new OperatorError("Invalid configuration file");
|
|
61
|
+
const bytes = await file.readFile();
|
|
62
|
+
if (bytes.length > maximumBytes)
|
|
63
|
+
throw new OperatorError("Configuration file exceeds its size limit");
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
await file.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function tree(directory, privateFiles, prefix = "") {
|
|
71
|
+
const metadata = await lstat(directory);
|
|
72
|
+
if (!metadata.isDirectory() ||
|
|
73
|
+
(privateFiles &&
|
|
74
|
+
(metadata.uid !== process.getuid?.() || (metadata.mode & 0o077) !== 0)))
|
|
75
|
+
throw new OperatorError("Invalid configuration directory");
|
|
76
|
+
const files = new Map();
|
|
77
|
+
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
78
|
+
const relative = prefix + entry.name;
|
|
79
|
+
const path = join(directory, entry.name);
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
for (const [name, bytes] of await tree(path, privateFiles, relative + "/"))
|
|
82
|
+
files.set(name, bytes);
|
|
83
|
+
}
|
|
84
|
+
else if (entry.isFile())
|
|
85
|
+
files.set(relative, await readRegular(path, privateFiles, 128 * 1024 * 1024));
|
|
86
|
+
else
|
|
87
|
+
throw new OperatorError("Catalog entries must be regular files or directories");
|
|
88
|
+
}
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
async function writeFiles(directory, files) {
|
|
92
|
+
for (const [name, bytes] of files) {
|
|
93
|
+
const path = join(directory, name);
|
|
94
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
95
|
+
await ensurePrivateFile(path, bytes);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Snapshot and verify optional input before installation resource allocation. No provider call is made. */
|
|
99
|
+
export async function loadInitialConnections(input) {
|
|
100
|
+
if (!input)
|
|
101
|
+
return undefined;
|
|
102
|
+
try {
|
|
103
|
+
const checked = connectionsInputSchema.parse(input);
|
|
104
|
+
const ca = checked.caFile
|
|
105
|
+
? new TextDecoder("utf8", { fatal: true }).decode(await readRegular(checked.caFile, false, maximumCertificateBytes))
|
|
106
|
+
: undefined;
|
|
107
|
+
return {
|
|
108
|
+
mode: "external",
|
|
109
|
+
endpoint: endpoint(checked.brokerUrl, ca),
|
|
110
|
+
...(checked.managementKeyFile
|
|
111
|
+
? {
|
|
112
|
+
managementKey: await readInitialConnectionToken(checked.managementKeyFile),
|
|
113
|
+
}
|
|
114
|
+
: {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new LocalSetupError("invalid_connections_setup", "Connections requires an HTTPS DNS endpoint, a private management credential when supplied, and a valid trust certificate when supplied. Configuration files cannot be symbolic links.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Caller holds the installation lock. A complete private snapshot is published once, never refreshed. */
|
|
122
|
+
export async function prepareInitialConnections(directory, loaded, replacement) {
|
|
123
|
+
if (!loaded)
|
|
124
|
+
return;
|
|
125
|
+
const state = replacement?.state ?? (await readState(directory));
|
|
126
|
+
const input = state.input.connections;
|
|
127
|
+
if (input?.mode !== loaded.mode)
|
|
128
|
+
changed();
|
|
129
|
+
if (input.brokerUrl.replace(/\/$/, "") !== loaded.endpoint.brokerUrl)
|
|
130
|
+
changed();
|
|
131
|
+
const runtime = loaded.endpoint;
|
|
132
|
+
const endpointBytes = Buffer.from(JSON.stringify(runtime) + "\n");
|
|
133
|
+
const expected = new Map([
|
|
134
|
+
[
|
|
135
|
+
"owner.json",
|
|
136
|
+
Buffer.from(JSON.stringify({
|
|
137
|
+
ownerId: state.ownerId,
|
|
138
|
+
mode: loaded.mode,
|
|
139
|
+
endpointSha256: createHash("sha256")
|
|
140
|
+
.update(endpointBytes)
|
|
141
|
+
.digest("hex"),
|
|
142
|
+
}) + "\n"),
|
|
143
|
+
],
|
|
144
|
+
["endpoint.json", endpointBytes],
|
|
145
|
+
]);
|
|
146
|
+
if (loaded.managementKey)
|
|
147
|
+
expected.set("management-key", Buffer.from(loaded.managementKey));
|
|
148
|
+
const target = join(directory, "private/connections");
|
|
149
|
+
try {
|
|
150
|
+
await lstat(target);
|
|
151
|
+
const retained = await tree(target, true);
|
|
152
|
+
if (retained.size !== expected.size ||
|
|
153
|
+
[...expected].some(([path, bytes]) => !retained.get(path)?.equals(bytes)))
|
|
154
|
+
changed();
|
|
155
|
+
return runtime;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!(error instanceof Error &&
|
|
159
|
+
"code" in error &&
|
|
160
|
+
error.code === "ENOENT" &&
|
|
161
|
+
"path" in error &&
|
|
162
|
+
error.path === target))
|
|
163
|
+
changed();
|
|
164
|
+
}
|
|
165
|
+
if (replacement && !replacement.apply)
|
|
166
|
+
return runtime;
|
|
167
|
+
const staging = await mkdtemp(join(directory, "private/.connections-"));
|
|
168
|
+
try {
|
|
169
|
+
await writeFiles(staging, expected);
|
|
170
|
+
// Reserve the destination before rename so an existing foreign empty directory is never adopted.
|
|
171
|
+
await mkdir(target, { mode: 0o700 });
|
|
172
|
+
await rename(staging, target);
|
|
173
|
+
return runtime;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
await rm(staging, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Read only retained public endpoint/trust material; never reads a provider or runtime credential. */
|
|
180
|
+
export async function readInitialConnectionsEndpoint(directory) {
|
|
181
|
+
const state = await readState(directory);
|
|
182
|
+
if (!state.input.connections)
|
|
183
|
+
return undefined;
|
|
184
|
+
try {
|
|
185
|
+
const target = join(directory, "private/connections");
|
|
186
|
+
for (const path of [join(directory, "private"), target]) {
|
|
187
|
+
const info = await lstat(path);
|
|
188
|
+
if (!info.isDirectory() ||
|
|
189
|
+
info.uid !== process.getuid?.() ||
|
|
190
|
+
(info.mode & 0o077) !== 0)
|
|
191
|
+
changed();
|
|
192
|
+
}
|
|
193
|
+
const owner = ownerSchema.parse(JSON.parse((await readRegular(join(target, "owner.json"), true, 8192)).toString("utf8")));
|
|
194
|
+
const endpointBytes = await readRegular(join(target, "endpoint.json"), true, 1024 * 1024);
|
|
195
|
+
if (createHash("sha256").update(endpointBytes).digest("hex") !==
|
|
196
|
+
owner.endpointSha256)
|
|
197
|
+
changed();
|
|
198
|
+
const retained = endpointSchema.parse(JSON.parse(endpointBytes.toString("utf8")));
|
|
199
|
+
const input = state.input.connections;
|
|
200
|
+
if (owner.ownerId !== state.ownerId)
|
|
201
|
+
changed();
|
|
202
|
+
const expected = endpoint(input.brokerUrl, retained.ca);
|
|
203
|
+
if (Boolean(input.caFile) !== (retained.ca !== undefined) ||
|
|
204
|
+
retained.brokerUrl !== expected.brokerUrl ||
|
|
205
|
+
retained.ca !== expected.ca ||
|
|
206
|
+
JSON.stringify(retained.network) !== JSON.stringify(expected.network))
|
|
207
|
+
changed();
|
|
208
|
+
return retained;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
changed();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { openshellGatewayImage } from "./images.js";
|
|
3
|
+
import { resourceNames } from "./state.js";
|
|
4
|
+
/** OpenShell alone receives Docker authority. Forwarders receive only client credentials. */
|
|
5
|
+
export function controllerServices(directory, state) {
|
|
6
|
+
const controller = join(directory, "controller");
|
|
7
|
+
const { input } = state;
|
|
8
|
+
const name = resourceNames(state).sandbox;
|
|
9
|
+
const port = input.ports.controller;
|
|
10
|
+
const forward = (target, port) => ({
|
|
11
|
+
image: input.openshellClientImage,
|
|
12
|
+
init: true,
|
|
13
|
+
read_only: true,
|
|
14
|
+
cap_drop: ["ALL"],
|
|
15
|
+
security_opt: ["no-new-privileges:true"],
|
|
16
|
+
tmpfs: ["/tmp"],
|
|
17
|
+
environment: {
|
|
18
|
+
HOME: "/tmp",
|
|
19
|
+
XDG_CONFIG_HOME: "/controller/config",
|
|
20
|
+
XDG_STATE_HOME: "/tmp/state",
|
|
21
|
+
XDG_DATA_HOME: "/tmp/data",
|
|
22
|
+
},
|
|
23
|
+
volumes: [`${join(controller, "config")}:/controller/config:ro`],
|
|
24
|
+
extra_hosts: ["host.openshell.internal:host-gateway"],
|
|
25
|
+
command: [
|
|
26
|
+
"forward",
|
|
27
|
+
"start",
|
|
28
|
+
`0.0.0.0:${String(port)}`,
|
|
29
|
+
target,
|
|
30
|
+
"--gateway",
|
|
31
|
+
name,
|
|
32
|
+
"--gateway-endpoint",
|
|
33
|
+
`https://host.openshell.internal:${String(input.ports.controller)}`,
|
|
34
|
+
],
|
|
35
|
+
ports: [`127.0.0.1:${String(port)}:${String(port)}`],
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
controller: {
|
|
39
|
+
image: openshellGatewayImage,
|
|
40
|
+
user: "0:0",
|
|
41
|
+
command: ["--config", join(controller, "gateway.toml")],
|
|
42
|
+
environment: {
|
|
43
|
+
XDG_CONFIG_HOME: join(controller, "config"),
|
|
44
|
+
XDG_STATE_HOME: join(controller, "state"),
|
|
45
|
+
XDG_DATA_HOME: join(controller, "data"),
|
|
46
|
+
HOME: controller,
|
|
47
|
+
},
|
|
48
|
+
// Identical paths are required: Docker resolves sandbox mounts on its host.
|
|
49
|
+
volumes: [
|
|
50
|
+
`${controller}:${controller}`,
|
|
51
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
52
|
+
],
|
|
53
|
+
ports: [`127.0.0.1:${String(port)}:${String(port)}`],
|
|
54
|
+
extra_hosts: ["host.openshell.internal:host-gateway"],
|
|
55
|
+
},
|
|
56
|
+
application: forward(name, input.ports.native),
|
|
57
|
+
widgets: forward(name, input.ports.nativeWidgets),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { compose } from "./compose.js";
|
|
4
|
+
import { LocalSetupError } from "./process.js";
|
|
5
|
+
import pg from "pg";
|
|
6
|
+
import { runner } from "node-pg-migrate";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
const inputSchema = z.strictObject({
|
|
10
|
+
adminUrl: z.string(),
|
|
11
|
+
runtimePassword: z
|
|
12
|
+
.string()
|
|
13
|
+
.min(24)
|
|
14
|
+
.max(1024)
|
|
15
|
+
.refine((v) => !v.includes("\0")),
|
|
16
|
+
ownerId: z.uuid(),
|
|
17
|
+
});
|
|
18
|
+
export class LocalDatabaseError extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code) {
|
|
21
|
+
super({
|
|
22
|
+
invalid_configuration: "Use the isolated local clawscarf database and private setup credentials.",
|
|
23
|
+
ownership_conflict: "This database or runtime role is not owned by this local installation.",
|
|
24
|
+
runtime_credentials_changed: "The saved runtime credentials do not match this local installation.",
|
|
25
|
+
database_setup_failed: "Local database setup failed. Check the local PostgreSQL service and setup configuration.",
|
|
26
|
+
}[code]);
|
|
27
|
+
this.code = code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const quiet = () => undefined;
|
|
31
|
+
/** Operator-only initialization. Never call from companion startup. */
|
|
32
|
+
export async function initializeLocalDatabase(input) {
|
|
33
|
+
const parsed = inputSchema.safeParse(input);
|
|
34
|
+
if (!parsed.success)
|
|
35
|
+
throw new LocalDatabaseError("invalid_configuration");
|
|
36
|
+
let url;
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(input.adminUrl);
|
|
39
|
+
decodeURIComponent(url.username);
|
|
40
|
+
decodeURIComponent(url.password);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new LocalDatabaseError("invalid_configuration");
|
|
44
|
+
}
|
|
45
|
+
if (!["postgres:", "postgresql:"].includes(url.protocol) ||
|
|
46
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
|
|
47
|
+
url.pathname !== "/clawscarf" ||
|
|
48
|
+
!url.username ||
|
|
49
|
+
!url.password ||
|
|
50
|
+
url.search ||
|
|
51
|
+
url.hash ||
|
|
52
|
+
decodeURIComponent(url.username) === "clawscarf_runtime")
|
|
53
|
+
throw new LocalDatabaseError("invalid_configuration");
|
|
54
|
+
const runtimeUrl = new URL(url);
|
|
55
|
+
runtimeUrl.username = "clawscarf_runtime";
|
|
56
|
+
runtimeUrl.password = encodeURIComponent(input.runtimePassword);
|
|
57
|
+
const client = new pg.Client({ connectionString: url.toString() });
|
|
58
|
+
let connected = false;
|
|
59
|
+
try {
|
|
60
|
+
await client.connect();
|
|
61
|
+
connected = true;
|
|
62
|
+
await client.query("SELECT pg_advisory_lock(17893240, 1)");
|
|
63
|
+
const facts = await client.query(`SELECT
|
|
64
|
+
(SELECT datdba = (SELECT oid FROM pg_roles WHERE rolname = current_user) FROM pg_database WHERE datname = current_database()) AS owned,
|
|
65
|
+
to_regnamespace('clawscarf_operator')::text AS marker,
|
|
66
|
+
EXISTS(SELECT FROM pg_roles WHERE rolname = 'clawscarf_runtime') AS role_exists`);
|
|
67
|
+
const fact = facts.rows[0];
|
|
68
|
+
if (!fact?.owned)
|
|
69
|
+
throw new LocalDatabaseError("ownership_conflict");
|
|
70
|
+
if (!fact.marker) {
|
|
71
|
+
const objects = await client.query(`SELECT
|
|
72
|
+
EXISTS(SELECT FROM pg_namespace WHERE nspname NOT IN ('public', 'information_schema') AND nspname !~ '^pg_')
|
|
73
|
+
OR EXISTS(SELECT FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = 'public')
|
|
74
|
+
OR EXISTS(SELECT FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public')
|
|
75
|
+
OR EXISTS(SELECT FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace WHERE n.nspname = 'public')
|
|
76
|
+
OR EXISTS(SELECT FROM pg_extension WHERE extname != 'plpgsql') AS dirty`);
|
|
77
|
+
if (fact.role_exists || objects.rows[0]?.dirty !== false)
|
|
78
|
+
throw new LocalDatabaseError("ownership_conflict");
|
|
79
|
+
await client.query("BEGIN");
|
|
80
|
+
try {
|
|
81
|
+
await client.query("CREATE SCHEMA clawscarf_operator");
|
|
82
|
+
await client.query("REVOKE ALL ON SCHEMA clawscarf_operator FROM PUBLIC");
|
|
83
|
+
await client.query("CREATE TABLE clawscarf_operator.installation (singleton boolean PRIMARY KEY CHECK(singleton), owner_id uuid NOT NULL)");
|
|
84
|
+
await client.query("INSERT INTO clawscarf_operator.installation VALUES (true, $1)", [input.ownerId]);
|
|
85
|
+
const statement = await client.query("SELECT format('CREATE ROLE clawscarf_runtime LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS PASSWORD %L', $1::text) AS sql", [input.runtimePassword]);
|
|
86
|
+
const sql = statement.rows[0]?.sql;
|
|
87
|
+
if (!sql)
|
|
88
|
+
throw new LocalDatabaseError("database_setup_failed");
|
|
89
|
+
await client.query(sql);
|
|
90
|
+
await client.query("COMMIT");
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
await client.query("ROLLBACK");
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const marker = await client.query(`SELECT owner_id,
|
|
99
|
+
(SELECT nspowner = (SELECT oid FROM pg_roles WHERE rolname = current_user) FROM pg_namespace WHERE nspname = 'clawscarf_operator')
|
|
100
|
+
AND (SELECT relowner = (SELECT oid FROM pg_roles WHERE rolname = current_user) FROM pg_class WHERE oid = 'clawscarf_operator.installation'::regclass) AS operator_owned
|
|
101
|
+
FROM clawscarf_operator.installation WHERE singleton`);
|
|
102
|
+
if (marker.rowCount !== 1 ||
|
|
103
|
+
marker.rows[0]?.owner_id !== input.ownerId ||
|
|
104
|
+
!marker.rows[0].operator_owned ||
|
|
105
|
+
!fact.role_exists)
|
|
106
|
+
throw new LocalDatabaseError("ownership_conflict");
|
|
107
|
+
const runtime = new pg.Client({
|
|
108
|
+
connectionString: runtimeUrl.toString(),
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
await runtime.connect();
|
|
112
|
+
await runtime.query("SELECT 1");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new LocalDatabaseError("runtime_credentials_changed");
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await runtime.end();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const authority = await client.query(`SELECT NOT (rolsuper OR rolcreatedb OR rolcreaterole OR rolinherit OR rolreplication OR rolbypassrls)
|
|
122
|
+
AND NOT EXISTS(SELECT FROM pg_auth_members WHERE member = r.oid)
|
|
123
|
+
AND NOT EXISTS(SELECT FROM pg_database WHERE datdba = r.oid)
|
|
124
|
+
AND NOT EXISTS(SELECT FROM pg_namespace WHERE nspowner = r.oid)
|
|
125
|
+
AND NOT EXISTS(SELECT FROM pg_class WHERE relowner = r.oid) AS safe
|
|
126
|
+
FROM pg_roles r WHERE rolname = 'clawscarf_runtime'`);
|
|
127
|
+
if (!authority.rows[0]?.safe)
|
|
128
|
+
throw new LocalDatabaseError("ownership_conflict");
|
|
129
|
+
await runner({
|
|
130
|
+
dbClient: client,
|
|
131
|
+
dir: fileURLToPath(new URL("../../services/access/migrations", import.meta.url)),
|
|
132
|
+
direction: "up",
|
|
133
|
+
migrationsTable: "clawscarf_access_migrations",
|
|
134
|
+
count: Infinity,
|
|
135
|
+
logger: { debug: quiet, info: quiet, warn: quiet, error: quiet },
|
|
136
|
+
});
|
|
137
|
+
await client.query("BEGIN");
|
|
138
|
+
try {
|
|
139
|
+
await client.query("REVOKE ALL ON DATABASE clawscarf FROM PUBLIC, clawscarf_runtime");
|
|
140
|
+
await client.query("GRANT CONNECT ON DATABASE clawscarf TO clawscarf_runtime");
|
|
141
|
+
await client.query("REVOKE ALL ON SCHEMA public FROM PUBLIC, clawscarf_runtime");
|
|
142
|
+
await client.query("REVOKE ALL ON SCHEMA clawscarf_operator FROM PUBLIC, clawscarf_runtime");
|
|
143
|
+
await client.query("REVOKE ALL ON SCHEMA clawscarf_access FROM PUBLIC, clawscarf_runtime");
|
|
144
|
+
await client.query("GRANT USAGE ON SCHEMA clawscarf_access TO clawscarf_runtime");
|
|
145
|
+
await client.query("REVOKE ALL ON ALL TABLES IN SCHEMA clawscarf_access FROM PUBLIC, clawscarf_runtime");
|
|
146
|
+
await client.query("GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA clawscarf_access TO clawscarf_runtime");
|
|
147
|
+
await client.query("COMMIT");
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
await client.query("ROLLBACK");
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
return { runtimeUrl: runtimeUrl.toString() };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof LocalDatabaseError)
|
|
157
|
+
throw error;
|
|
158
|
+
throw new LocalDatabaseError("database_setup_failed");
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
if (connected)
|
|
162
|
+
await client.end();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Operator-only database preparation; always close its temporary service and pool. */
|
|
166
|
+
export async function withPreparedDatabase(directory, state, operation) {
|
|
167
|
+
try {
|
|
168
|
+
try {
|
|
169
|
+
await compose(directory, [
|
|
170
|
+
"up",
|
|
171
|
+
"-d",
|
|
172
|
+
"--wait",
|
|
173
|
+
"--wait-timeout",
|
|
174
|
+
"90",
|
|
175
|
+
"postgres",
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
throw new LocalSetupError("database_start_failed", "PostgreSQL startup did not confirm success. Check this installation’s Compose logs and Docker network capacity.");
|
|
180
|
+
}
|
|
181
|
+
const adminUrl = new URL(`postgresql://postgres@127.0.0.1:${String(state.input.ports.database)}/clawscarf`);
|
|
182
|
+
adminUrl.password = await readFile(join(directory, "private/database-admin-password"), "utf8");
|
|
183
|
+
const { runtimeUrl } = await initializeLocalDatabase({
|
|
184
|
+
adminUrl: adminUrl.toString(),
|
|
185
|
+
runtimePassword: await readFile(join(directory, "private/database-runtime-password"), "utf8"),
|
|
186
|
+
ownerId: state.ownerId,
|
|
187
|
+
});
|
|
188
|
+
const pool = new pg.Pool({ connectionString: runtimeUrl });
|
|
189
|
+
try {
|
|
190
|
+
return await operation(pool, runtimeUrl);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
await pool.end();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
await compose(directory, ["stop", "postgres"]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
/** Execute the same entry point from a contributor checkout or its compiled artifact. */
|
|
3
|
+
export function nodeEntrypoint(relative) {
|
|
4
|
+
const source = import.meta.url.endsWith(".ts");
|
|
5
|
+
const file = new URL(relative + (source ? ".ts" : ".js"), import.meta.url);
|
|
6
|
+
return [...(source ? ["--import", "tsx"] : []), fileURLToPath(file)];
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import components from "../../release/components.json" with { type: "json" };
|
|
2
|
+
import { LocalSetupError, run } from "./process.js";
|
|
3
|
+
/** Verify the runtime declares the packaged execution contract. */
|
|
4
|
+
export async function verifyRuntimeImage(image, command = run) {
|
|
5
|
+
const model = await command("docker", [
|
|
6
|
+
"image",
|
|
7
|
+
"inspect",
|
|
8
|
+
"--format",
|
|
9
|
+
'{{index .Config.Labels "io.clawscarf.execution-model"}}',
|
|
10
|
+
image,
|
|
11
|
+
]);
|
|
12
|
+
if (model.trim() !== "team-runtime")
|
|
13
|
+
throw new LocalSetupError("configuration_changed", "Use a runtime image built for the team-runtime execution model. The selected image has not declared that contract.");
|
|
14
|
+
}
|
|
15
|
+
export const liteLlmImage = "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf";
|
|
16
|
+
export const postgresImage = "postgres@sha256:742f40ea20b9ff2ff31db5458d127452988a2164df9e17441e191f3b72252193";
|
|
17
|
+
export const openshellGatewayImage = components.openshell.gatewayImage;
|