@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,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { OperatorError } from "./errors.js";
|
|
3
|
+
import { ModelConfigurationError } from "../runtime/model-contract.js";
|
|
4
|
+
import { CommanderError } from "commander";
|
|
5
|
+
import { ZodError } from "zod";
|
|
6
|
+
import { InstallationError } from "./installation/errors.js";
|
|
7
|
+
import { LocalSetupError } from "./deployment/process.js";
|
|
8
|
+
import { LocalDatabaseError } from "./deployment/database.js";
|
|
9
|
+
import { InstallerCancelled } from "./installation/installer/prompts.js";
|
|
10
|
+
import { installationCommand } from "./installation/command.js";
|
|
11
|
+
const program = installationCommand();
|
|
12
|
+
const commands = [program];
|
|
13
|
+
for (const command of commands) {
|
|
14
|
+
command.exitOverride().configureOutput({ outputError: () => { } });
|
|
15
|
+
commands.push(...command.commands);
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await program.parseAsync();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof CommanderError && error.exitCode === 0)
|
|
22
|
+
process.exit(0);
|
|
23
|
+
const known = error instanceof OperatorError ||
|
|
24
|
+
error instanceof InstallationError ||
|
|
25
|
+
error instanceof LocalSetupError ||
|
|
26
|
+
error instanceof LocalDatabaseError ||
|
|
27
|
+
error instanceof ModelConfigurationError;
|
|
28
|
+
const remote = typeof error === "object" &&
|
|
29
|
+
error !== null &&
|
|
30
|
+
"code" in error &&
|
|
31
|
+
typeof error.code === "string" &&
|
|
32
|
+
"detail" in error &&
|
|
33
|
+
typeof error.detail === "string"
|
|
34
|
+
? { code: error.code, detail: error.detail }
|
|
35
|
+
: null;
|
|
36
|
+
const fileFailure = error instanceof Error &&
|
|
37
|
+
"code" in error &&
|
|
38
|
+
"path" in error &&
|
|
39
|
+
typeof error.path === "string" &&
|
|
40
|
+
typeof error.code === "string" &&
|
|
41
|
+
[
|
|
42
|
+
"ENOENT",
|
|
43
|
+
"ENOTDIR",
|
|
44
|
+
"EACCES",
|
|
45
|
+
"EPERM",
|
|
46
|
+
"EEXIST",
|
|
47
|
+
"ELOOP",
|
|
48
|
+
"ENOSPC",
|
|
49
|
+
].includes(error.code)
|
|
50
|
+
? {
|
|
51
|
+
code: error.code,
|
|
52
|
+
detail: `File operation failed (${error.code}): ${JSON.stringify(error.path)}. Check the path, permissions and available disk space.`,
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
const failure = {
|
|
56
|
+
...(error instanceof LocalSetupError && error.commandFailure
|
|
57
|
+
? { command: error.commandFailure }
|
|
58
|
+
: {}),
|
|
59
|
+
code: fileFailure?.code ??
|
|
60
|
+
remote?.code ??
|
|
61
|
+
(error instanceof InstallerCancelled
|
|
62
|
+
? "cancelled"
|
|
63
|
+
: known
|
|
64
|
+
? error.code
|
|
65
|
+
: error instanceof ZodError
|
|
66
|
+
? "invalid_configuration"
|
|
67
|
+
: error instanceof CommanderError
|
|
68
|
+
? "invalid_arguments"
|
|
69
|
+
: "operation_failed"),
|
|
70
|
+
detail: fileFailure?.detail ??
|
|
71
|
+
remote?.detail ??
|
|
72
|
+
(error instanceof InstallerCancelled
|
|
73
|
+
? "Cancelled. Saved files and any running installation are retained."
|
|
74
|
+
: error instanceof ModelConfigurationError
|
|
75
|
+
? `Model configuration failed (${error.code}).${error.code === "outcome_unknown" ? " Inspect native settings before retrying an apply." : ""}`
|
|
76
|
+
: known
|
|
77
|
+
? error.message
|
|
78
|
+
: error instanceof ZodError
|
|
79
|
+
? "Invalid or missing fields: " +
|
|
80
|
+
[
|
|
81
|
+
...new Set(error.issues.map((issue) => issue.path.join(".") || "configuration")),
|
|
82
|
+
].join(", ") +
|
|
83
|
+
"."
|
|
84
|
+
: error instanceof CommanderError
|
|
85
|
+
? error.message
|
|
86
|
+
: "Check the configuration, file permissions and local prerequisites. No operation was automatically retried."),
|
|
87
|
+
};
|
|
88
|
+
process.stderr.write((program.opts().json
|
|
89
|
+
? JSON.stringify(failure)
|
|
90
|
+
: `Error: ${failure.detail}`) + "\n");
|
|
91
|
+
process.exitCode = error instanceof InstallerCancelled ? 130 : 1;
|
|
92
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as oidc from "openid-client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import { createClient } from "../../generated/http/client/index.js";
|
|
6
|
+
import { getCliIdentity } from "../../services/cloud/generated/sdk.gen.js";
|
|
7
|
+
import { cloudUrlSchema } from "./url.js";
|
|
8
|
+
import { readInputFile } from "../installation/files.js";
|
|
9
|
+
import { writePrivate } from "../deployment/state.js";
|
|
10
|
+
import { InstallationError } from "../installation/errors.js";
|
|
11
|
+
const pendingSchema = z.strictObject({
|
|
12
|
+
cloudUrl: cloudUrlSchema,
|
|
13
|
+
issuer: z.url(),
|
|
14
|
+
clientId: z.string().min(1),
|
|
15
|
+
deviceCode: z.string().min(1),
|
|
16
|
+
userCode: z.string().regex(/^[A-Z0-9]{4}-[A-Z0-9]{4}$/),
|
|
17
|
+
expiresAt: z.number(),
|
|
18
|
+
interval: z.number().positive(),
|
|
19
|
+
nextPollAt: z.number(),
|
|
20
|
+
token: z.string().min(1).optional(),
|
|
21
|
+
tokenExpiresAt: z.number().optional(),
|
|
22
|
+
});
|
|
23
|
+
export class CloudAuthorizationRequired extends Error {
|
|
24
|
+
action;
|
|
25
|
+
constructor(action) {
|
|
26
|
+
super("Sign in to ClawScarf, then run configure again with the same directory.");
|
|
27
|
+
this.action = action;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** The private OAuth challenge survives terminal exit; resuming never allocates another installation. */
|
|
31
|
+
export async function authorizeCloud(cloudUrl, file, options) {
|
|
32
|
+
const client = createClient({
|
|
33
|
+
baseUrl: cloudUrlSchema.parse(cloudUrl),
|
|
34
|
+
redirect: "error",
|
|
35
|
+
});
|
|
36
|
+
const response = await getCliIdentity({
|
|
37
|
+
client,
|
|
38
|
+
signal: AbortSignal.any([
|
|
39
|
+
AbortSignal.timeout(10_000),
|
|
40
|
+
...(options.signal ? [options.signal] : []),
|
|
41
|
+
]),
|
|
42
|
+
});
|
|
43
|
+
if (!response.data)
|
|
44
|
+
throw new InstallationError("unavailable", "ClawScarf login is unavailable. Try again later.");
|
|
45
|
+
const identity = z
|
|
46
|
+
.object({
|
|
47
|
+
issuer: cloudUrlSchema.refine((value) => value.startsWith("https:")),
|
|
48
|
+
clientId: z.string().min(1),
|
|
49
|
+
})
|
|
50
|
+
.parse(response.data);
|
|
51
|
+
const configuration = await oidc.discovery(new URL(identity.issuer), identity.clientId, undefined, oidc.None(), { timeout: 10 });
|
|
52
|
+
let pending;
|
|
53
|
+
try {
|
|
54
|
+
pending = pendingSchema.parse(JSON.parse((await readInputFile(file, true)).toString("utf8")));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (pending &&
|
|
61
|
+
(pending.cloudUrl !== cloudUrl ||
|
|
62
|
+
pending.issuer !== identity.issuer ||
|
|
63
|
+
pending.clientId !== identity.clientId))
|
|
64
|
+
throw new InstallationError("invalid_configuration", "Pending authorization belongs to a different cloud identity provider.");
|
|
65
|
+
if (pending?.token) {
|
|
66
|
+
if (pending.tokenExpiresAt && pending.tokenExpiresAt > Date.now())
|
|
67
|
+
return pending.token;
|
|
68
|
+
pending = undefined;
|
|
69
|
+
}
|
|
70
|
+
if (!pending || pending.expiresAt <= Date.now()) {
|
|
71
|
+
const challenge = await oidc.initiateDeviceAuthorization(configuration, {
|
|
72
|
+
scope: "openid profile email",
|
|
73
|
+
});
|
|
74
|
+
pending = pendingSchema.parse({
|
|
75
|
+
cloudUrl,
|
|
76
|
+
...identity,
|
|
77
|
+
deviceCode: challenge.device_code,
|
|
78
|
+
userCode: challenge.user_code,
|
|
79
|
+
expiresAt: Date.now() + challenge.expires_in * 1000,
|
|
80
|
+
interval: (challenge.interval ?? 5) * 1000,
|
|
81
|
+
nextPollAt: 0,
|
|
82
|
+
});
|
|
83
|
+
await writePrivate(file, JSON.stringify(pending));
|
|
84
|
+
}
|
|
85
|
+
const url = new URL("/setup", cloudUrl);
|
|
86
|
+
url.searchParams.set("code", pending.userCode);
|
|
87
|
+
const required = () => new CloudAuthorizationRequired({
|
|
88
|
+
url: url.href,
|
|
89
|
+
expiresAt: new Date(pending.expiresAt).toISOString(),
|
|
90
|
+
retryAfterSeconds: Math.max(1, Math.ceil((pending.nextPollAt - Date.now()) / 1000)),
|
|
91
|
+
});
|
|
92
|
+
await options.present?.(url.href, pending.userCode, new Date(pending.expiresAt).toISOString());
|
|
93
|
+
for (;;) {
|
|
94
|
+
options.signal?.throwIfAborted();
|
|
95
|
+
if (pending.expiresAt <= Date.now()) {
|
|
96
|
+
await rm(file, { force: true });
|
|
97
|
+
throw new InstallationError("unavailable", "Sign-in expired. Run configure again with the same directory for a new link.");
|
|
98
|
+
}
|
|
99
|
+
if (pending.nextPollAt > Date.now()) {
|
|
100
|
+
if (!options.wait)
|
|
101
|
+
throw required();
|
|
102
|
+
await delay(pending.nextPollAt - Date.now(), undefined, {
|
|
103
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
pending.nextPollAt = Date.now() + pending.interval;
|
|
107
|
+
await writePrivate(file, JSON.stringify(pending));
|
|
108
|
+
try {
|
|
109
|
+
const tokens = await oidc.genericGrantRequest(configuration, "urn:ietf:params:oauth:grant-type:device_code", { device_code: pending.deviceCode });
|
|
110
|
+
pending.token = tokens.access_token;
|
|
111
|
+
pending.tokenExpiresAt = Date.now() + (tokens.expires_in ?? 60) * 1000;
|
|
112
|
+
await writePrivate(file, JSON.stringify(pending));
|
|
113
|
+
return pending.token;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!(error instanceof oidc.ResponseBodyError))
|
|
117
|
+
throw error;
|
|
118
|
+
if (error.error === "slow_down") {
|
|
119
|
+
pending.interval += 5000;
|
|
120
|
+
pending.nextPollAt = Date.now() + pending.interval;
|
|
121
|
+
await writePrivate(file, JSON.stringify(pending));
|
|
122
|
+
}
|
|
123
|
+
else if (error.error !== "authorization_pending") {
|
|
124
|
+
if (["access_denied", "expired_token", "invalid_grant"].includes(error.error)) {
|
|
125
|
+
await rm(file, { force: true });
|
|
126
|
+
throw new InstallationError("invalid_configuration", "Sign-in was declined or expired. Run configure again with the same directory to retry.");
|
|
127
|
+
}
|
|
128
|
+
throw new InstallationError("unavailable", "Cloud authorization failed. Retry configure with the same directory.");
|
|
129
|
+
}
|
|
130
|
+
if (!options.wait)
|
|
131
|
+
throw required();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import lockfile from "proper-lockfile";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { createClient } from "../../generated/http/client/index.js";
|
|
7
|
+
import { registerInstallation, getInstallationIdentity, getAccount, } from "../../services/cloud/generated/sdk.gen.js";
|
|
8
|
+
import { cloudUrlSchema } from "./url.js";
|
|
9
|
+
import { installationSchema, } from "../installation/configuration.js";
|
|
10
|
+
import { readInputFile, readJson } from "../installation/files.js";
|
|
11
|
+
import { writePrivate, ensurePrivateFile } from "../deployment/state.js";
|
|
12
|
+
import { InstallationError } from "../installation/errors.js";
|
|
13
|
+
const identitySchema = z.strictObject({
|
|
14
|
+
issuer: cloudUrlSchema,
|
|
15
|
+
clientId: z.string().min(1),
|
|
16
|
+
clientSecret: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
const administratorSchema = z.strictObject({
|
|
19
|
+
issuer: cloudUrlSchema,
|
|
20
|
+
subject: z.string().min(1),
|
|
21
|
+
email: z.email(),
|
|
22
|
+
});
|
|
23
|
+
const registrationSchema = z.strictObject({
|
|
24
|
+
cloudUrl: cloudUrlSchema,
|
|
25
|
+
request: z.strictObject({
|
|
26
|
+
reference: z.uuid(),
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
origin: cloudUrlSchema.nullable(),
|
|
29
|
+
managementSecret: z.string().min(43),
|
|
30
|
+
runtimeSecret: z.string().min(43),
|
|
31
|
+
}),
|
|
32
|
+
accountId: z.uuid().optional(),
|
|
33
|
+
installationId: z.uuid().optional(),
|
|
34
|
+
identity: identitySchema.optional(),
|
|
35
|
+
administrator: administratorSchema.optional(),
|
|
36
|
+
});
|
|
37
|
+
export async function readHostedRegistration(file) {
|
|
38
|
+
return registrationSchema.parse(JSON.parse((await readInputFile(file, true)).toString("utf8")));
|
|
39
|
+
}
|
|
40
|
+
/** Hosted login and Connections share registration only when they use the same service. */
|
|
41
|
+
function connectionsRegistration(config) {
|
|
42
|
+
if (config.connections.mode !== "hosted")
|
|
43
|
+
return undefined;
|
|
44
|
+
return config.access.mode === "hosted" &&
|
|
45
|
+
config.access.cloudUrl === config.connections.cloudUrl
|
|
46
|
+
? config.access
|
|
47
|
+
: config.connections;
|
|
48
|
+
}
|
|
49
|
+
export async function registerCloudServices(configFile, authorize) {
|
|
50
|
+
const config = installationSchema.parse(await readJson(configFile));
|
|
51
|
+
const connections = connectionsRegistration(config);
|
|
52
|
+
const selections = [
|
|
53
|
+
...(config.access.mode === "hosted"
|
|
54
|
+
? [{ ...config.access, login: true }]
|
|
55
|
+
: []),
|
|
56
|
+
...(connections && connections !== config.access
|
|
57
|
+
? [{ ...connections, login: false }]
|
|
58
|
+
: []),
|
|
59
|
+
];
|
|
60
|
+
for (const selection of selections) {
|
|
61
|
+
const origin = selection.login
|
|
62
|
+
? config.exposure.mode === "https"
|
|
63
|
+
? config.exposure.applicationOrigin
|
|
64
|
+
: `http://127.0.0.1:${String(config.exposure.applicationPort)}`
|
|
65
|
+
: null;
|
|
66
|
+
await registerService(resolve(dirname(configFile), selection.registrationFile), selection.cloudUrl, config.name, origin, authorize, selection.login && !config.access.administratorSubject);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Persist intent before dispatch; retries reuse the same reference and keys. */
|
|
70
|
+
async function registerService(path, cloudUrl, name, origin, authorize, claimAdministrator) {
|
|
71
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
72
|
+
const unlock = await lockfile.lock(dirname(path), {
|
|
73
|
+
lockfilePath: path + ".lock",
|
|
74
|
+
retries: 0,
|
|
75
|
+
});
|
|
76
|
+
try {
|
|
77
|
+
let registration;
|
|
78
|
+
try {
|
|
79
|
+
registration = await readHostedRegistration(path);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (!(error instanceof Error &&
|
|
83
|
+
"code" in error &&
|
|
84
|
+
error.code === "ENOENT"))
|
|
85
|
+
throw error;
|
|
86
|
+
registration = {
|
|
87
|
+
cloudUrl,
|
|
88
|
+
request: {
|
|
89
|
+
reference: randomUUID(),
|
|
90
|
+
name,
|
|
91
|
+
origin,
|
|
92
|
+
managementSecret: randomBytes(32).toString("base64url"),
|
|
93
|
+
runtimeSecret: randomBytes(32).toString("base64url"),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
await writePrivate(path, JSON.stringify(registration));
|
|
97
|
+
}
|
|
98
|
+
if (registration.cloudUrl !== cloudUrl ||
|
|
99
|
+
registration.request.origin !== origin ||
|
|
100
|
+
registration.request.name !== name)
|
|
101
|
+
throw new InstallationError("change_unsupported", "Saved cloud registration belongs to different settings. Do not replace it; use the original installation settings.");
|
|
102
|
+
if (registration.identity) {
|
|
103
|
+
await ensurePrivateFile(join(dirname(path), "hosted-oidc-secret"), registration.identity.clientSecret);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const client = createClient({ baseUrl: cloudUrl, redirect: "error" });
|
|
107
|
+
if (!registration.installationId) {
|
|
108
|
+
const auth = await authorize(cloudUrl, path + ".login", claimAdministrator);
|
|
109
|
+
const account = await getAccount({
|
|
110
|
+
client,
|
|
111
|
+
auth,
|
|
112
|
+
signal: AbortSignal.timeout(10_000),
|
|
113
|
+
});
|
|
114
|
+
if (!account.data) {
|
|
115
|
+
if (account.response?.status === 401)
|
|
116
|
+
await rm(path + ".login", { force: true });
|
|
117
|
+
throw new InstallationError("unavailable", "Cloud account could not be verified. Registration has not been dispatched.");
|
|
118
|
+
}
|
|
119
|
+
const { accountId, identity } = z
|
|
120
|
+
.object({
|
|
121
|
+
accountId: z.uuid(),
|
|
122
|
+
identity: administratorSchema.nullable(),
|
|
123
|
+
})
|
|
124
|
+
.parse(account.data);
|
|
125
|
+
if (registration.accountId && registration.accountId !== accountId)
|
|
126
|
+
throw new InstallationError("change_unsupported", "Resume registration with the original ClawScarf account.");
|
|
127
|
+
if (claimAdministrator && !identity)
|
|
128
|
+
throw new InstallationError("invalid_configuration", "A provisioning credential does not select an administrator. Supply --administrator-subject and --administrator-email during initial configuration, or sign in interactively.");
|
|
129
|
+
registration.accountId = accountId;
|
|
130
|
+
if (claimAdministrator && identity)
|
|
131
|
+
registration.administrator = identity;
|
|
132
|
+
await writePrivate(path, JSON.stringify(registration));
|
|
133
|
+
const result = await registerInstallation({
|
|
134
|
+
client,
|
|
135
|
+
auth,
|
|
136
|
+
body: registration.request,
|
|
137
|
+
signal: AbortSignal.timeout(60_000),
|
|
138
|
+
});
|
|
139
|
+
if (!result.data)
|
|
140
|
+
throw new InstallationError("unavailable", `Cloud registration was not confirmed (HTTP ${String(result.response?.status ?? "unavailable")}). Retry this installation with its saved settings.`);
|
|
141
|
+
const installation = z
|
|
142
|
+
.object({
|
|
143
|
+
id: z.uuid(),
|
|
144
|
+
origin: z.string().nullable(),
|
|
145
|
+
oidcState: z.enum([
|
|
146
|
+
"ready",
|
|
147
|
+
"pending",
|
|
148
|
+
"provisioning",
|
|
149
|
+
"failed",
|
|
150
|
+
"uncertain",
|
|
151
|
+
"disabled",
|
|
152
|
+
]),
|
|
153
|
+
})
|
|
154
|
+
.parse(result.data);
|
|
155
|
+
if (installation.origin !== origin)
|
|
156
|
+
throw new InstallationError("invalid_configuration", "Cloud registration returned a different installation origin.");
|
|
157
|
+
registration.installationId = installation.id;
|
|
158
|
+
await writePrivate(path, JSON.stringify(registration));
|
|
159
|
+
if (origin && installation.oidcState !== "ready")
|
|
160
|
+
throw new InstallationError("unavailable", `Hosted login registration is ${installation.oidcState}. No new client will be allocated; contact the cloud operator.`);
|
|
161
|
+
}
|
|
162
|
+
await rm(path + ".login", { force: true });
|
|
163
|
+
if (!origin)
|
|
164
|
+
return;
|
|
165
|
+
const result = await getInstallationIdentity({
|
|
166
|
+
client,
|
|
167
|
+
auth: registration.request.managementSecret,
|
|
168
|
+
path: { id: registration.installationId },
|
|
169
|
+
signal: AbortSignal.timeout(15_000),
|
|
170
|
+
});
|
|
171
|
+
if (!result.data)
|
|
172
|
+
throw new InstallationError("unavailable", "Hosted login credentials are unavailable. The saved registration is retained; no keys are replaced.");
|
|
173
|
+
registration.identity = identitySchema.parse(result.data);
|
|
174
|
+
if (registration.administrator &&
|
|
175
|
+
registration.administrator.issuer !== registration.identity.issuer)
|
|
176
|
+
throw new InstallationError("invalid_configuration", "Administrator identity belongs to a different issuer.");
|
|
177
|
+
await ensurePrivateFile(join(dirname(path), "hosted-oidc-secret"), registration.identity.clientSecret);
|
|
178
|
+
await writePrivate(path, JSON.stringify(registration));
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await unlock();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export async function hostedOidc(file) {
|
|
185
|
+
const registration = await readHostedRegistration(file);
|
|
186
|
+
if (!registration.identity)
|
|
187
|
+
throw new InstallationError("unavailable", "Hosted login registration is incomplete. Resume the installer or run configure again.");
|
|
188
|
+
if (registration.administrator &&
|
|
189
|
+
registration.administrator.issuer !== registration.identity.issuer)
|
|
190
|
+
throw new InstallationError("invalid_configuration", "Administrator identity belongs to a different issuer.");
|
|
191
|
+
const clientSecretFile = join(dirname(file), "hosted-oidc-secret");
|
|
192
|
+
if ((await readInputFile(clientSecretFile, true)).toString("utf8") !==
|
|
193
|
+
registration.identity.clientSecret)
|
|
194
|
+
throw new InstallationError("invalid_configuration", "Hosted OIDC credentials do not match the saved registration.");
|
|
195
|
+
return {
|
|
196
|
+
issuer: registration.identity.issuer,
|
|
197
|
+
clientId: registration.identity.clientId,
|
|
198
|
+
clientSecretFile,
|
|
199
|
+
...(registration.administrator
|
|
200
|
+
? {
|
|
201
|
+
administratorSubject: registration.administrator.subject,
|
|
202
|
+
administratorEmail: registration.administrator.email,
|
|
203
|
+
}
|
|
204
|
+
: {}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
export async function hostedConnections(config, configFile) {
|
|
208
|
+
const selected = connectionsRegistration(config);
|
|
209
|
+
if (!selected)
|
|
210
|
+
return undefined;
|
|
211
|
+
const file = resolve(dirname(configFile), selected.registrationFile);
|
|
212
|
+
const registration = await readHostedRegistration(file);
|
|
213
|
+
const origin = selected === config.access
|
|
214
|
+
? config.exposure.mode === "https"
|
|
215
|
+
? config.exposure.applicationOrigin
|
|
216
|
+
: `http://127.0.0.1:${String(config.exposure.applicationPort)}`
|
|
217
|
+
: null;
|
|
218
|
+
if (!registration.installationId ||
|
|
219
|
+
registration.cloudUrl !== selected.cloudUrl ||
|
|
220
|
+
registration.request.name !== config.name ||
|
|
221
|
+
registration.request.origin !== origin)
|
|
222
|
+
throw new InstallationError("invalid_configuration", "Connections registration is incomplete or belongs to another cloud service.");
|
|
223
|
+
const managementKeyFile = file + ".management-key";
|
|
224
|
+
const credentialFile = file + ".runtime-key";
|
|
225
|
+
await ensurePrivateFile(managementKeyFile, registration.request.managementSecret);
|
|
226
|
+
await ensurePrivateFile(credentialFile, registration.request.runtimeSecret);
|
|
227
|
+
return {
|
|
228
|
+
brokerUrl: `${registration.cloudUrl}/api/connections`,
|
|
229
|
+
managementKeyFile,
|
|
230
|
+
credentialFile,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const defaultCloudUrl = "https://cloud.clawscarf.com";
|
|
3
|
+
export const cloudUrlSchema = z.url().refine((value) => {
|
|
4
|
+
const url = new URL(value);
|
|
5
|
+
return (url.origin === value &&
|
|
6
|
+
(url.protocol === "https:" ||
|
|
7
|
+
(url.protocol === "http:" &&
|
|
8
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))));
|
|
9
|
+
}, "Use an HTTPS origin, or loopback HTTP for development.");
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { sessionRequest } from "./session.js";
|
|
4
|
+
import * as api from "../services/connections/cloud/generated/sdk.gen.js";
|
|
5
|
+
import { InstallationError } from "./installation/errors.js";
|
|
6
|
+
import { writeResult } from "./output.js";
|
|
7
|
+
export function connectionCommands(command, program) {
|
|
8
|
+
command
|
|
9
|
+
.description("Manage connected accounts and their agent access")
|
|
10
|
+
.option("--origin <url>", "Installation origin")
|
|
11
|
+
.option("--session-file <path>", "Private signed-in administrator session");
|
|
12
|
+
const output = (value, text) => {
|
|
13
|
+
writeResult(program, value, text);
|
|
14
|
+
};
|
|
15
|
+
async function request(revision) {
|
|
16
|
+
const base = await sessionRequest(command.opts());
|
|
17
|
+
return {
|
|
18
|
+
...base,
|
|
19
|
+
headers: {
|
|
20
|
+
...base.headers,
|
|
21
|
+
"idempotency-key": randomUUID(),
|
|
22
|
+
"if-match": `"${String(revision ?? 0)}"`,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const grant = (ids) => ids === undefined
|
|
27
|
+
? { mode: "all" }
|
|
28
|
+
: {
|
|
29
|
+
mode: "selected",
|
|
30
|
+
agentIds: ids
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((x) => x.trim())
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
};
|
|
35
|
+
command
|
|
36
|
+
.command("list")
|
|
37
|
+
.option("--all", "Include disconnected history")
|
|
38
|
+
.option("--cursor <cursor>", "Continue a previous page")
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
const result = (await api.listConnections({
|
|
41
|
+
...(await request()),
|
|
42
|
+
query: {
|
|
43
|
+
limit: 100,
|
|
44
|
+
includeDisconnected: options.all ?? false,
|
|
45
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
46
|
+
},
|
|
47
|
+
})).data;
|
|
48
|
+
output(result, (result.items
|
|
49
|
+
.map((c) => `${c.name} · ${c.state} · ${c.id}`)
|
|
50
|
+
.join("\n") || "No connections.") +
|
|
51
|
+
(result.nextCursor ? `\nMore: --cursor ${result.nextCursor}` : ""));
|
|
52
|
+
});
|
|
53
|
+
command
|
|
54
|
+
.command("catalog")
|
|
55
|
+
.option("--query <text>", "Find a service")
|
|
56
|
+
.option("--cursor <cursor>", "Continue a previous page")
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
const result = (await api.listConnectors({
|
|
59
|
+
...(await request()),
|
|
60
|
+
query: { limit: 100, ...options },
|
|
61
|
+
})).data;
|
|
62
|
+
output(result, result.items.map((c) => `${c.name} · ${c.id}`).join("\n") +
|
|
63
|
+
(result.nextCursor ? `\nMore: --cursor ${result.nextCursor}` : ""));
|
|
64
|
+
});
|
|
65
|
+
command.command("agents").action(async () => {
|
|
66
|
+
const result = (await api.listConnectionAgents(await request())).data;
|
|
67
|
+
output(result, result.agents.map((a) => a.name ?? a.id).join("\n"));
|
|
68
|
+
});
|
|
69
|
+
command
|
|
70
|
+
.command("add <connectorId>")
|
|
71
|
+
.option("--name <name>", "Connection name")
|
|
72
|
+
.option("--agents <ids>", "Comma-separated agent IDs; omitted means all agents")
|
|
73
|
+
.action(async (connectorId, options) => {
|
|
74
|
+
const result = (await api.createConnection({
|
|
75
|
+
...(await request()),
|
|
76
|
+
body: {
|
|
77
|
+
connectorId,
|
|
78
|
+
name: options.name ?? connectorId,
|
|
79
|
+
grant: grant(options.agents),
|
|
80
|
+
},
|
|
81
|
+
})).data;
|
|
82
|
+
output(result, `${result.name} created · ${result.id}\nUse clawscarf connections connect ${result.id} with the same origin and session.`);
|
|
83
|
+
});
|
|
84
|
+
command
|
|
85
|
+
.command("edit <connectionId>")
|
|
86
|
+
.option("--name <name>", "New name")
|
|
87
|
+
.option("--agents <ids>", "Selected agent IDs")
|
|
88
|
+
.option("--all-agents", "Grant all current and future agents")
|
|
89
|
+
.action(async (connectionId, options) => {
|
|
90
|
+
if (options.agents !== undefined && options.allAgents)
|
|
91
|
+
throw new InstallationError("invalid_configuration", "Choose --agents or --all-agents.");
|
|
92
|
+
const current = (await api.getConnection({
|
|
93
|
+
...(await request()),
|
|
94
|
+
path: { connectionId },
|
|
95
|
+
})).data;
|
|
96
|
+
const result = (await api.updateConnection({
|
|
97
|
+
...(await request(current.revision)),
|
|
98
|
+
path: { connectionId },
|
|
99
|
+
body: {
|
|
100
|
+
name: options.name ?? current.name,
|
|
101
|
+
grant: options.allAgents
|
|
102
|
+
? { mode: "all" }
|
|
103
|
+
: options.agents === undefined
|
|
104
|
+
? current.grant
|
|
105
|
+
: grant(options.agents),
|
|
106
|
+
},
|
|
107
|
+
})).data;
|
|
108
|
+
output(result, "Connection updated.");
|
|
109
|
+
});
|
|
110
|
+
command
|
|
111
|
+
.command("connect <connectionId>")
|
|
112
|
+
.action(async (connectionId) => {
|
|
113
|
+
const current = (await api.getConnection({
|
|
114
|
+
...(await request()),
|
|
115
|
+
path: { connectionId },
|
|
116
|
+
})).data;
|
|
117
|
+
const result = current.setup &&
|
|
118
|
+
["creating", "pending", "verifying"].includes(current.setup.state)
|
|
119
|
+
? (await api.getConnectionSetup({
|
|
120
|
+
...(await request()),
|
|
121
|
+
path: { connectionId, setupId: current.setup.id },
|
|
122
|
+
})).data
|
|
123
|
+
: (await api.startConnectionSetup({
|
|
124
|
+
...(await request(current.revision)),
|
|
125
|
+
path: { connectionId },
|
|
126
|
+
body: {
|
|
127
|
+
kind: current.state === "connected" ? "reconnect" : "initial",
|
|
128
|
+
},
|
|
129
|
+
})).data;
|
|
130
|
+
output(result, result.url
|
|
131
|
+
? `Continue in your signed-in installation browser:\n${result.url}`
|
|
132
|
+
: `Setup ${result.setup.state}. Refresh before retrying.`);
|
|
133
|
+
});
|
|
134
|
+
command
|
|
135
|
+
.command("complete <connectionId> <setupId>")
|
|
136
|
+
.requiredOption("--receipt-file <path>", "Private OAuth return receipt")
|
|
137
|
+
.action(async (connectionId, setupId, options) => {
|
|
138
|
+
const result = (await api.completeConnectionSetup({
|
|
139
|
+
...(await request()),
|
|
140
|
+
path: { connectionId, setupId },
|
|
141
|
+
body: {
|
|
142
|
+
sessionUri: (await readFile(options.receiptFile, "utf8")).trim(),
|
|
143
|
+
},
|
|
144
|
+
})).data;
|
|
145
|
+
output(result, "Connection completed.");
|
|
146
|
+
});
|
|
147
|
+
command
|
|
148
|
+
.command("cancel <connectionId> <setupId>")
|
|
149
|
+
.action(async (connectionId, setupId) => {
|
|
150
|
+
const current = (await api.getConnection({
|
|
151
|
+
...(await request()),
|
|
152
|
+
path: { connectionId },
|
|
153
|
+
})).data;
|
|
154
|
+
const result = (await api.cancelConnectionSetup({
|
|
155
|
+
...(await request(current.revision)),
|
|
156
|
+
path: { connectionId, setupId },
|
|
157
|
+
})).data;
|
|
158
|
+
output(result, "Setup cancelled.");
|
|
159
|
+
});
|
|
160
|
+
command
|
|
161
|
+
.command("disconnect <connectionId>")
|
|
162
|
+
.description("Disconnect an account or remove an inactive entry")
|
|
163
|
+
.action(async (connectionId) => {
|
|
164
|
+
const current = (await api.getConnection({
|
|
165
|
+
...(await request()),
|
|
166
|
+
path: { connectionId },
|
|
167
|
+
})).data;
|
|
168
|
+
const result = (await api.disconnectConnection({
|
|
169
|
+
...(await request(current.revision)),
|
|
170
|
+
path: { connectionId },
|
|
171
|
+
})).data;
|
|
172
|
+
output(result, "Connection removed. Remote account cleanup may still be pending.");
|
|
173
|
+
});
|
|
174
|
+
command
|
|
175
|
+
.command("refresh <connectionId>")
|
|
176
|
+
.action(async (connectionId) => {
|
|
177
|
+
const current = (await api.getConnection({
|
|
178
|
+
...(await request()),
|
|
179
|
+
path: { connectionId },
|
|
180
|
+
})).data;
|
|
181
|
+
const result = (await api.refreshConnection({
|
|
182
|
+
...(await request(current.revision)),
|
|
183
|
+
path: { connectionId },
|
|
184
|
+
})).data;
|
|
185
|
+
output(result, `${result.name} · ${result.state}`);
|
|
186
|
+
});
|
|
187
|
+
command.command("usage").action(async () => {
|
|
188
|
+
const result = (await api.getConnectionUsage(await request())).data;
|
|
189
|
+
output(result, result.limits
|
|
190
|
+
.map((l) => `${l.installation ? "Installation" : "Account"}${l.backend ? ` / ${l.backend}` : ""}: ${String(l.used)}/${String(l.limit)} calls`)
|
|
191
|
+
.join("\n") + `\nResets ${result.resetsAt}`);
|
|
192
|
+
});
|
|
193
|
+
}
|