openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
337 lines (336 loc) • 15.1 kB
JavaScript
import { t as mutateConfigFile } from "./mutate-ZNN4iFCn.js";
import "./config-mutation-CUEqNwmX.js";
import { n as createChannelPairingController } from "./channel-pairing-BDYxqbDV.js";
import { i as parseReefRelayUrl, t as ReefChannelConfigSchema } from "./config-schema-DGoCUvrP.js";
import { D as loadKeys, E as generateAndStoreKeys, F as clearReefSetupSession, H as reserveReefIdentityBinding, I as finalizeReefIdentityBinding, L as loadReefIdentityBinding, P as assertReefIdentityBinding, R as loadReefSetupSession, U as saveReefSetupSession, V as releaseReefIdentityReservation, l as openReefTrustStore, ot as fingerprint } from "./doctor-state-paths-Bf7anfKJ.js";
import { d as isReefOwnershipRejection, n as assertLegacyReefKeysMigrated, r as ReefFriendManager, s as ReefTransportClient, u as isDefinitiveReefRegistrationFailure } from "./legacy-key-guard-BMnoqjyn.js";
import { t as ReefAutonomySchema } from "./friend-types-DBQVddbp.js";
import { i as getReefRuntime } from "./runtime-DYnXNGON.js";
//#region extensions/reef/src/cli.ts
const HANDLE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,62}$/;
const GUARD_DEFAULTS = {
openai: {
pinnedModel: "gpt-5.6-terra",
apiKeyEnv: "REEF_GUARD_OPENAI_KEY"
},
anthropic: {
pinnedModel: "claude-haiku-4-5-20251001",
apiKeyEnv: "REEF_GUARD_ANTHROPIC_KEY"
}
};
function emit(output, payload, lines) {
if (output.json) {
process.stdout.write(`${JSON.stringify(payload)}\n`);
return;
}
for (const line of lines) process.stdout.write(`${line}\n`);
}
async function fail(output, message) {
const stream = output.json ? process.stdout : process.stderr;
const text = output.json ? `${JSON.stringify({ error: message })}\n` : `${message}\n`;
await new Promise((resolve) => {
stream.write(text, () => resolve());
});
process.exit(1);
}
function reefCliAction(run) {
return async (...args) => {
const optionsIndex = args.length - 2;
const options = args[optionsIndex];
const positional = args.slice(0, optionsIndex);
const output = { json: options.json };
try {
await run(output, options, ...positional);
} catch (error) {
await fail(output, error instanceof Error ? error.message : String(error));
}
};
}
async function loadOrCreateKeys(createMissing, legacyStateDir) {
const runtime = getReefRuntime();
try {
return await loadKeys(runtime);
} catch (error) {
if (createMissing && error.code === "ENOENT") {
await assertLegacyReefKeysMigrated(legacyStateDir);
return await generateAndStoreKeys(runtime);
}
throw error;
}
}
function currentReefConfig() {
const raw = getReefRuntime().config.current().channels?.reef;
if (!raw) return;
const parsed = ReefChannelConfigSchema.safeParse(raw);
return parsed.success ? parsed.data : void 0;
}
async function loadConfiguredManager(output) {
const config = currentReefConfig();
if (!config?.handle) return await fail(output, "Reef is not configured. Run `openclaw reef register` first.");
const keys = await loadOrCreateKeys(false);
const runtime = getReefRuntime();
const relayUrl = parseReefRelayUrl(config.relayUrl);
assertReefIdentityBinding(runtime, {
handle: config.handle,
relayUrl
});
const transport = new ReefTransportClient(relayUrl, config.handle, keys);
const pairing = createChannelPairingController({
core: runtime,
channel: "reef",
accountId: "default"
});
return {
config,
keys,
manager: new ReefFriendManager(transport, openReefTrustStore(runtime, config), {
list: pairing.readAllowFromStore,
remove: async (peer) => {
return (await pairing.removeAllowFromStoreEntry(peer)).changed;
}
})
};
}
async function writeReefRegistration(candidate) {
await mutateConfigFile({
afterWrite: { mode: "auto" },
mutate(draft) {
draft.channels = {
...draft.channels,
reef: candidate
};
}
});
}
async function writeReefMigrationStateDir(stateDir) {
await mutateConfigFile({
afterWrite: { mode: "auto" },
mutate(draft) {
const existing = draft.channels?.reef;
draft.channels = {
...draft.channels,
reef: {
...existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {},
stateDir
}
};
}
});
}
async function runRegister(output, options) {
if (!options.email.includes("@")) return await fail(output, "A valid --email is required.");
const provider = options.guardProvider;
const guardDefaults = GUARD_DEFAULTS[provider];
if (!guardDefaults) return await fail(output, "--guard-provider must be one of: anthropic, openai.");
const relayUrl = parseReefRelayUrl(options.relay);
const legacyStateDir = options.stateDir ?? currentReefConfig()?.stateDir;
const explicitHandle = options.handle?.toLowerCase();
const runtime = getReefRuntime();
const identity = loadReefIdentityBinding(runtime);
if (identity?.handle && (identity.relayUrl !== relayUrl || explicitHandle !== void 0 && identity.handle !== explicitHandle)) return await fail(output, `This OpenClaw state already holds the Reef identity @${identity.handle} on ${identity.relayUrl}. Re-register the same handle and relay.`);
const requestedHandle = explicitHandle ?? identity?.handle;
let keys;
try {
keys = await loadOrCreateKeys(true, legacyStateDir);
} catch (error) {
if (options.stateDir && error.code === "REEF_LEGACY_KEYS_PENDING") try {
await writeReefMigrationStateDir(options.stateDir);
} catch (writeError) {
throw new Error("Failed to save the Reef legacy state directory for Doctor", { cause: writeError });
}
throw error;
}
const bootstrap = new ReefTransportClient(relayUrl, requestedHandle ?? "pending", keys);
const stored = loadReefSetupSession(runtime);
const token = options.token?.trim();
const storedSession = !options.session?.trim() && stored?.relayUrl === relayUrl && stored?.email === options.email ? stored.session : void 0;
const session = options.session?.trim() || storedSession;
if (!session && !token) {
const started = await bootstrap.authStart(options.email);
emit(output, {
status: "email_sent",
email: options.email,
...started.magicLink ? { magicLink: started.magicLink } : {},
next: "Open the magic link, copy the token from the URL fragment, then rerun the exact same command with --token <token> added (or --session from the welcome page)."
}, [
`Sign-in link sent to ${options.email}.`,
...started.magicLink ? [`Development magic link: ${started.magicLink}`] : [],
"Open the link, copy the token from the URL fragment, then rerun the exact",
"same command with --token <token> added."
]);
return;
}
const handle = requestedHandle;
if (!handle || !HANDLE_PATTERN.test(handle)) return await fail(output, "A valid --handle is required (lowercase letters, digits, - or _).");
const guard = {
provider,
pinnedModel: options.guardModel ?? guardDefaults.pinnedModel,
apiKeyEnv: options.guardEnv ?? guardDefaults.apiKeyEnv,
policyVersion: options.guardPolicy,
timeoutMs: 3e4
};
const provisional = {
enabled: true,
relayUrl,
handle,
email: options.email,
requestPolicy: options.policy,
...legacyStateDir ? { stateDir: legacyStateDir } : {},
guard
};
ReefChannelConfigSchema.parse(provisional);
const reservation = reserveReefIdentityBinding(runtime, {
handle,
relayUrl
});
let resolvedSession = session;
if (!resolvedSession) {
try {
resolvedSession = (await bootstrap.authComplete(token ?? "")).session;
} catch (error) {
if (isDefinitiveReefRegistrationFailure(error)) releaseReefIdentityReservation(runtime, reservation);
else finalizeReefIdentityBinding(runtime, reservation);
throw error;
}
try {
saveReefSetupSession(runtime, {
session: resolvedSession,
relayUrl,
email: options.email
});
} catch (error) {
releaseReefIdentityReservation(runtime, reservation);
throw error;
}
}
const transport = new ReefTransportClient(relayUrl, handle, keys);
let effectivePolicy = options.policy;
try {
await transport.createHandle(resolvedSession, options.policy);
} catch (error) {
const unavailable = error instanceof Error && error.message.includes("handle_unavailable");
let owned = false;
if (unavailable) try {
await transport.listFriends();
owned = true;
} catch (verificationError) {
if (isReefOwnershipRejection(verificationError)) releaseReefIdentityReservation(runtime, reservation);
else finalizeReefIdentityBinding(runtime, reservation);
throw verificationError;
}
if (!owned) {
if (isDefinitiveReefRegistrationFailure(error)) releaseReefIdentityReservation(runtime, reservation);
else finalizeReefIdentityBinding(runtime, reservation);
throw error;
}
finalizeReefIdentityBinding(runtime, reservation);
const { handles } = await transport.listOwnHandles(resolvedSession);
const existingHandle = handles.find((entry) => entry.handle === handle);
if (!existingHandle) return await fail(output, `Handle @${handle} is owned by this claw's keys, but the supplied session belongs to a different relay account. Use a session for the account that registered the handle.`);
effectivePolicy = existingHandle.request_policy;
}
finalizeReefIdentityBinding(runtime, reservation);
const candidate = ReefChannelConfigSchema.parse({
...provisional,
requestPolicy: effectivePolicy
});
try {
await writeReefRegistration(candidate);
} catch (error) {
await fail(output, `Handle @${handle} is claimed, but writing the local config failed: ${error instanceof Error ? error.message : String(error)}. Fix the local issue and rerun the exact same command — the retry reuses the stored session and recognizes the existing claim.`);
}
clearReefSetupSession(runtime);
const printed = fingerprint(keys.signing.publicKey, keys.encryption.publicKey);
emit(output, {
status: "registered",
handle,
relayUrl,
fingerprint: printed
}, [
`Registered @${handle} on ${relayUrl}.`,
`Safety fingerprint (share out of band): ${printed}`,
"Restart the gateway to connect: openclaw gateway restart"
]);
}
function registerReefCli({ program }) {
const reef = program.command("reef").description("Register on a Reef relay and manage guarded claw-to-claw friendships");
reef.command("register").description("Claim a handle and configure the Reef channel without the wizard").requiredOption("--email <email>", "Owner email registered with the relay").option("--handle <handle>", "Unlisted handle for this claw").option("--session <session>", "Setup session from the relay welcome page").option("--token <token>", "Magic-link token to exchange for a session").option("--relay <url>", "Relay origin URL", "https://reefwire.ai").option("--policy <policy>", "Inbound friend-request policy", "code-only").option("--state-dir <dir>", "Legacy Reef file directory for Doctor import").option("--guard-provider <provider>", "Guard provider (anthropic|openai)", "openai").option("--guard-model <model>", "Immutable guard model id (default depends on provider)").option("--guard-env <name>", "Env var holding the guard API key (default depends on provider)").option("--guard-policy <version>", "Guard policy version", "reef-v1").option("--json", "Emit JSON", false).action(reefCliAction(runRegister));
reef.command("status").description("Show Reef configuration and relay-side friendships").option("--json", "Emit JSON", false).action(reefCliAction(async (output) => {
const { config, keys, manager } = await loadConfiguredManager(output);
const friends = await manager.list();
const printed = fingerprint(keys.signing.publicKey, keys.encryption.publicKey);
emit(output, {
handle: config.handle,
relayUrl: config.relayUrl,
requestPolicy: config.requestPolicy,
guard: {
provider: config.guard?.provider,
pinnedModel: config.guard?.pinnedModel
},
fingerprint: printed,
friends: friends.map((friend) => ({
peer: friend.peer,
status: friend.status,
autonomy: friend.autonomy ?? null,
fingerprint: friend.fingerprint
}))
}, [
`@${config.handle} on ${config.relayUrl} (policy ${config.requestPolicy})`,
`Guard: ${config.guard?.provider}/${config.guard?.pinnedModel}`,
`Fingerprint: ${printed}`,
...friends.map((friend) => `- @${friend.peer}: ${friend.status}${friend.autonomy ? ` (${friend.autonomy})` : ""}`),
...friends.length === 0 ? ["No friendships yet."] : []
]);
}));
const friend = reef.command("friend").description("Manage Reef friendships");
friend.command("code").description("Mint a short-lived code a friend can use to request pairing").option("--json", "Emit JSON", false).action(reefCliAction(async (output) => {
const { manager } = await loadConfiguredManager(output);
const minted = await manager.mintCode();
const expires = (/* @__PURE__ */ new Date(minted.expires * 1e3)).toISOString();
emit(output, {
code: minted.code,
expires
}, [`Friend code: ${minted.code} (expires ${expires})`]);
}));
friend.command("autonomy <handle> <tier>").description("Set a trusted friend's autonomy tier").option("--json", "Emit JSON", false).action(reefCliAction(async (output, _options, handle, tier) => {
const { manager } = await loadConfiguredManager(output);
const peer = handle.replace(/^@/, "").toLowerCase();
const autonomy = ReefAutonomySchema.parse(tier);
await manager.setAutonomy(peer, autonomy);
emit(output, {
peer,
autonomy
}, [`Set @${peer} autonomy to ${autonomy}.`]);
}));
friend.command("request <handle>").description("Request a friendship (adopted automatically once accepted)").option("--code <code>", "Friend code minted by the recipient").option("--json", "Emit JSON", false).action(reefCliAction(async (output, options, handle) => {
const { manager } = await loadConfiguredManager(output);
const peer = handle.replace(/^@/, "").toLowerCase();
const result = await manager.request(peer, options.code);
emit(output, {
peer,
status: result.status
}, [`Friend request to @${peer}: ${result.status}. Adopted automatically once the peer accepts.`]);
}));
friend.command("list").description("List relay-side friendships with local autonomy").option("--json", "Emit JSON", false).action(reefCliAction(async (output) => {
const { manager } = await loadConfiguredManager(output);
const friends = await manager.list();
emit(output, { friends: friends.map((entry) => ({
peer: entry.peer,
status: entry.status,
autonomy: entry.autonomy ?? null,
keyEpoch: entry.key_epoch,
fingerprint: entry.fingerprint
})) }, friends.length ? friends.map((entry) => `@${entry.peer} ${entry.status} epoch=${entry.key_epoch} fingerprint=${entry.fingerprint}${entry.autonomy ? ` autonomy=${entry.autonomy}` : ""}`) : ["No friendships yet."]);
}));
friend.command("remove <handle>").description("Remove or block a friendship").option("--json", "Emit JSON", false).action(reefCliAction(async (output, _options, handle) => {
const { manager } = await loadConfiguredManager(output);
const peer = handle.replace(/^@/, "").toLowerCase();
await manager.remove(peer);
emit(output, {
peer,
status: "removed"
}, [`Removed @${peer}.`]);
}));
}
//#endregion
export { registerReefCli };