@unblocklabs/unblock-codex-prompt 0.1.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 +21 -0
- package/README.md +229 -0
- package/RELEASING.md +72 -0
- package/dist/adapter.d.ts +37 -0
- package/dist/adapter.js +104 -0
- package/dist/catalog.d.ts +51 -0
- package/dist/catalog.js +185 -0
- package/dist/compiler.d.ts +23 -0
- package/dist/compiler.js +57 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +48 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +153 -0
- package/dist/manager.d.ts +80 -0
- package/dist/manager.js +218 -0
- package/dist/remote.d.ts +9 -0
- package/dist/remote.js +44 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.js +30 -0
- package/dist/toml.d.ts +3 -0
- package/dist/toml.js +49 -0
- package/openclaw.plugin.json +25 -0
- package/package.json +44 -0
- package/prompts/developer.md +22 -0
- package/prompts/system.md +173 -0
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { parseTOML, getStaticTOMLValue } from "toml-eslint-parser";
|
|
3
|
+
import { readOptional, replaceFile, sha256, withLock } from "./manager.js";
|
|
4
|
+
import { readPromptPointer, setPromptPointer } from "./toml.js";
|
|
5
|
+
export const CATALOG_REFRESH_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const KEY = "model_catalog_json";
|
|
7
|
+
function record(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
8
|
+
function files(agentDir) {
|
|
9
|
+
const home = join(resolve(agentDir), "codex-home");
|
|
10
|
+
return { home, config: join(home, "config.toml"), catalog: join(home, "unblock-codex-models.json"), receipt: join(home, ".unblock-codex-catalog.json") };
|
|
11
|
+
}
|
|
12
|
+
function parseReceipt(source) {
|
|
13
|
+
if (source === null)
|
|
14
|
+
return null;
|
|
15
|
+
const value = JSON.parse(source);
|
|
16
|
+
if (!record(value) || value.schema !== 1 || typeof value.active !== "boolean" ||
|
|
17
|
+
(value.previousPointer !== null && typeof value.previousPointer !== "string") ||
|
|
18
|
+
typeof value.refreshedAt !== "number" || !Number.isFinite(value.refreshedAt) ||
|
|
19
|
+
typeof value.sha256 !== "string" || typeof value.sourceSha256 !== "string" || typeof value.modelCount !== "number") {
|
|
20
|
+
throw new Error("Invalid catalog receipt");
|
|
21
|
+
}
|
|
22
|
+
return { schema: 1, active: value.active, previousPointer: value.previousPointer, refreshedAt: value.refreshedAt,
|
|
23
|
+
sha256: value.sha256, sourceSha256: value.sourceSha256, modelCount: value.modelCount };
|
|
24
|
+
}
|
|
25
|
+
export function patchCatalog(value) {
|
|
26
|
+
if (!record(value) || !Array.isArray(value.models) || value.models.length === 0)
|
|
27
|
+
throw new Error("Upstream response is not a full Codex model catalog");
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const models = value.models.map((model) => {
|
|
30
|
+
if (!record(model) || typeof model.slug !== "string" || !model.slug || seen.has(model.slug) ||
|
|
31
|
+
typeof model.base_instructions !== "string" || !Array.isArray(model.supported_reasoning_levels)) {
|
|
32
|
+
throw new Error("Invalid or duplicate Codex model metadata");
|
|
33
|
+
}
|
|
34
|
+
seen.add(model.slug);
|
|
35
|
+
const messages = model.model_messages ?? {};
|
|
36
|
+
if (!record(messages))
|
|
37
|
+
throw new Error("Invalid model_messages");
|
|
38
|
+
const multi = messages.multi_agent ?? {};
|
|
39
|
+
if (!record(multi))
|
|
40
|
+
throw new Error("Invalid multi_agent metadata");
|
|
41
|
+
const mode = multi.mode ?? {};
|
|
42
|
+
if (!record(mode))
|
|
43
|
+
throw new Error("Invalid multi_agent mode metadata");
|
|
44
|
+
return { ...model, model_messages: { ...messages, multi_agent: { ...multi, mode: { ...mode, explicit: "" } } } };
|
|
45
|
+
});
|
|
46
|
+
return { ...value, models };
|
|
47
|
+
}
|
|
48
|
+
/** Use the configured provider, not the override file or its refreshable cache. */
|
|
49
|
+
export async function fetchUpstreamCatalog(agentDir, configText, request = fetch) {
|
|
50
|
+
let config;
|
|
51
|
+
try {
|
|
52
|
+
config = getStaticTOMLValue(parseTOML(configText));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new Error("Invalid Codex TOML");
|
|
56
|
+
}
|
|
57
|
+
if (!record(config) || typeof config.model_provider !== "string" || !record(config.model_providers))
|
|
58
|
+
throw new Error("Catalog refresh requires an explicit Codex provider");
|
|
59
|
+
const provider = config.model_providers[config.model_provider];
|
|
60
|
+
if (!record(provider) || typeof provider.base_url !== "string" || typeof provider.env_key !== "string") {
|
|
61
|
+
throw new Error("Catalog refresh requires a provider base_url and existing env_key credential");
|
|
62
|
+
}
|
|
63
|
+
// Never resolve arbitrary auth commands, copy credentials, or fall back to another endpoint.
|
|
64
|
+
const token = process.env[provider.env_key];
|
|
65
|
+
if (!token)
|
|
66
|
+
throw new Error("Configured catalog provider credential is unavailable in the plugin environment");
|
|
67
|
+
const url = new URL(provider.base_url.replace(/\/$/u, "") + "/models");
|
|
68
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.hash || url.search)
|
|
69
|
+
throw new Error("Unsupported catalog provider URL");
|
|
70
|
+
const cached = JSON.parse(await readOptional(join(files(agentDir).home, "models_cache.json")) ?? "null");
|
|
71
|
+
if (!record(cached) || typeof cached.client_version !== "string" || !/^\d+\.\d+\.\d+$/u.test(cached.client_version))
|
|
72
|
+
throw new Error("Managed Codex client version missing from its model cache");
|
|
73
|
+
url.searchParams.set("client_version", cached.client_version);
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await request(url, { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
|
|
77
|
+
redirect: "error", signal: AbortSignal.timeout(15_000) });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new Error("Catalog request failed; installed catalog retained");
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok || !response.body)
|
|
83
|
+
throw new Error(`Catalog endpoint returned HTTP ${response.status}`);
|
|
84
|
+
const reader = response.body.getReader();
|
|
85
|
+
const chunks = [];
|
|
86
|
+
let size = 0;
|
|
87
|
+
try {
|
|
88
|
+
for (;;) {
|
|
89
|
+
const { done, value } = await reader.read();
|
|
90
|
+
if (done)
|
|
91
|
+
break;
|
|
92
|
+
size += value.byteLength;
|
|
93
|
+
if (size > 8 * 1024 * 1024)
|
|
94
|
+
throw new Error("Catalog response exceeds 8 MiB");
|
|
95
|
+
chunks.push(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await reader.cancel();
|
|
100
|
+
reader.releaseLock();
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new Error("Catalog endpoint returned invalid JSON");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function catalogStatus(agentDir) {
|
|
110
|
+
const path = files(agentDir);
|
|
111
|
+
const receipt = parseReceipt(await readOptional(path.receipt));
|
|
112
|
+
const text = await readOptional(path.catalog);
|
|
113
|
+
return { enabled: receipt?.active === true, catalogPath: path.catalog, pointerMatches: readPromptPointer(await readOptional(path.config) ?? "", KEY) === path.catalog,
|
|
114
|
+
contentMatches: receipt !== null && text !== null && sha256(text) === receipt.sha256,
|
|
115
|
+
refreshedAt: receipt?.refreshedAt ?? null, nextRefreshAt: receipt ? receipt.refreshedAt + CATALOG_REFRESH_MS : null, modelCount: receipt?.modelCount ?? 0 };
|
|
116
|
+
}
|
|
117
|
+
export async function refreshCatalog(agentDir, options = {}) {
|
|
118
|
+
return withLock({ agentDir, prompt: "" }, async () => {
|
|
119
|
+
const path = files(agentDir);
|
|
120
|
+
const config = await readOptional(path.config) ?? "";
|
|
121
|
+
const previousReceipt = await readOptional(path.receipt);
|
|
122
|
+
const receipt = parseReceipt(previousReceipt);
|
|
123
|
+
const enrollment = JSON.parse(await readOptional(join(path.home, ".unblock-codex-prompt.json")) ?? "null");
|
|
124
|
+
if (!record(enrollment) || enrollment.active !== true || (options.automatic && receipt?.active === false))
|
|
125
|
+
return { skipped: true, changed: false };
|
|
126
|
+
const pointer = readPromptPointer(config, KEY);
|
|
127
|
+
if (pointer !== null && pointer !== path.catalog)
|
|
128
|
+
throw new Error("A different model_catalog_json is configured; refusing to replace it");
|
|
129
|
+
const previous = await readOptional(path.catalog);
|
|
130
|
+
if (!receipt && (previous !== null || pointer === path.catalog))
|
|
131
|
+
throw new Error("Unowned model catalog exists; recover its receipt first");
|
|
132
|
+
// Deliberate edits to this generated file are overwritten from upstream; never reuse it as source.
|
|
133
|
+
const upstream = await (options.fetchCatalog ?? fetchUpstreamCatalog)(agentDir, config);
|
|
134
|
+
const patched = patchCatalog(upstream);
|
|
135
|
+
const contents = `${JSON.stringify(patched, null, 2)}\n`;
|
|
136
|
+
const next = { schema: 1, active: true, previousPointer: receipt?.active ? receipt.previousPointer : pointer,
|
|
137
|
+
refreshedAt: options.now ?? Date.now(), sha256: sha256(contents), sourceSha256: sha256(JSON.stringify(upstream)), modelCount: patched.models.length };
|
|
138
|
+
await replaceFile(path.receipt, previousReceipt, `${JSON.stringify(next, null, 2)}\n`);
|
|
139
|
+
const catalogChanged = await replaceFile(path.catalog, previous, contents);
|
|
140
|
+
const configChanged = await replaceFile(path.config, config, setPromptPointer(config, path.catalog, KEY));
|
|
141
|
+
// The pinned managed Codex reloads catalog contents for fresh sessions. A new
|
|
142
|
+
// config pointer still requires activation; existing conversations are not reset.
|
|
143
|
+
return { changed: catalogChanged || configChanged, catalogChanged, configChanged, modelCount: next.modelCount, refreshedAt: next.refreshedAt, sha256: next.sha256, restartRequired: configChanged, newSessionRequired: catalogChanged || configChanged };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
export async function restoreCatalog(agentDir) {
|
|
147
|
+
return withLock({ agentDir, prompt: "" }, async () => {
|
|
148
|
+
const path = files(agentDir);
|
|
149
|
+
const source = await readOptional(path.receipt);
|
|
150
|
+
const receipt = parseReceipt(source);
|
|
151
|
+
if (!receipt)
|
|
152
|
+
return;
|
|
153
|
+
const config = await readOptional(path.config) ?? "";
|
|
154
|
+
const pointer = readPromptPointer(config, KEY);
|
|
155
|
+
if (pointer !== path.catalog && pointer !== receipt.previousPointer)
|
|
156
|
+
throw new Error("Foreign catalog pointer; refusing restore");
|
|
157
|
+
await replaceFile(path.receipt, source, `${JSON.stringify({ ...receipt, active: false }, null, 2)}\n`);
|
|
158
|
+
await replaceFile(path.config, config, setPromptPointer(config, receipt.previousPointer, KEY));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Start promptly if overdue, persist successful refresh time, retry failures hourly. */
|
|
162
|
+
export function startCatalogRefresh(run, nextAt, onError) {
|
|
163
|
+
let stopped = false;
|
|
164
|
+
let timer;
|
|
165
|
+
let pending;
|
|
166
|
+
const schedule = (delay) => {
|
|
167
|
+
timer = setTimeout(() => {
|
|
168
|
+
pending = (async () => {
|
|
169
|
+
let next = CATALOG_REFRESH_MS;
|
|
170
|
+
try {
|
|
171
|
+
await run();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
onError();
|
|
175
|
+
next = 60 * 60 * 1000;
|
|
176
|
+
}
|
|
177
|
+
if (!stopped)
|
|
178
|
+
schedule(next);
|
|
179
|
+
})();
|
|
180
|
+
}, Math.max(0, delay));
|
|
181
|
+
timer.unref();
|
|
182
|
+
};
|
|
183
|
+
schedule(nextAt - Date.now());
|
|
184
|
+
return async () => { stopped = true; clearTimeout(timer); await pending; };
|
|
185
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
export declare const SUPPORTED_OPENCLAW = "2026.9.2";
|
|
3
|
+
export declare function openclawRoot(): Promise<string>;
|
|
4
|
+
export declare function buildSkillsCatalog(config: OpenClawConfig, agentId: string, workspaceDir: string): Promise<{
|
|
5
|
+
prompt: string;
|
|
6
|
+
count: number;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function compilePrompt(base: string, policy: string, workspaceDir: string, catalog: {
|
|
9
|
+
prompt: string;
|
|
10
|
+
count: number;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
prompt: string;
|
|
13
|
+
bundle: {
|
|
14
|
+
schema: 1;
|
|
15
|
+
policy: string;
|
|
16
|
+
sources: {
|
|
17
|
+
name: string;
|
|
18
|
+
sha256: string | null;
|
|
19
|
+
}[];
|
|
20
|
+
skillCount: number;
|
|
21
|
+
basePromptLength: number;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { readOptional, sha256 } from "./manager.js";
|
|
5
|
+
export const SUPPORTED_OPENCLAW = "2026.9.2";
|
|
6
|
+
export async function openclawRoot() {
|
|
7
|
+
let directory = dirname(fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/agent-runtime")));
|
|
8
|
+
for (;;) {
|
|
9
|
+
const source = await readOptional(join(directory, "package.json"));
|
|
10
|
+
if (source) {
|
|
11
|
+
const pkg = JSON.parse(source);
|
|
12
|
+
if (pkg && typeof pkg === "object" && "name" in pkg && pkg.name === "openclaw") {
|
|
13
|
+
if (!("version" in pkg) || pkg.version !== SUPPORTED_OPENCLAW)
|
|
14
|
+
throw new Error(`Frozen bridge supports OpenClaw ${SUPPORTED_OPENCLAW} only; review upgrades before syncing`);
|
|
15
|
+
return directory;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const parent = dirname(directory);
|
|
19
|
+
if (parent === directory)
|
|
20
|
+
throw new Error("Cannot locate the running OpenClaw package");
|
|
21
|
+
directory = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// OpenClaw has no public catalog-building SDK. Pin this single internal export,
|
|
25
|
+
// retaining its eligibility, agent allowlist, prompt visibility and size limits.
|
|
26
|
+
export async function buildSkillsCatalog(config, agentId, workspaceDir) {
|
|
27
|
+
const dist = join(await openclawRoot(), "dist");
|
|
28
|
+
const names = (await readdir(dist)).filter(name => /^workspace-skill-prompt-[\w-]+\.js$/u.test(name));
|
|
29
|
+
if (names.length !== 1)
|
|
30
|
+
throw new Error("Unsupported OpenClaw skills module layout");
|
|
31
|
+
const path = join(dist, names[0]);
|
|
32
|
+
const source = await readFile(path, "utf8");
|
|
33
|
+
if (!source.includes("buildSkillSnapshot as t"))
|
|
34
|
+
throw new Error("Unsupported OpenClaw skills export");
|
|
35
|
+
const module = await import(pathToFileURL(path).href);
|
|
36
|
+
if (typeof module.t !== "function")
|
|
37
|
+
throw new Error("Missing OpenClaw skill snapshot builder");
|
|
38
|
+
const result = module.t(workspaceDir, { config, agentId });
|
|
39
|
+
if (!result || typeof result !== "object" || !("prompt" in result) || typeof result.prompt !== "string" ||
|
|
40
|
+
!("resolvedSkills" in result) || !Array.isArray(result.resolvedSkills))
|
|
41
|
+
throw new Error("Unsupported OpenClaw skill snapshot result");
|
|
42
|
+
return { prompt: result.prompt, count: result.resolvedSkills.length };
|
|
43
|
+
}
|
|
44
|
+
export async function compilePrompt(base, policy, workspaceDir, catalog) {
|
|
45
|
+
const sources = [];
|
|
46
|
+
const sections = [base.trim(), "## Frozen agent context\nThis context is a local snapshot, refreshed explicitly by the operator. Native AGENTS.md, native skills, actual tool schemas and current session context remain separate. This snapshot does not grant tools or permissions."];
|
|
47
|
+
for (const name of ["SOUL.md", "IDENTITY.md", "USER.md"]) {
|
|
48
|
+
const content = await readOptional(join(workspaceDir, name));
|
|
49
|
+
sources.push({ name, sha256: content === null ? null : sha256(content) });
|
|
50
|
+
if (content?.trim())
|
|
51
|
+
sections.push(`### ${name}\n${content.trim()}`);
|
|
52
|
+
}
|
|
53
|
+
sections.push(`## Frozen memory reference\nWhen prior context matters, use the currently available memory tools and follow their schemas. If retrieval is unavailable, say so; do not invent results. The workspace memory reference is ${join(workspaceDir, "MEMORY.md")}. Its contents are not included here. Read it only when the active session's privacy policy permits; do not expose private memory in group or customer conversations.`);
|
|
54
|
+
sources.push({ name: "OpenClaw skills catalog", sha256: sha256(catalog.prompt) });
|
|
55
|
+
sections.push(`## Frozen OpenClaw skills catalog\nThis is the approved agent-workspace catalog at sync time, not a promise that credentials or tools remain available. Session-local skill selection and execution-directory skill discovery do not refresh this snapshot. Use the native catalog separately.\n\n${catalog.prompt.trim() || "No eligible model-visible OpenClaw skills at sync time."}`);
|
|
56
|
+
return { prompt: `${sections.join("\n\n")}\n`, bundle: { schema: 1, policy: policy.trim(), sources, skillCount: catalog.count, basePromptLength: base.trim().length } };
|
|
57
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const PLUGIN_ID = "unblock-codex-prompt";
|
|
2
|
+
export declare function parseConfig(raw: unknown): {
|
|
3
|
+
agentId: string;
|
|
4
|
+
agentName: string | undefined;
|
|
5
|
+
frozenContext: boolean;
|
|
6
|
+
promptUrl: string | undefined;
|
|
7
|
+
suppressExplicitDelegationPrompt: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function validatePromptUrl(value: string): URL;
|
|
10
|
+
export declare function renderPrompt(template: string, agentName?: string): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const PLUGIN_ID = "unblock-codex-prompt";
|
|
2
|
+
export function parseConfig(raw) {
|
|
3
|
+
const value = raw ?? {};
|
|
4
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
throw new Error("Plugin config must be an object");
|
|
6
|
+
for (const key of Object.keys(value)) {
|
|
7
|
+
if (key !== "agentId" && key !== "agentName" && key !== "frozenContext" && key !== "promptUrl" && key !== "suppressExplicitDelegationPrompt")
|
|
8
|
+
throw new Error(`Unknown plugin option: ${key}`);
|
|
9
|
+
}
|
|
10
|
+
const agentId = "agentId" in value ? value.agentId : "main";
|
|
11
|
+
const agentName = "agentName" in value ? value.agentName : undefined;
|
|
12
|
+
if (typeof agentId !== "string" || !/^[a-z0-9][a-z0-9_-]*$/u.test(agentId)) {
|
|
13
|
+
throw new Error("agentId must be a lowercase OpenClaw agent ID");
|
|
14
|
+
}
|
|
15
|
+
if (agentName !== undefined && (typeof agentName !== "string" || !agentName.trim() ||
|
|
16
|
+
agentName.length > 100 || /\p{C}/u.test(agentName))) {
|
|
17
|
+
throw new Error("agentName must be 1–100 characters without control characters");
|
|
18
|
+
}
|
|
19
|
+
const frozenContext = "frozenContext" in value ? value.frozenContext : false;
|
|
20
|
+
if (typeof frozenContext !== "boolean")
|
|
21
|
+
throw new Error("frozenContext must be boolean");
|
|
22
|
+
const promptUrl = "promptUrl" in value ? value.promptUrl : undefined;
|
|
23
|
+
if (promptUrl !== undefined) {
|
|
24
|
+
if (typeof promptUrl !== "string")
|
|
25
|
+
throw new Error("promptUrl must be an HTTPS URL");
|
|
26
|
+
validatePromptUrl(promptUrl);
|
|
27
|
+
if (!frozenContext)
|
|
28
|
+
throw new Error("promptUrl requires frozenContext to replace the OpenClaw policy too");
|
|
29
|
+
}
|
|
30
|
+
const suppressExplicitDelegationPrompt = "suppressExplicitDelegationPrompt" in value ? value.suppressExplicitDelegationPrompt : false;
|
|
31
|
+
if (typeof suppressExplicitDelegationPrompt !== "boolean")
|
|
32
|
+
throw new Error("suppressExplicitDelegationPrompt must be boolean");
|
|
33
|
+
if (suppressExplicitDelegationPrompt && !frozenContext)
|
|
34
|
+
throw new Error("suppressExplicitDelegationPrompt requires frozenContext");
|
|
35
|
+
return { agentId, agentName: agentName?.trim(), frozenContext, promptUrl, suppressExplicitDelegationPrompt };
|
|
36
|
+
}
|
|
37
|
+
export function validatePromptUrl(value) {
|
|
38
|
+
const url = new URL(value);
|
|
39
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash) {
|
|
40
|
+
throw new Error("promptUrl must use HTTPS without credentials or a fragment");
|
|
41
|
+
}
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
export function renderPrompt(template, agentName) {
|
|
45
|
+
if (template.split("{{identity}}").length !== 2)
|
|
46
|
+
throw new Error("Prompt template must contain exactly one identity placeholder");
|
|
47
|
+
return template.replace("{{identity}}", () => agentName ? `${agentName}, an OpenClaw agent` : "an OpenClaw agent");
|
|
48
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const _default: Omit<{
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
kind?: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["kind"];
|
|
6
|
+
configSchema?: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginConfigSchema | (() => import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginConfigSchema);
|
|
7
|
+
reload?: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["reload"];
|
|
8
|
+
nodeHostCommands?: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["nodeHostCommands"];
|
|
9
|
+
securityAuditCollectors?: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["securityAuditCollectors"];
|
|
10
|
+
register: NonNullable<import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["register"]>;
|
|
11
|
+
}, "configSchema"> & {
|
|
12
|
+
configSchema: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginConfigSchema;
|
|
13
|
+
};
|
|
14
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { listAgentIds, resolveAgentDir, resolveAgentWorkspaceDir } from "openclaw/plugin-sdk/agent-runtime";
|
|
3
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
|
+
import { parseConfig, PLUGIN_ID, renderPrompt } from "./config.js";
|
|
5
|
+
import { promptStatus, restorePrompt, syncPrompt, refreshSharedPrompt } from "./manager.js";
|
|
6
|
+
import { adapterStatus, installAdapter, prepareAdapter, restoreAdapter } from "./adapter.js";
|
|
7
|
+
import { buildSkillsCatalog, compilePrompt } from "./compiler.js";
|
|
8
|
+
import { readFrozenPolicy } from "./runtime.js";
|
|
9
|
+
import { fetchPrompt, REMOTE_POLICY } from "./remote.js";
|
|
10
|
+
import { readOptional } from "./manager.js";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { CATALOG_REFRESH_MS, catalogStatus, refreshCatalog, restoreCatalog, startCatalogRefresh } from "./catalog.js";
|
|
13
|
+
async function refresh(target, automatic = false) {
|
|
14
|
+
const failures = [];
|
|
15
|
+
let catalog;
|
|
16
|
+
let prompt;
|
|
17
|
+
// Attempt both independently, sequentially because they share the enrollment lock.
|
|
18
|
+
if (target.settings.suppressExplicitDelegationPrompt) {
|
|
19
|
+
try {
|
|
20
|
+
catalog = await refreshCatalog(target.agentDir, { automatic });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
failures.push(error);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (target.settings.promptUrl) {
|
|
27
|
+
const url = target.settings.promptUrl;
|
|
28
|
+
try {
|
|
29
|
+
prompt = await refreshSharedPrompt(target.agentDir, () => fetchPrompt(url));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
failures.push(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (failures.length)
|
|
36
|
+
throw new AggregateError(failures, "Refresh incomplete; failed components retain their installed data");
|
|
37
|
+
return { catalog, prompt };
|
|
38
|
+
}
|
|
39
|
+
async function resolveTarget(config) {
|
|
40
|
+
const settings = parseConfig(config.plugins?.entries?.[PLUGIN_ID]?.config);
|
|
41
|
+
if (!listAgentIds(config).includes(settings.agentId))
|
|
42
|
+
throw new Error(`Unknown OpenClaw agent: ${settings.agentId}`);
|
|
43
|
+
const codexConfig = config.plugins?.entries?.codex?.config;
|
|
44
|
+
const appServer = codexConfig?.appServer;
|
|
45
|
+
if (appServer !== undefined) {
|
|
46
|
+
if (!appServer || typeof appServer !== "object" || Array.isArray(appServer))
|
|
47
|
+
throw new Error("Invalid Codex appServer config");
|
|
48
|
+
if (("transport" in appServer && appServer.transport !== "stdio") ||
|
|
49
|
+
("homeScope" in appServer && appServer.homeScope !== "agent") ||
|
|
50
|
+
("command" in appServer && appServer.command !== undefined)) {
|
|
51
|
+
throw new Error("Only OpenClaw-managed, agent-scoped local stdio Codex is supported");
|
|
52
|
+
}
|
|
53
|
+
if ("args" in appServer && appServer.args !== undefined) {
|
|
54
|
+
throw new Error("Custom Codex appServer.args may override the prompt; remove them or manage the prompt outside this plugin");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { agentDir: resolveAgentDir(config, settings.agentId), settings, workspaceDir: resolveAgentWorkspaceDir(config, settings.agentId) };
|
|
58
|
+
}
|
|
59
|
+
async function bundledPrompt(agentName) {
|
|
60
|
+
return renderPrompt(await readFile(new URL("../prompts/system.md", import.meta.url), "utf8"), agentName);
|
|
61
|
+
}
|
|
62
|
+
async function status(config) {
|
|
63
|
+
const target = await resolveTarget(config);
|
|
64
|
+
const disk = await promptStatus({ ...target, prompt: target.settings.frozenContext ? "" : await bundledPrompt(target.settings.agentName) });
|
|
65
|
+
if (!target.settings.frozenContext)
|
|
66
|
+
return disk;
|
|
67
|
+
const adapter = await adapterStatus(target.agentDir);
|
|
68
|
+
let ready = false;
|
|
69
|
+
let error;
|
|
70
|
+
try {
|
|
71
|
+
ready = readFrozenPolicy(target.agentDir) !== null;
|
|
72
|
+
}
|
|
73
|
+
catch (cause) {
|
|
74
|
+
error = cause instanceof Error ? cause.message : "Invalid frozen state";
|
|
75
|
+
}
|
|
76
|
+
// A frozen snapshot intentionally differs from today's template/documents.
|
|
77
|
+
const { promptMatches: _matches, expectedSha256: _expected, ...frozenDisk } = disk;
|
|
78
|
+
const bundle = await readOptional(join(target.agentDir, "codex-home", ".unblock-codex-prompt-bundle.json"));
|
|
79
|
+
const receipt = bundle ? JSON.parse(bundle) : null;
|
|
80
|
+
const remotePrompt = receipt && typeof receipt === "object" && "remotePrompt" in receipt ? receipt.remotePrompt : null;
|
|
81
|
+
return { ...frozenDisk, frozen: true, ready: ready && adapter.installed, adapter, error, configuredPromptUrl: target.settings.promptUrl, remotePrompt };
|
|
82
|
+
}
|
|
83
|
+
export default definePluginEntry({
|
|
84
|
+
id: PLUGIN_ID,
|
|
85
|
+
name: "Unblock Codex Prompt",
|
|
86
|
+
description: "Explicitly compile a frozen Codex prompt with a narrow, version-pinned OpenClaw bridge.",
|
|
87
|
+
register(api) {
|
|
88
|
+
let stopCatalogRefresh;
|
|
89
|
+
// Registration/import stays read-only, including plugin inspection and CLI discovery.
|
|
90
|
+
api.registerService({
|
|
91
|
+
id: PLUGIN_ID,
|
|
92
|
+
reload: { configPrefixes: [`plugins.entries.${PLUGIN_ID}.config`] },
|
|
93
|
+
async start(ctx) {
|
|
94
|
+
const result = await status(ctx.config);
|
|
95
|
+
ctx.logger.info(`${PLUGIN_ID}: ${JSON.stringify(result)}`);
|
|
96
|
+
const target = await resolveTarget(ctx.config);
|
|
97
|
+
if ((target.settings.suppressExplicitDelegationPrompt || target.settings.promptUrl) && result.enrolled) {
|
|
98
|
+
const catalog = await catalogStatus(target.agentDir);
|
|
99
|
+
const remote = "remotePrompt" in result ? result.remotePrompt : null;
|
|
100
|
+
const checkedAt = remote && typeof remote === "object" && "checkedAt" in remote && typeof remote.checkedAt === "number" ? remote.checkedAt : 0;
|
|
101
|
+
const nextAt = Math.min(target.settings.suppressExplicitDelegationPrompt ? catalog.nextRefreshAt ?? 0 : Infinity, target.settings.promptUrl ? checkedAt ? checkedAt + CATALOG_REFRESH_MS : 0 : Infinity);
|
|
102
|
+
stopCatalogRefresh = startCatalogRefresh(async () => {
|
|
103
|
+
const refreshed = await refresh(target, true);
|
|
104
|
+
ctx.logger.info(`${PLUGIN_ID}: daily refresh ${JSON.stringify(refreshed)}`);
|
|
105
|
+
}, nextAt, () => ctx.logger.error(`${PLUGIN_ID}: daily refresh incomplete; failed components retain installed data, retrying in one hour`));
|
|
106
|
+
ctx.logger.info(`${PLUGIN_ID}: daily prompt/catalog refresh scheduled`);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
async stop() { await stopCatalogRefresh?.(); stopCatalogRefresh = undefined; },
|
|
110
|
+
});
|
|
111
|
+
api.registerCli(({ program, config }) => {
|
|
112
|
+
const root = program.command("codex-prompt").description("Manage this agent's Codex base prompt (JSON output)");
|
|
113
|
+
root.command("status").description("Inspect disk state without writing or claiming live adoption")
|
|
114
|
+
.action(async () => {
|
|
115
|
+
const target = await resolveTarget(config);
|
|
116
|
+
console.log(JSON.stringify({ ...await status(config), catalog: await catalogStatus(target.agentDir) }, null, 2));
|
|
117
|
+
});
|
|
118
|
+
root.command("refresh-catalog").description("Fetch fresh provider metadata and repair the managed delegation-policy catalog")
|
|
119
|
+
.action(async () => {
|
|
120
|
+
const target = await resolveTarget(config);
|
|
121
|
+
if (!target.settings.suppressExplicitDelegationPrompt)
|
|
122
|
+
throw new Error("Enable suppressExplicitDelegationPrompt before refreshing the catalog");
|
|
123
|
+
console.log(JSON.stringify(await refreshCatalog(target.agentDir), null, 2));
|
|
124
|
+
});
|
|
125
|
+
root.command("refresh").description("Refresh remote prompt and enabled catalog without rereading frozen agent context")
|
|
126
|
+
.action(async () => console.log(JSON.stringify(await refresh(await resolveTarget(config)), null, 2)));
|
|
127
|
+
const sync = root.command("sync").description("Explicitly refresh the prompt snapshot and install the reviewed adapter")
|
|
128
|
+
.option("--adopt", "Take ownership of an existing custom prompt pointer")
|
|
129
|
+
.option("--codex-plugin-dir <path>", "Verified official Codex package directory (required on first frozen sync)");
|
|
130
|
+
sync.action(async () => {
|
|
131
|
+
const target = await resolveTarget(config);
|
|
132
|
+
const options = sync.opts();
|
|
133
|
+
const plan = target.settings.frozenContext ? await prepareAdapter(target.agentDir, target.settings.agentId, options.codexPluginDir) : null;
|
|
134
|
+
const remote = target.settings.promptUrl ? await fetchPrompt(target.settings.promptUrl) : null;
|
|
135
|
+
const prompt = remote?.prompt ?? await bundledPrompt(target.settings.agentName);
|
|
136
|
+
const compiled = target.settings.frozenContext ? await compilePrompt(prompt, remote ? REMOTE_POLICY : await readFile(new URL("../prompts/developer.md", import.meta.url), "utf8"), target.workspaceDir, await buildSkillsCatalog(config, target.settings.agentId, target.workspaceDir)) : null;
|
|
137
|
+
if (compiled && remote)
|
|
138
|
+
Object.assign(compiled.bundle, { remotePrompt: { ...remote.source, checkedAt: Date.now() } });
|
|
139
|
+
const result = await syncPrompt({ ...target, prompt, ...compiled }, { adopt: options.adopt === true });
|
|
140
|
+
const adapter = plan ? await installAdapter(target.agentDir, plan) : undefined;
|
|
141
|
+
const catalog = target.settings.suppressExplicitDelegationPrompt ? await refreshCatalog(target.agentDir) : undefined;
|
|
142
|
+
console.log(JSON.stringify({ ...result, adapter, skillCount: compiled?.bundle.skillCount, remotePrompt: remote?.source, catalog }, null, 2));
|
|
143
|
+
});
|
|
144
|
+
root.command("restore").description("Restore the original pointer; pause automatic management and retain files")
|
|
145
|
+
.action(async () => {
|
|
146
|
+
const target = await resolveTarget(config);
|
|
147
|
+
await restoreCatalog(target.agentDir);
|
|
148
|
+
await restoreAdapter(target.agentDir);
|
|
149
|
+
console.log(JSON.stringify(await restorePrompt({ ...target, prompt: "" }), null, 2));
|
|
150
|
+
});
|
|
151
|
+
}, { descriptors: [{ name: "codex-prompt", description: "Manage an agent-scoped Codex base prompt", hasSubcommands: true }] });
|
|
152
|
+
},
|
|
153
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type FrozenBundle = {
|
|
2
|
+
schema: 1;
|
|
3
|
+
policy: string;
|
|
4
|
+
sources: {
|
|
5
|
+
name: string;
|
|
6
|
+
sha256: string | null;
|
|
7
|
+
}[];
|
|
8
|
+
skillCount: number;
|
|
9
|
+
basePromptLength?: number;
|
|
10
|
+
remotePrompt?: {
|
|
11
|
+
url: string;
|
|
12
|
+
revision: string;
|
|
13
|
+
checkedAt?: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export declare const FROZEN_CONTEXT_HEADER = "\n\n## Frozen agent context\n";
|
|
17
|
+
export type PromptTarget = {
|
|
18
|
+
agentDir: string;
|
|
19
|
+
prompt: string;
|
|
20
|
+
bundle?: FrozenBundle;
|
|
21
|
+
};
|
|
22
|
+
export declare function readOptional(path: string): Promise<string | null>;
|
|
23
|
+
export declare function sha256(text: string): string;
|
|
24
|
+
/** Disk state only: this cannot attest to instructions retained by a live Codex thread. */
|
|
25
|
+
export declare function promptStatus(target: PromptTarget): Promise<{
|
|
26
|
+
enrolled: boolean;
|
|
27
|
+
restored: boolean;
|
|
28
|
+
configPath: string;
|
|
29
|
+
promptPath: string;
|
|
30
|
+
pointerMatches: boolean;
|
|
31
|
+
promptMatches: boolean;
|
|
32
|
+
expectedSha256: string;
|
|
33
|
+
actualSha256: string | null;
|
|
34
|
+
runtimeVerified: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
export declare function replaceFile(path: string, previous: string | null, contents: string): Promise<boolean>;
|
|
37
|
+
export declare function withLock<T>(target: PromptTarget, operation: () => Promise<T>): Promise<T>;
|
|
38
|
+
export declare function syncPrompt(target: PromptTarget, options?: {
|
|
39
|
+
adopt?: boolean;
|
|
40
|
+
automatic?: boolean;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
changed: boolean;
|
|
43
|
+
restored: boolean;
|
|
44
|
+
promptChanged?: never;
|
|
45
|
+
configChanged?: never;
|
|
46
|
+
sha256?: never;
|
|
47
|
+
activation?: never;
|
|
48
|
+
} | {
|
|
49
|
+
changed: boolean;
|
|
50
|
+
promptChanged: boolean;
|
|
51
|
+
configChanged: boolean;
|
|
52
|
+
sha256: string;
|
|
53
|
+
activation: string;
|
|
54
|
+
restored?: never;
|
|
55
|
+
}>;
|
|
56
|
+
export declare function restorePrompt(target: PromptTarget): Promise<{
|
|
57
|
+
changed: boolean;
|
|
58
|
+
restored: boolean;
|
|
59
|
+
retainedPromptPath: string;
|
|
60
|
+
}>;
|
|
61
|
+
/** Replace only the shared prefix; never reread workspace documents or skills. */
|
|
62
|
+
export declare function refreshSharedPrompt(agentDir: string, fetchBase: () => Promise<{
|
|
63
|
+
prompt: string;
|
|
64
|
+
source: {
|
|
65
|
+
url: string;
|
|
66
|
+
revision: string;
|
|
67
|
+
};
|
|
68
|
+
}>, now?: number): Promise<{
|
|
69
|
+
changed: boolean;
|
|
70
|
+
skipped: boolean;
|
|
71
|
+
revision?: never;
|
|
72
|
+
checkedAt?: never;
|
|
73
|
+
newSessionRequired?: never;
|
|
74
|
+
} | {
|
|
75
|
+
changed: boolean;
|
|
76
|
+
revision: string;
|
|
77
|
+
checkedAt: number;
|
|
78
|
+
newSessionRequired: boolean;
|
|
79
|
+
skipped?: never;
|
|
80
|
+
}>;
|