agent-control-openclaw-plugin 1.0.0
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 +201 -0
- package/README.md +69 -0
- package/index.ts +1 -0
- package/openclaw.plugin.json +61 -0
- package/package.json +48 -0
- package/src/agent-control-plugin.ts +333 -0
- package/src/openclaw-runtime.ts +157 -0
- package/src/session-context.ts +135 -0
- package/src/session-store.ts +162 -0
- package/src/shared.ts +90 -0
- package/src/tool-catalog.ts +326 -0
- package/src/types.ts +98 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { SessionIdentitySnapshot, SessionMetadataCacheEntry, SessionStoreInternals } from "./types.ts";
|
|
2
|
+
import { asString, isRecord } from "./shared.ts";
|
|
3
|
+
import { getResolvedOpenClawRootDir, importOpenClawInternalModule } from "./openclaw-runtime.ts";
|
|
4
|
+
|
|
5
|
+
const SESSION_META_CACHE_TTL_MS = 2_000;
|
|
6
|
+
const SESSION_META_CACHE_MAX = 512;
|
|
7
|
+
|
|
8
|
+
let sessionStoreInternalsPromise: Promise<SessionStoreInternals> | null = null;
|
|
9
|
+
const sessionMetadataCache = new Map<string, SessionMetadataCacheEntry>();
|
|
10
|
+
|
|
11
|
+
async function loadSessionStoreInternals(): Promise<SessionStoreInternals> {
|
|
12
|
+
if (sessionStoreInternalsPromise) {
|
|
13
|
+
return sessionStoreInternalsPromise;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
sessionStoreInternalsPromise = (async () => {
|
|
17
|
+
const openClawRoot = getResolvedOpenClawRootDir();
|
|
18
|
+
const [configModule, sessionsModule] = await Promise.all([
|
|
19
|
+
importOpenClawInternalModule(openClawRoot, [
|
|
20
|
+
"dist/config/config.js",
|
|
21
|
+
"src/config/config.ts",
|
|
22
|
+
]),
|
|
23
|
+
importOpenClawInternalModule(openClawRoot, [
|
|
24
|
+
"dist/config/sessions.js",
|
|
25
|
+
"src/config/sessions.ts",
|
|
26
|
+
]),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const loadConfig = configModule.loadConfig;
|
|
30
|
+
const resolveStorePath = sessionsModule.resolveStorePath;
|
|
31
|
+
const loadSessionStore = sessionsModule.loadSessionStore;
|
|
32
|
+
|
|
33
|
+
if (typeof loadConfig !== "function") {
|
|
34
|
+
throw new Error("agent-control: openclaw internal loadConfig is unavailable");
|
|
35
|
+
}
|
|
36
|
+
if (typeof resolveStorePath !== "function") {
|
|
37
|
+
throw new Error("agent-control: openclaw internal resolveStorePath is unavailable");
|
|
38
|
+
}
|
|
39
|
+
if (typeof loadSessionStore !== "function") {
|
|
40
|
+
throw new Error("agent-control: openclaw internal loadSessionStore is unavailable");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
loadConfig: loadConfig as SessionStoreInternals["loadConfig"],
|
|
45
|
+
resolveStorePath: resolveStorePath as SessionStoreInternals["resolveStorePath"],
|
|
46
|
+
loadSessionStore: loadSessionStore as SessionStoreInternals["loadSessionStore"],
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
|
|
50
|
+
return sessionStoreInternalsPromise;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unknownSessionIdentity(): SessionIdentitySnapshot {
|
|
54
|
+
return {
|
|
55
|
+
provider: null,
|
|
56
|
+
type: "unknown",
|
|
57
|
+
channelName: null,
|
|
58
|
+
dmUserName: null,
|
|
59
|
+
label: null,
|
|
60
|
+
from: null,
|
|
61
|
+
to: null,
|
|
62
|
+
accountId: null,
|
|
63
|
+
source: "unknown",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeSessionStoreKey(sessionKey: string | undefined): string | undefined {
|
|
68
|
+
const normalized = asString(sessionKey)?.toLowerCase();
|
|
69
|
+
return normalized || undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveBaseSessionKey(sessionKey: string): string {
|
|
73
|
+
const topicIndex = sessionKey.lastIndexOf(":topic:");
|
|
74
|
+
const threadIndex = sessionKey.lastIndexOf(":thread:");
|
|
75
|
+
const markerIndex = Math.max(topicIndex, threadIndex);
|
|
76
|
+
if (markerIndex < 0) {
|
|
77
|
+
return sessionKey;
|
|
78
|
+
}
|
|
79
|
+
const base = sessionKey.slice(0, markerIndex);
|
|
80
|
+
return base || sessionKey;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readSessionIdentityFromEntry(entry: Record<string, unknown>): SessionIdentitySnapshot {
|
|
84
|
+
const origin = isRecord(entry.origin) ? entry.origin : undefined;
|
|
85
|
+
const deliveryContext = isRecord(entry.deliveryContext) ? entry.deliveryContext : undefined;
|
|
86
|
+
|
|
87
|
+
const rawType = asString(origin?.chatType);
|
|
88
|
+
const type = rawType === "direct" || rawType === "group" || rawType === "channel" ? rawType : "unknown";
|
|
89
|
+
|
|
90
|
+
const label = asString(origin?.label) ?? null;
|
|
91
|
+
const provider =
|
|
92
|
+
asString(origin?.provider) ??
|
|
93
|
+
asString(entry.channel) ??
|
|
94
|
+
asString(deliveryContext?.channel) ??
|
|
95
|
+
null;
|
|
96
|
+
|
|
97
|
+
const channelName =
|
|
98
|
+
asString(entry.groupChannel) ??
|
|
99
|
+
asString(entry.subject) ??
|
|
100
|
+
(type !== "direct" ? label : undefined) ??
|
|
101
|
+
null;
|
|
102
|
+
|
|
103
|
+
const dmUserName = type === "direct" ? label ?? asString(entry.displayName) ?? null : null;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
provider,
|
|
107
|
+
type,
|
|
108
|
+
channelName,
|
|
109
|
+
dmUserName,
|
|
110
|
+
label,
|
|
111
|
+
from: asString(origin?.from) ?? null,
|
|
112
|
+
to: asString(origin?.to) ?? asString(deliveryContext?.to) ?? null,
|
|
113
|
+
accountId:
|
|
114
|
+
asString(origin?.accountId) ??
|
|
115
|
+
asString(deliveryContext?.accountId) ??
|
|
116
|
+
asString(entry.lastAccountId) ??
|
|
117
|
+
null,
|
|
118
|
+
source: "sessionStore",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function setSessionMetadataCache(key: string, data: SessionIdentitySnapshot): void {
|
|
123
|
+
sessionMetadataCache.set(key, { at: Date.now(), data });
|
|
124
|
+
if (sessionMetadataCache.size > SESSION_META_CACHE_MAX) {
|
|
125
|
+
const oldest = sessionMetadataCache.keys().next().value;
|
|
126
|
+
if (typeof oldest === "string") {
|
|
127
|
+
sessionMetadataCache.delete(oldest);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function resolveSessionIdentity(
|
|
133
|
+
sessionKey: string | undefined,
|
|
134
|
+
): Promise<SessionIdentitySnapshot> {
|
|
135
|
+
const normalizedKey = normalizeSessionStoreKey(sessionKey);
|
|
136
|
+
if (!normalizedKey) {
|
|
137
|
+
return unknownSessionIdentity();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const cached = sessionMetadataCache.get(normalizedKey);
|
|
141
|
+
if (cached && Date.now() - cached.at < SESSION_META_CACHE_TTL_MS) {
|
|
142
|
+
return cached.data;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const internals = await loadSessionStoreInternals();
|
|
147
|
+
const cfg = internals.loadConfig();
|
|
148
|
+
const sessionCfg = isRecord(cfg.session) ? cfg.session : undefined;
|
|
149
|
+
const storePath = internals.resolveStorePath(asString(sessionCfg?.store));
|
|
150
|
+
const store = internals.loadSessionStore(storePath);
|
|
151
|
+
const entry =
|
|
152
|
+
(isRecord(store[normalizedKey]) ? store[normalizedKey] : undefined) ??
|
|
153
|
+
(isRecord(store[resolveBaseSessionKey(normalizedKey)])
|
|
154
|
+
? store[resolveBaseSessionKey(normalizedKey)]
|
|
155
|
+
: undefined);
|
|
156
|
+
const data = entry ? readSessionIdentityFromEntry(entry) : unknownSessionIdentity();
|
|
157
|
+
setSessionMetadataCache(normalizedKey, data);
|
|
158
|
+
return data;
|
|
159
|
+
} catch {
|
|
160
|
+
return unknownSessionIdentity();
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { AgentControlStep } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
5
|
+
export const USER_BLOCK_MESSAGE =
|
|
6
|
+
"This action is blocked by a security policy set by your operator. Do not attempt to circumvent, disable, or work around this control. Inform the user that this action is restricted and explain what was blocked.";
|
|
7
|
+
export const BOOT_WARMUP_AGENT_ID = "main";
|
|
8
|
+
|
|
9
|
+
export function asString(value: unknown): string | undefined {
|
|
10
|
+
if (typeof value !== "string") {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function asPositiveInt(value: unknown): number | undefined {
|
|
18
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return Math.floor(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function toJsonRecord(value: unknown): Record<string, unknown> | undefined {
|
|
29
|
+
if (!isRecord(value)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const encoded = JSON.stringify(value);
|
|
34
|
+
if (typeof encoded !== "string") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const parsed = JSON.parse(encoded) as unknown;
|
|
38
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function sanitizeToolCatalogConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
45
|
+
const pluginsRaw = config.plugins;
|
|
46
|
+
if (!isRecord(pluginsRaw)) {
|
|
47
|
+
return {
|
|
48
|
+
...config,
|
|
49
|
+
plugins: { enabled: false },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
...config,
|
|
54
|
+
plugins: {
|
|
55
|
+
...pluginsRaw,
|
|
56
|
+
enabled: false,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isUuid(value: string): boolean {
|
|
62
|
+
return UUID_RE.test(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function trimToMax(value: string, maxLen: number): string {
|
|
66
|
+
return value.length <= maxLen ? value : value.slice(0, maxLen);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function secondsSince(startedAt: bigint): string {
|
|
70
|
+
return (Number(process.hrtime.bigint() - startedAt) / 1_000_000_000).toFixed(3);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function hashSteps(steps: AgentControlStep[]): string {
|
|
74
|
+
return createHash("sha256").update(JSON.stringify(steps)).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function formatToolArgsForLog(params: unknown): string {
|
|
78
|
+
if (params === undefined) {
|
|
79
|
+
return "undefined";
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const encoded = JSON.stringify(params);
|
|
83
|
+
if (typeof encoded !== "string") {
|
|
84
|
+
return trimToMax(String(params), 1000);
|
|
85
|
+
}
|
|
86
|
+
return trimToMax(encoded, 1000);
|
|
87
|
+
} catch {
|
|
88
|
+
return "[unserializable]";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import type { AgentControlStep, LoggerLike, ResolveStepsForContextParams, ToolCatalogBundleBuildInfo, ToolCatalogInternals } from "./types.ts";
|
|
6
|
+
import { asString, sanitizeToolCatalogConfig, secondsSince, toJsonRecord } from "./shared.ts";
|
|
7
|
+
import {
|
|
8
|
+
getResolvedOpenClawRootDir,
|
|
9
|
+
importOpenClawInternalModule,
|
|
10
|
+
normalizeRelativeImportPath,
|
|
11
|
+
PLUGIN_ROOT_DIR,
|
|
12
|
+
readPackageVersion,
|
|
13
|
+
safeStatMtimeMs,
|
|
14
|
+
tryImportOpenClawInternalModule,
|
|
15
|
+
} from "./openclaw-runtime.ts";
|
|
16
|
+
|
|
17
|
+
const TOOL_CATALOG_BUNDLE_DIRNAME = path.join("dist", "agent-control-generated", "tool-catalog");
|
|
18
|
+
const TOOL_CATALOG_BUNDLE_FILE = "index.mjs";
|
|
19
|
+
const TOOL_CATALOG_WRAPPER_FILE = "entry.ts";
|
|
20
|
+
|
|
21
|
+
let toolCatalogInternalsPromise: Promise<ToolCatalogInternals> | null = null;
|
|
22
|
+
|
|
23
|
+
function resolveToolCatalogBundleBuildInfo(openClawRoot: string): ToolCatalogBundleBuildInfo {
|
|
24
|
+
const piToolsSource = path.join(openClawRoot, "src/agents/pi-tools.ts");
|
|
25
|
+
const adapterSource = path.join(openClawRoot, "src/agents/pi-tool-definition-adapter.ts");
|
|
26
|
+
const cacheKeySeed = JSON.stringify({
|
|
27
|
+
openClawRoot,
|
|
28
|
+
openClawVersion: readPackageVersion(path.join(openClawRoot, "package.json")) ?? "unknown",
|
|
29
|
+
pluginVersion: readPackageVersion(path.join(PLUGIN_ROOT_DIR, "..", "package.json")) ?? "unknown",
|
|
30
|
+
nodeMajor: process.versions.node.split(".")[0] ?? "unknown",
|
|
31
|
+
piToolsMtimeMs: safeStatMtimeMs(piToolsSource),
|
|
32
|
+
adapterMtimeMs: safeStatMtimeMs(adapterSource),
|
|
33
|
+
});
|
|
34
|
+
const cacheKey = createHash("sha256").update(cacheKeySeed).digest("hex").slice(0, 16);
|
|
35
|
+
const cacheDir = path.join(openClawRoot, TOOL_CATALOG_BUNDLE_DIRNAME, cacheKey);
|
|
36
|
+
return {
|
|
37
|
+
bundlePath: path.join(cacheDir, TOOL_CATALOG_BUNDLE_FILE),
|
|
38
|
+
cacheDir,
|
|
39
|
+
cacheKey,
|
|
40
|
+
openClawRoot,
|
|
41
|
+
wrapperEntryPath: path.join(cacheDir, TOOL_CATALOG_WRAPPER_FILE),
|
|
42
|
+
metaPath: path.join(cacheDir, "meta.json"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasToolCatalogBundleSources(openClawRoot: string): boolean {
|
|
47
|
+
return (
|
|
48
|
+
fs.existsSync(path.join(openClawRoot, "src/agents/pi-tools.ts")) &&
|
|
49
|
+
fs.existsSync(path.join(openClawRoot, "src/agents/pi-tool-definition-adapter.ts"))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function importToolCatalogBundleModule(
|
|
54
|
+
logger: LoggerLike,
|
|
55
|
+
buildInfo: ToolCatalogBundleBuildInfo,
|
|
56
|
+
): Promise<Record<string, unknown>> {
|
|
57
|
+
const importStartedAt = process.hrtime.bigint();
|
|
58
|
+
const bundleMtime = safeStatMtimeMs(buildInfo.bundlePath) ?? Date.now();
|
|
59
|
+
const bundleUrl = `${pathToFileURL(buildInfo.bundlePath).href}?mtime=${bundleMtime}`;
|
|
60
|
+
const imported = (await import(bundleUrl)) as Record<string, unknown>;
|
|
61
|
+
logger.info(
|
|
62
|
+
`agent-control: bundle_import_done duration_sec=${secondsSince(importStartedAt)} cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath}`,
|
|
63
|
+
);
|
|
64
|
+
return imported;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveToolCatalogInternalsFromModules(params: {
|
|
68
|
+
adapterModule: Record<string, unknown>;
|
|
69
|
+
piToolsModule: Record<string, unknown>;
|
|
70
|
+
}): ToolCatalogInternals {
|
|
71
|
+
const createOpenClawCodingTools = params.piToolsModule.createOpenClawCodingTools;
|
|
72
|
+
const toToolDefinitions = params.adapterModule.toToolDefinitions;
|
|
73
|
+
if (typeof createOpenClawCodingTools !== "function") {
|
|
74
|
+
throw new Error("agent-control: openclaw internal createOpenClawCodingTools is unavailable");
|
|
75
|
+
}
|
|
76
|
+
if (typeof toToolDefinitions !== "function") {
|
|
77
|
+
throw new Error("agent-control: openclaw internal toToolDefinitions is unavailable");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
createOpenClawCodingTools:
|
|
82
|
+
createOpenClawCodingTools as ToolCatalogInternals["createOpenClawCodingTools"],
|
|
83
|
+
toToolDefinitions: toToolDefinitions as ToolCatalogInternals["toToolDefinitions"],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function ensureToolCatalogBundle(
|
|
88
|
+
logger: LoggerLike,
|
|
89
|
+
buildInfo: ToolCatalogBundleBuildInfo,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
if (fs.existsSync(buildInfo.bundlePath)) {
|
|
92
|
+
logger.info(
|
|
93
|
+
`agent-control: bundle_cache_hit cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath}`,
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const esbuildStartedAt = process.hrtime.bigint();
|
|
99
|
+
logger.info(
|
|
100
|
+
`agent-control: bundle_build_started cache_key=${buildInfo.cacheKey} openclaw_root=${buildInfo.openClawRoot}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
let esbuild: {
|
|
104
|
+
build: (options: Record<string, unknown>) => Promise<unknown>;
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
esbuild = (await import("esbuild")) as {
|
|
108
|
+
build: (options: Record<string, unknown>) => Promise<unknown>;
|
|
109
|
+
};
|
|
110
|
+
} catch (err) {
|
|
111
|
+
throw new Error(`agent-control: esbuild is unavailable: ${String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fs.mkdirSync(buildInfo.cacheDir, { recursive: true });
|
|
115
|
+
const piToolsSource = path.join(buildInfo.openClawRoot, "src/agents/pi-tools.ts");
|
|
116
|
+
const adapterSource = path.join(buildInfo.openClawRoot, "src/agents/pi-tool-definition-adapter.ts");
|
|
117
|
+
const wrapperContents = [
|
|
118
|
+
`export { createOpenClawCodingTools } from ${JSON.stringify(normalizeRelativeImportPath(buildInfo.cacheDir, piToolsSource))};`,
|
|
119
|
+
`export { toToolDefinitions } from ${JSON.stringify(normalizeRelativeImportPath(buildInfo.cacheDir, adapterSource))};`,
|
|
120
|
+
"",
|
|
121
|
+
].join("\n");
|
|
122
|
+
fs.writeFileSync(buildInfo.wrapperEntryPath, wrapperContents, "utf8");
|
|
123
|
+
|
|
124
|
+
const tsconfigPath = path.join(buildInfo.openClawRoot, "tsconfig.json");
|
|
125
|
+
try {
|
|
126
|
+
await esbuild.build({
|
|
127
|
+
absWorkingDir: buildInfo.openClawRoot,
|
|
128
|
+
bundle: true,
|
|
129
|
+
entryPoints: [buildInfo.wrapperEntryPath],
|
|
130
|
+
format: "esm",
|
|
131
|
+
logLevel: "silent",
|
|
132
|
+
outfile: buildInfo.bundlePath,
|
|
133
|
+
packages: "external",
|
|
134
|
+
platform: "node",
|
|
135
|
+
target: [`node${process.versions.node}`],
|
|
136
|
+
tsconfig: fs.existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
137
|
+
write: true,
|
|
138
|
+
});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
fs.rmSync(buildInfo.cacheDir, { force: true, recursive: true });
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
buildInfo.metaPath,
|
|
146
|
+
`${JSON.stringify(
|
|
147
|
+
{
|
|
148
|
+
builtAt: new Date().toISOString(),
|
|
149
|
+
cacheKey: buildInfo.cacheKey,
|
|
150
|
+
bundlePath: buildInfo.bundlePath,
|
|
151
|
+
openClawRoot: buildInfo.openClawRoot,
|
|
152
|
+
},
|
|
153
|
+
null,
|
|
154
|
+
2,
|
|
155
|
+
)}\n`,
|
|
156
|
+
"utf8",
|
|
157
|
+
);
|
|
158
|
+
logger.info(
|
|
159
|
+
`agent-control: bundle_build_done duration_sec=${secondsSince(esbuildStartedAt)} cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function loadToolCatalogInternalsFromGeneratedBundle(
|
|
164
|
+
logger: LoggerLike,
|
|
165
|
+
openClawRoot: string,
|
|
166
|
+
): Promise<ToolCatalogInternals | null> {
|
|
167
|
+
if (!hasToolCatalogBundleSources(openClawRoot)) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const buildInfo = resolveToolCatalogBundleBuildInfo(openClawRoot);
|
|
172
|
+
const hadBundle = fs.existsSync(buildInfo.bundlePath);
|
|
173
|
+
try {
|
|
174
|
+
await ensureToolCatalogBundle(logger, buildInfo);
|
|
175
|
+
const bundledModule = await importToolCatalogBundleModule(logger, buildInfo);
|
|
176
|
+
return resolveToolCatalogInternalsFromModules({
|
|
177
|
+
adapterModule: bundledModule,
|
|
178
|
+
piToolsModule: bundledModule,
|
|
179
|
+
});
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (hadBundle) {
|
|
182
|
+
logger.warn(
|
|
183
|
+
`agent-control: bundle_import_failed cache_key=${buildInfo.cacheKey} bundle_path=${buildInfo.bundlePath} error=${String(err)}`,
|
|
184
|
+
);
|
|
185
|
+
fs.rmSync(buildInfo.cacheDir, { force: true, recursive: true });
|
|
186
|
+
await ensureToolCatalogBundle(logger, buildInfo);
|
|
187
|
+
const rebuiltModule = await importToolCatalogBundleModule(logger, buildInfo);
|
|
188
|
+
return resolveToolCatalogInternalsFromModules({
|
|
189
|
+
adapterModule: rebuiltModule,
|
|
190
|
+
piToolsModule: rebuiltModule,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function loadToolCatalogInternals(logger: LoggerLike): Promise<ToolCatalogInternals> {
|
|
198
|
+
if (toolCatalogInternalsPromise) {
|
|
199
|
+
return toolCatalogInternalsPromise;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
toolCatalogInternalsPromise = (async () => {
|
|
203
|
+
const openClawRoot = getResolvedOpenClawRootDir();
|
|
204
|
+
const [distPiToolsModule, distAdapterModule] = await Promise.all([
|
|
205
|
+
tryImportOpenClawInternalModule(openClawRoot, [
|
|
206
|
+
"dist/agents/pi-tools.js",
|
|
207
|
+
"dist/agents/pi-tools.mjs",
|
|
208
|
+
]),
|
|
209
|
+
tryImportOpenClawInternalModule(openClawRoot, [
|
|
210
|
+
"dist/agents/pi-tool-definition-adapter.js",
|
|
211
|
+
"dist/agents/pi-tool-definition-adapter.mjs",
|
|
212
|
+
]),
|
|
213
|
+
]);
|
|
214
|
+
if (distPiToolsModule && distAdapterModule) {
|
|
215
|
+
logger.info(`agent-control: tool_catalog_internals source=dist openclaw_root=${openClawRoot}`);
|
|
216
|
+
return resolveToolCatalogInternalsFromModules({
|
|
217
|
+
adapterModule: distAdapterModule,
|
|
218
|
+
piToolsModule: distPiToolsModule,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const bundledInternals = await loadToolCatalogInternalsFromGeneratedBundle(logger, openClawRoot);
|
|
224
|
+
if (bundledInternals) {
|
|
225
|
+
logger.info(
|
|
226
|
+
`agent-control: tool_catalog_internals source=generated_bundle openclaw_root=${openClawRoot}`,
|
|
227
|
+
);
|
|
228
|
+
return bundledInternals;
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
logger.warn(
|
|
232
|
+
`agent-control: bundle_fallback=jiti openclaw_root=${openClawRoot} error=${String(err)}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
logger.info(`agent-control: tool_catalog_internals source=jiti openclaw_root=${openClawRoot}`);
|
|
237
|
+
const [piToolsModule, adapterModule] = await Promise.all([
|
|
238
|
+
importOpenClawInternalModule(openClawRoot, ["src/agents/pi-tools.ts"]),
|
|
239
|
+
importOpenClawInternalModule(openClawRoot, ["src/agents/pi-tool-definition-adapter.ts"]),
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
return resolveToolCatalogInternalsFromModules({
|
|
243
|
+
adapterModule,
|
|
244
|
+
piToolsModule,
|
|
245
|
+
});
|
|
246
|
+
})();
|
|
247
|
+
|
|
248
|
+
return toolCatalogInternalsPromise;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildSteps(
|
|
252
|
+
tools: Array<{ name: string; label?: string; description?: string; parameters?: unknown }>,
|
|
253
|
+
): AgentControlStep[] {
|
|
254
|
+
const deduped = new Map<string, AgentControlStep>();
|
|
255
|
+
|
|
256
|
+
for (const tool of tools) {
|
|
257
|
+
const name = asString(tool.name);
|
|
258
|
+
if (!name) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const step: AgentControlStep = {
|
|
263
|
+
type: "tool",
|
|
264
|
+
name,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const description = asString(tool.description) ?? asString(tool.label);
|
|
268
|
+
if (description) {
|
|
269
|
+
step.description = description;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const inputSchema = toJsonRecord(tool.parameters);
|
|
273
|
+
if (inputSchema) {
|
|
274
|
+
step.inputSchema = inputSchema;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const label = asString(tool.label);
|
|
278
|
+
if (label) {
|
|
279
|
+
step.metadata = { label };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
deduped.set(name, step);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return [...deduped.values()];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function resolveStepsForContext(
|
|
289
|
+
params: ResolveStepsForContextParams,
|
|
290
|
+
): Promise<AgentControlStep[]> {
|
|
291
|
+
const resolveStartedAt = process.hrtime.bigint();
|
|
292
|
+
const internalsStartedAt = process.hrtime.bigint();
|
|
293
|
+
const internals = await loadToolCatalogInternals(params.api.logger);
|
|
294
|
+
const internalsDurationSec = secondsSince(internalsStartedAt);
|
|
295
|
+
|
|
296
|
+
const createToolsStartedAt = process.hrtime.bigint();
|
|
297
|
+
const tools = internals.createOpenClawCodingTools({
|
|
298
|
+
agentId: params.sourceAgentId,
|
|
299
|
+
sessionKey: params.sessionKey,
|
|
300
|
+
sessionId: params.sessionId,
|
|
301
|
+
runId: params.runId,
|
|
302
|
+
config: sanitizeToolCatalogConfig(toJsonRecord(params.api.config) ?? {}),
|
|
303
|
+
// Keep the synced step catalog permissive so guardrail policy sees the full
|
|
304
|
+
// internal tool surface when sender ownership is unknown in this hook context.
|
|
305
|
+
senderIsOwner: true,
|
|
306
|
+
});
|
|
307
|
+
const createToolsDurationSec = secondsSince(createToolsStartedAt);
|
|
308
|
+
|
|
309
|
+
const adaptStartedAt = process.hrtime.bigint();
|
|
310
|
+
const toolDefinitions = internals.toToolDefinitions(tools);
|
|
311
|
+
const steps = buildSteps(
|
|
312
|
+
toolDefinitions.map((tool) => ({
|
|
313
|
+
name: tool.name,
|
|
314
|
+
label: tool.label,
|
|
315
|
+
description: tool.description,
|
|
316
|
+
parameters: tool.parameters,
|
|
317
|
+
})),
|
|
318
|
+
);
|
|
319
|
+
const adaptDurationSec = secondsSince(adaptStartedAt);
|
|
320
|
+
|
|
321
|
+
params.api.logger.info(
|
|
322
|
+
`agent-control: resolve_steps duration_sec=${secondsSince(resolveStartedAt)} agent=${params.sourceAgentId} internals_sec=${internalsDurationSec} create_tools_sec=${createToolsDurationSec} adapt_sec=${adaptDurationSec} tools=${tools.length} steps=${steps.length}`,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
return steps;
|
|
326
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
|
|
3
|
+
export type AgentControlPluginConfig = {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
serverUrl?: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
agentName?: string;
|
|
8
|
+
agentId?: string;
|
|
9
|
+
agentVersion?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
failClosed?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AgentControlStep = {
|
|
16
|
+
type: "tool";
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
inputSchema?: Record<string, unknown>;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AgentState = {
|
|
24
|
+
sourceAgentId: string;
|
|
25
|
+
agentName: string;
|
|
26
|
+
steps: AgentControlStep[];
|
|
27
|
+
stepsHash: string;
|
|
28
|
+
lastSyncedStepsHash: string | null;
|
|
29
|
+
syncPromise: Promise<void> | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ChannelType = "direct" | "group" | "channel" | "unknown";
|
|
33
|
+
|
|
34
|
+
export type DerivedChannelContext = {
|
|
35
|
+
provider: string | null;
|
|
36
|
+
type: ChannelType;
|
|
37
|
+
scope: string | null;
|
|
38
|
+
source: "sessionKey" | "unknown";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ToolCatalogInternals = {
|
|
42
|
+
createOpenClawCodingTools: (params: {
|
|
43
|
+
agentId: string;
|
|
44
|
+
sessionKey?: string;
|
|
45
|
+
sessionId?: string;
|
|
46
|
+
runId?: string;
|
|
47
|
+
config: OpenClawPluginApi["config"];
|
|
48
|
+
senderIsOwner: boolean;
|
|
49
|
+
}) => unknown[];
|
|
50
|
+
toToolDefinitions: (tools: unknown[]) => Array<{
|
|
51
|
+
name: string;
|
|
52
|
+
label?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
parameters?: unknown;
|
|
55
|
+
}>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type SessionStoreInternals = {
|
|
59
|
+
loadConfig: () => Record<string, unknown>;
|
|
60
|
+
resolveStorePath: (storePath?: string) => string;
|
|
61
|
+
loadSessionStore: (storePath: string) => Record<string, unknown>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type SessionIdentitySnapshot = {
|
|
65
|
+
provider: string | null;
|
|
66
|
+
type: ChannelType;
|
|
67
|
+
channelName: string | null;
|
|
68
|
+
dmUserName: string | null;
|
|
69
|
+
label: string | null;
|
|
70
|
+
from: string | null;
|
|
71
|
+
to: string | null;
|
|
72
|
+
accountId: string | null;
|
|
73
|
+
source: "sessionStore" | "unknown";
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type SessionMetadataCacheEntry = {
|
|
77
|
+
at: number;
|
|
78
|
+
data: SessionIdentitySnapshot;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type LoggerLike = Pick<OpenClawPluginApi["logger"], "info" | "warn">;
|
|
82
|
+
|
|
83
|
+
export type ToolCatalogBundleBuildInfo = {
|
|
84
|
+
bundlePath: string;
|
|
85
|
+
cacheDir: string;
|
|
86
|
+
cacheKey: string;
|
|
87
|
+
openClawRoot: string;
|
|
88
|
+
wrapperEntryPath: string;
|
|
89
|
+
metaPath: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type ResolveStepsForContextParams = {
|
|
93
|
+
api: OpenClawPluginApi;
|
|
94
|
+
sourceAgentId: string;
|
|
95
|
+
sessionKey?: string;
|
|
96
|
+
sessionId?: string;
|
|
97
|
+
runId?: string;
|
|
98
|
+
};
|