Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Luke The Dev
2026-03-23 11:44:25 -05:00
committed by GitHub
parent c2cbdeec44
commit 5e7812c352
30 changed files with 2213 additions and 128 deletions
+129
View File
@@ -0,0 +1,129 @@
import { buildAgentMainSessionKey, type GatewayClient } from "@/lib/gateway/GatewayClient";
import {
removeGatewayAgentFromConfigOnly,
updateGatewayAgentOverrides,
} from "@/lib/gateway/agentConfig";
import { getPackagedSkillById } from "@/lib/skills/catalog";
import { readPackagedSkillFiles } from "@/lib/skills/packaged";
import type { PackagedSkillInstallRequest, PackagedSkillInstallResult } from "@/lib/skills/types";
const normalizeRequired = (value: string, field: string): string => {
const trimmed = value.trim();
if (!trimmed) {
throw new Error(`${field} is required.`);
}
return trimmed;
};
const escapeForJsonString = (value: string) => JSON.stringify(value);
const buildInstallerMessage = (params: {
skillKey: string;
files: Array<{ relativePath: string; content: string }>;
}) => {
const fileEntries = params.files
.map(
(file) =>
`- path: ${escapeForJsonString(`skills/${params.skillKey}/${file.relativePath}`)}\n content: ${escapeForJsonString(file.content)}`
)
.join("\n");
return [
"Create these exact skill files inside the current workspace.",
"You must use the file tools and write the files exactly as provided.",
"Do not modify filenames, frontmatter, spacing, or content.",
"Create parent directories if they do not exist.",
"After writing the files, verify they exist and then reply only with: INSTALLED",
"",
"Files:",
fileEntries,
].join("\n");
};
const resolveRunId = (payload: unknown): string => {
if (!payload || typeof payload !== "object") {
throw new Error("Gateway returned an invalid chat.send response.");
}
const record = payload as Record<string, unknown>;
const runId = typeof record.runId === "string" ? record.runId.trim() : "";
if (!runId) {
throw new Error("Gateway returned an invalid chat.send response (missing runId).");
}
return runId;
};
const resolveMainKey = async (client: GatewayClient): Promise<string> => {
const result = (await client.call("agents.list", {})) as { mainKey?: unknown };
return typeof result?.mainKey === "string" && result.mainKey.trim() ? result.mainKey.trim() : "main";
};
export const installPackagedSkillViaGatewayAgent = async (params: {
client: GatewayClient;
request: PackagedSkillInstallRequest;
}): Promise<PackagedSkillInstallResult> => {
const packageId = normalizeRequired(params.request.packageId, "packageId");
const packagedSkill = getPackagedSkillById(packageId);
if (!packagedSkill) {
throw new Error(`Unknown packaged skill: ${packageId}`);
}
if (params.request.source !== "openclaw-workspace") {
throw new Error("Gateway-native packaged install currently supports workspace skills only.");
}
const workspaceDir = normalizeRequired(params.request.workspaceDir, "workspaceDir");
const files = readPackagedSkillFiles(packagedSkill.packageId);
const installerName = `Skill Installer ${Date.now()}`;
let installerAgentId: string | null = null;
try {
const created = (await params.client.call("agents.create", {
name: installerName,
workspace: workspaceDir,
})) as { agentId?: unknown };
installerAgentId =
typeof created?.agentId === "string" ? created.agentId.trim() : "";
if (!installerAgentId) {
throw new Error("Gateway returned an invalid agents.create response (missing agentId).");
}
await updateGatewayAgentOverrides({
client: params.client,
agentId: installerAgentId,
overrides: {
tools: {
alsoAllow: ["group:runtime", "group:fs"],
deny: ["group:web"],
},
},
});
const mainKey = await resolveMainKey(params.client);
const sessionKey = buildAgentMainSessionKey(installerAgentId, mainKey);
const sendResult = await params.client.call("chat.send", {
sessionKey,
message: buildInstallerMessage({ skillKey: packagedSkill.skillKey, files }),
deliver: false,
idempotencyKey: `skill-install:${packagedSkill.skillKey}:${Date.now()}`,
});
const runId = resolveRunId(sendResult);
await params.client.call("agent.wait", { runId, timeoutMs: 60_000 });
return {
installed: true,
installedPath: `${workspaceDir.replace(/\/+$/, "")}/skills/${packagedSkill.skillKey}`,
source: "openclaw-workspace",
skillKey: packagedSkill.skillKey,
};
} finally {
if (installerAgentId) {
try {
await removeGatewayAgentFromConfigOnly({
client: params.client,
agentId: installerAgentId,
});
} catch {
// Best-effort cleanup for temporary installer agents.
}
}
}
};