@unblocklabs/unblock-codex-prompt 0.1.0 → 0.2.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/README.md +32 -7
- package/RELEASING.md +6 -1
- package/dist/adapter.d.ts +2 -2
- package/dist/adapter.js +62 -23
- package/dist/compatibility.d.ts +2 -0
- package/dist/compatibility.js +9 -0
- package/dist/compiler.d.ts +0 -1
- package/dist/compiler.js +9 -8
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -2
- package/dist/index.js +28 -8
- package/dist/skills.d.ts +34 -0
- package/dist/skills.js +181 -0
- package/openclaw.plugin.json +6 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Unblock Codex Prompt
|
|
2
2
|
|
|
3
3
|
A small, explicit prompt-management bridge for dedicated OpenClaw Mac agents.
|
|
4
|
-
**Version 0.
|
|
4
|
+
**Version 0.2.0.** The frozen bridge supports OpenClaw/Codex 2026.9.2 and newer
|
|
5
|
+
stable releases with a compatible context-assembly layout.
|
|
5
6
|
|
|
6
7
|
## What you control
|
|
7
8
|
|
|
@@ -27,15 +28,18 @@ files use private permissions. Missing identity documents are recorded as absent
|
|
|
27
28
|
|
|
28
29
|
## Compatibility
|
|
29
30
|
|
|
30
|
-
- Frozen bridge: **OpenClaw
|
|
31
|
-
|
|
31
|
+
- Frozen bridge: **OpenClaw and @openclaw/codex >=2026.9.2**. Reviewed layouts cover
|
|
32
|
+
2026.9.2 and 2026.9.4, including the newer parent-local context path. Bundle names
|
|
33
|
+
may change; fingerprints of the context-assembly functions must still match.
|
|
34
|
+
Changed assembly is refused pending review, rather than patched blindly.
|
|
32
35
|
- Managed local stdio Codex, agent-scoped home, no custom command/arguments.
|
|
33
36
|
- One target agent per installation. Other agents use upstream behavior. Native
|
|
34
37
|
children may inherit the parent's frozen context.
|
|
35
38
|
|
|
36
39
|
This bridge uses two private upstream seams because public prompt hooks cannot
|
|
37
40
|
fix the dropped-context path: one skill-catalog builder and two entry points in
|
|
38
|
-
one Codex adapter bundle
|
|
41
|
+
one Codex adapter bundle (plus the parent-local context entry point on newer builds).
|
|
42
|
+
It validates private seams and is not a promise of compatibility with unseen layouts.
|
|
39
43
|
The catalog uses OpenClaw's eligibility, allowlist, visibility and size-limit
|
|
40
44
|
logic. Session-specific and execution-directory skills are not snapshotted.
|
|
41
45
|
Remote-node-only skills are not added; this is for dedicated local Mac agents.
|
|
@@ -46,7 +50,7 @@ Remote-node-only skills are not added; this is for dedicated local Mac agents.
|
|
|
46
50
|
npm ci
|
|
47
51
|
npm run preflight
|
|
48
52
|
npm pack
|
|
49
|
-
openclaw plugins install ./unblocklabs-unblock-codex-prompt-0.
|
|
53
|
+
openclaw plugins install ./unblocklabs-unblock-codex-prompt-0.2.0.tgz --force --accept-capabilities
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
Include the plugin in `plugins.allow` if applicable, then configure:
|
|
@@ -181,6 +185,26 @@ contents without a Gateway restart. Content-only updates report
|
|
|
181
185
|
are not reset, and their adoption is not guaranteed. The plugin never restarts
|
|
182
186
|
the Gateway automatically; revalidate this behavior when upgrading Codex.
|
|
183
187
|
|
|
188
|
+
## Codex skills removal
|
|
189
|
+
|
|
190
|
+
Codex lists every skill it finds in the managed Codex home, including skills from
|
|
191
|
+
ChatGPT remote plugins that sync in on their own. `disabledCodexSkills` names the
|
|
192
|
+
ones to remove: a plugin name removes all of that plugin's skills, and an exact
|
|
193
|
+
skill name (`pages:write-page`, `imagegen`) removes one. It defaults to
|
|
194
|
+
`["pages", "work-pets"]` (ChatGPT Pages and Pets). Other skills are untouched.
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
"disabledCodexSkills": ["pages", "work-pets"]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The plugin scans `skills/` and `plugins/cache/` under the managed Codex home and
|
|
201
|
+
writes one marked block of `[[skills.config]] name = "…" enabled = false` rules at
|
|
202
|
+
the end of `config.toml`. It re-applies the block at Gateway startup, `sync`,
|
|
203
|
+
`refresh`, and the daily refresh, so newly synced plugin skills are caught.
|
|
204
|
+
Codex reads the rules when its app-server starts; running sessions keep their
|
|
205
|
+
list until the Gateway or app-server restarts. Set `[]` to remove the block.
|
|
206
|
+
`restore` also removes it. User-authored `[[skills.config]]` entries stay as written.
|
|
207
|
+
|
|
184
208
|
## Restore
|
|
185
209
|
|
|
186
210
|
```sh
|
|
@@ -212,7 +236,7 @@ without restoring leaves the frozen base but re-enables live injections.
|
|
|
212
236
|
|
|
213
237
|
```sh
|
|
214
238
|
npm run preflight
|
|
215
|
-
npm run release:check -- v0.
|
|
239
|
+
npm run release:check -- v0.2.0
|
|
216
240
|
```
|
|
217
241
|
|
|
218
242
|
Tests cover compilation, explicit refresh, native/private-body exclusion,
|
|
@@ -221,7 +245,8 @@ pointer ownership and foreign edits. See [VALIDATION.md](VALIDATION.md) for live
|
|
|
221
245
|
proof and limits.
|
|
222
246
|
|
|
223
247
|
Install with
|
|
224
|
-
|
|
248
|
+
the packed archive above for an unreleased candidate. After publication, use
|
|
249
|
+
`openclaw plugins install npm:@unblocklabs/unblock-codex-prompt@0.2.0 --accept-capabilities`.
|
|
225
250
|
The existing [GitHub Release workflow](RELEASING.md) publishes npm only when a
|
|
226
251
|
GitHub Release is published. Normal pushes do not release the package.
|
|
227
252
|
|
package/RELEASING.md
CHANGED
|
@@ -25,7 +25,7 @@ If first-package bootstrapping is needed, an authenticated owner can publish the
|
|
|
25
25
|
reviewed version once with `npm publish --access public`, then configure trust:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
npm trust github @unblocklabs/unblock-codex-prompt --repo unblocklabs-ai/unblock-codex-prompt --file release.yml --yes
|
|
28
|
+
npx --yes --package npm@12.0.2 npm trust github @unblocklabs/unblock-codex-prompt --repo unblocklabs-ai/unblock-codex-prompt --file release.yml --allow-publish --yes
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
The Release workflow verifies the registry's tarball integrity against its own
|
|
@@ -33,6 +33,11 @@ packed artifact before accepting an already-published version. Different bytes
|
|
|
33
33
|
fail the job. Subsequent new versions publish through OIDC with provenance; a
|
|
34
34
|
local bootstrap publication does not have GitHub Actions provenance.
|
|
35
35
|
|
|
36
|
+
Trusted publishing was configured during the 0.1.0 release. The trust-management
|
|
37
|
+
command above uses npm 12 because the current registry requires explicit publish
|
|
38
|
+
permissions; npm 11.13.0's older trust request was rejected. Normal CI publishing
|
|
39
|
+
continues to use the pinned CLI in `release.yml`.
|
|
40
|
+
|
|
36
41
|
ClawHub publishing is not configured.
|
|
37
42
|
|
|
38
43
|
## Release checklist
|
package/dist/adapter.d.ts
CHANGED
|
@@ -27,11 +27,11 @@ export declare function installAdapter(agentDir: string, plan: Awaited<ReturnTyp
|
|
|
27
27
|
export declare function adapterStatus(agentDir: string): Promise<{
|
|
28
28
|
installed: boolean;
|
|
29
29
|
path?: never;
|
|
30
|
-
|
|
30
|
+
minimumVersion?: never;
|
|
31
31
|
} | {
|
|
32
32
|
installed: boolean;
|
|
33
33
|
path: string;
|
|
34
|
-
|
|
34
|
+
minimumVersion: string;
|
|
35
35
|
}>;
|
|
36
36
|
export declare function restoreAdapter(agentDir: string): Promise<void>;
|
|
37
37
|
export {};
|
package/dist/adapter.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse } from "acorn";
|
|
4
5
|
import { readOptional, replaceFile, sha256 } from "./manager.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import { openclawRoot } from "./compiler.js";
|
|
7
|
+
import { assertSupportedVersion, MINIMUM_OPENCLAW } from "./compatibility.js";
|
|
7
8
|
const MARKER = "// unblock-codex-prompt bridge v1";
|
|
8
|
-
const
|
|
9
|
-
const
|
|
9
|
+
const BUNDLE_NAME = /^thread-lifecycle-[\w-]+\.m?js$/u;
|
|
10
|
+
const HASH = /^[a-f0-9]{64}$/u;
|
|
11
|
+
// Fingerprint only the context-assembly seams, not release-specific bundle names
|
|
12
|
+
// or unrelated code. New releases can use either reviewed layout unchanged.
|
|
13
|
+
const CONTEXT_HASHES = {
|
|
14
|
+
legacy: "1fb45c46c50a63f68e8b6bdfbc5d7ae2d323470c00db69779d159592868940bb",
|
|
15
|
+
parent: "5ee80edf3f3887e841def5d64896a3d27c35063f6d4803dceeb8b8bf07b654f0",
|
|
16
|
+
collaboration: "aea7e95ca43f0997b0a3f616f327132bfdd057bb731e6eead132fba21de3f981",
|
|
17
|
+
};
|
|
10
18
|
function receiptPath(agentDir) { return join(agentDir, "codex-home", ".unblock-codex-prompt-adapter.json"); }
|
|
11
19
|
function backupPath(agentDir) { return join(agentDir, "codex-home", ".unblock-codex-prompt-adapter-original.js"); }
|
|
12
20
|
export async function readReceipt(agentDir) {
|
|
@@ -17,15 +25,39 @@ export async function readReceipt(agentDir) {
|
|
|
17
25
|
if (!value || typeof value !== "object" || !("schema" in value) || value.schema !== 1 ||
|
|
18
26
|
!("packageDir" in value) || typeof value.packageDir !== "string" ||
|
|
19
27
|
!("path" in value) || typeof value.path !== "string" ||
|
|
20
|
-
!(
|
|
21
|
-
!("
|
|
22
|
-
value
|
|
28
|
+
!isAbsolute(value.packageDir) || !BUNDLE_NAME.test(basename(value.path)) ||
|
|
29
|
+
!("originalSha256" in value) || typeof value.originalSha256 !== "string" || !HASH.test(value.originalSha256) ||
|
|
30
|
+
!("patchedSha256" in value) || typeof value.patchedSha256 !== "string" || !HASH.test(value.patchedSha256) ||
|
|
31
|
+
value.path !== join(value.packageDir, "dist", basename(value.path)))
|
|
23
32
|
throw new Error("Invalid adapter receipt");
|
|
24
33
|
return { schema: 1, packageDir: value.packageDir, path: value.path, originalSha256: value.originalSha256, patchedSha256: value.patchedSha256 };
|
|
25
34
|
}
|
|
26
35
|
export function patchAdapterSource(source, agentDir, agentId, runtimePath) {
|
|
27
|
-
if (source.includes(MARKER)
|
|
28
|
-
throw new Error("Unsupported Codex adapter shape");
|
|
36
|
+
if (source.includes(MARKER))
|
|
37
|
+
throw new Error("Unsupported Codex adapter shape: already patched");
|
|
38
|
+
const functions = parse(source, { ecmaVersion: "latest", sourceType: "module" }).body.filter(node => node.type === "FunctionDeclaration");
|
|
39
|
+
const find = (name) => {
|
|
40
|
+
const matches = functions.filter(node => node.id?.name === name);
|
|
41
|
+
if (matches.length !== 1)
|
|
42
|
+
throw new Error(`Unsupported Codex adapter shape: ${name}`);
|
|
43
|
+
return matches[0];
|
|
44
|
+
};
|
|
45
|
+
const developer = find("buildDeveloperInstructions");
|
|
46
|
+
const collaboration = find("buildTurnScopedCollaborationInstructions");
|
|
47
|
+
const parent = functions.some(node => node.id?.name === "buildCodexParentLocalInstructions") ? find("buildCodexParentLocalInstructions") : null;
|
|
48
|
+
for (const fn of [developer, collaboration, ...(parent ? [parent] : [])]) {
|
|
49
|
+
if (fn.async || fn.generator || fn.params.length !== 2 || fn.params[0]?.type !== "Identifier" || fn.params[0].name !== "params" ||
|
|
50
|
+
!/^(options|options\s*=\s*\{\})$/u.test(source.slice(fn.params[1].start, fn.params[1].end)))
|
|
51
|
+
throw new Error("Unsupported Codex adapter shape: parameters");
|
|
52
|
+
}
|
|
53
|
+
for (const name of ["buildDefaultCollaborationInstructions", "buildCronCollaborationInstructions"]) {
|
|
54
|
+
if (find(name).params.length !== 0)
|
|
55
|
+
throw new Error("Unsupported Codex adapter shape: mode helpers");
|
|
56
|
+
}
|
|
57
|
+
const fingerprint = (fn) => sha256(source.slice(fn.start, fn.end));
|
|
58
|
+
if (fingerprint(collaboration) !== (parent ? CONTEXT_HASHES.collaboration : CONTEXT_HASHES.legacy) ||
|
|
59
|
+
(parent && fingerprint(parent) !== CONTEXT_HASHES.parent))
|
|
60
|
+
throw new Error("Unsupported Codex context assembly; review the changed upstream functions before syncing");
|
|
29
61
|
const prefix = `${MARKER}
|
|
30
62
|
import { createRequire as unblockPromptCreateRequire } from "node:module";
|
|
31
63
|
const unblockPromptRequire = unblockPromptCreateRequire(import.meta.url);
|
|
@@ -40,11 +72,15 @@ function unblockFrozenPolicy(params) {
|
|
|
40
72
|
return unblockPromptRequire(${JSON.stringify(runtimePath)}).readFrozenPolicy(${JSON.stringify(agentDir)});
|
|
41
73
|
}
|
|
42
74
|
`;
|
|
43
|
-
|
|
44
|
-
\tconst unblockPolicy = unblockFrozenPolicy(params);
|
|
45
|
-
\tif (
|
|
46
|
-
.
|
|
47
|
-
|
|
75
|
+
const insertions = [
|
|
76
|
+
{ at: developer.body.start + 1, text: '\n\tconst unblockPolicy = unblockFrozenPolicy(params);\n\tif (unblockPolicy !== null) return [unblockPolicy, params.gitCoauthorPrompt, params.extraSystemPrompt].filter(section => typeof section === "string" && section.trim()).join("\\n\\n");' },
|
|
77
|
+
{ at: collaboration.body.start + 1, text: '\n\tif (unblockFrozenPolicy(params) !== null) return params.trigger === "cron" ? buildCronCollaborationInstructions() : buildDefaultCollaborationInstructions();' },
|
|
78
|
+
...(parent ? [{ at: parent.body.start + 1, text: '\n\tif (unblockFrozenPolicy(params) !== null) return params.trigger === "cron" ? buildCronCollaborationInstructions() : null;' }] : []),
|
|
79
|
+
];
|
|
80
|
+
let patched = source;
|
|
81
|
+
for (const { at, text } of insertions.sort((a, b) => b.at - a.at))
|
|
82
|
+
patched = patched.slice(0, at) + text + patched.slice(at);
|
|
83
|
+
return prefix + patched;
|
|
48
84
|
}
|
|
49
85
|
export async function prepareAdapter(agentDir, agentId, suppliedDir) {
|
|
50
86
|
await openclawRoot();
|
|
@@ -55,22 +91,25 @@ export async function prepareAdapter(agentDir, agentId, suppliedDir) {
|
|
|
55
91
|
if (receipt && receipt.packageDir !== packageDir)
|
|
56
92
|
throw new Error("Restore the previous adapter before changing Codex installations");
|
|
57
93
|
const pkg = JSON.parse(await readFile(join(packageDir, "package.json"), "utf8"));
|
|
58
|
-
if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== "@openclaw/codex"
|
|
59
|
-
throw new Error(
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== "@openclaw/codex")
|
|
95
|
+
throw new Error("Expected the official @openclaw/codex package");
|
|
96
|
+
assertSupportedVersion("version" in pkg ? pkg.version : undefined, "@openclaw/codex");
|
|
97
|
+
const files = (await readdir(join(packageDir, "dist"))).filter(name => BUNDLE_NAME.test(name));
|
|
98
|
+
if (files.length !== 1)
|
|
62
99
|
throw new Error("Unsupported Codex bundle layout");
|
|
63
100
|
const path = join(packageDir, "dist", files[0]);
|
|
64
101
|
const current = await readOptional(path);
|
|
65
102
|
if (current === null)
|
|
66
103
|
throw new Error("Missing Codex adapter");
|
|
67
104
|
const original = receipt ? await readOptional(backupPath(agentDir)) : current;
|
|
68
|
-
if (
|
|
69
|
-
throw new Error("
|
|
105
|
+
if (receipt && receipt.path !== path)
|
|
106
|
+
throw new Error("Codex adapter path changed; restore before changing installations");
|
|
107
|
+
if (original === null || (receipt && sha256(original) !== receipt.originalSha256))
|
|
108
|
+
throw new Error("Codex adapter backup invalid; refusing to patch");
|
|
70
109
|
if (receipt && sha256(current) !== receipt.patchedSha256 && current !== original)
|
|
71
110
|
throw new Error("Codex adapter changed by another writer; refusing to overwrite");
|
|
72
111
|
const patched = patchAdapterSource(original, agentDir, agentId, fileURLToPath(new URL("./runtime.js", import.meta.url)));
|
|
73
|
-
return { path, current, original, patched, receipt: { schema: 1, packageDir, path, originalSha256:
|
|
112
|
+
return { path, current, original, patched, receipt: { schema: 1, packageDir, path, originalSha256: sha256(original), patchedSha256: sha256(patched) } };
|
|
74
113
|
}
|
|
75
114
|
export async function installAdapter(agentDir, plan) {
|
|
76
115
|
const backup = await readOptional(backupPath(agentDir));
|
|
@@ -86,7 +125,7 @@ export async function adapterStatus(agentDir) {
|
|
|
86
125
|
if (!receipt)
|
|
87
126
|
return { installed: false };
|
|
88
127
|
const current = await readOptional(receipt.path);
|
|
89
|
-
return { installed: current !== null && sha256(current) === receipt.patchedSha256, path: receipt.path,
|
|
128
|
+
return { installed: current !== null && sha256(current) === receipt.patchedSha256, path: receipt.path, minimumVersion: MINIMUM_OPENCLAW };
|
|
90
129
|
}
|
|
91
130
|
export async function restoreAdapter(agentDir) {
|
|
92
131
|
const receipt = await readReceipt(agentDir);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const MINIMUM_OPENCLAW = "2026.9.2";
|
|
2
|
+
export function assertSupportedVersion(version, product) {
|
|
3
|
+
const match = typeof version === "string" ? /^(\d+)\.(\d+)\.(\d+)(?:-\d+)?$/u.exec(version) : null;
|
|
4
|
+
const supported = match && (Number(match[1]) > 2026 ||
|
|
5
|
+
(Number(match[1]) === 2026 && (Number(match[2]) > 9 ||
|
|
6
|
+
(Number(match[2]) === 9 && Number(match[3]) >= 2))));
|
|
7
|
+
if (!supported)
|
|
8
|
+
throw new Error(`Frozen bridge requires ${product} >=${MINIMUM_OPENCLAW} (stable release)`);
|
|
9
|
+
}
|
package/dist/compiler.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
-
export declare const SUPPORTED_OPENCLAW = "2026.9.2";
|
|
3
2
|
export declare function openclawRoot(): Promise<string>;
|
|
4
3
|
export declare function buildSkillsCatalog(config: OpenClawConfig, agentId: string, workspaceDir: string): Promise<{
|
|
5
4
|
prompt: string;
|
package/dist/compiler.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile, readdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { readOptional, sha256 } from "./manager.js";
|
|
5
|
-
|
|
5
|
+
import { assertSupportedVersion } from "./compatibility.js";
|
|
6
6
|
export async function openclawRoot() {
|
|
7
7
|
let directory = dirname(fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/agent-runtime")));
|
|
8
8
|
for (;;) {
|
|
@@ -10,8 +10,7 @@ export async function openclawRoot() {
|
|
|
10
10
|
if (source) {
|
|
11
11
|
const pkg = JSON.parse(source);
|
|
12
12
|
if (pkg && typeof pkg === "object" && "name" in pkg && pkg.name === "openclaw") {
|
|
13
|
-
|
|
14
|
-
throw new Error(`Frozen bridge supports OpenClaw ${SUPPORTED_OPENCLAW} only; review upgrades before syncing`);
|
|
13
|
+
assertSupportedVersion("version" in pkg ? pkg.version : undefined, "OpenClaw");
|
|
15
14
|
return directory;
|
|
16
15
|
}
|
|
17
16
|
}
|
|
@@ -21,21 +20,23 @@ export async function openclawRoot() {
|
|
|
21
20
|
directory = parent;
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
|
-
// OpenClaw has no public catalog-building SDK.
|
|
23
|
+
// OpenClaw has no public catalog-building SDK. Validate this internal export,
|
|
25
24
|
// retaining its eligibility, agent allowlist, prompt visibility and size limits.
|
|
26
25
|
export async function buildSkillsCatalog(config, agentId, workspaceDir) {
|
|
27
26
|
const dist = join(await openclawRoot(), "dist");
|
|
28
|
-
const names = (await readdir(dist)).filter(name => /^workspace-skill-prompt-[\w-]+\.js$/u.test(name));
|
|
27
|
+
const names = (await readdir(dist)).filter(name => /^workspace-skill-prompt-[\w-]+\.m?js$/u.test(name));
|
|
29
28
|
if (names.length !== 1)
|
|
30
29
|
throw new Error("Unsupported OpenClaw skills module layout");
|
|
31
30
|
const path = join(dist, names[0]);
|
|
32
31
|
const source = await readFile(path, "utf8");
|
|
33
|
-
|
|
32
|
+
const exports = [...source.matchAll(/\bbuildSkillSnapshot as ([\w$]+)\b/gu)];
|
|
33
|
+
if (exports.length !== 1)
|
|
34
34
|
throw new Error("Unsupported OpenClaw skills export");
|
|
35
35
|
const module = await import(pathToFileURL(path).href);
|
|
36
|
-
|
|
36
|
+
const buildSnapshot = module[exports[0][1]];
|
|
37
|
+
if (typeof buildSnapshot !== "function")
|
|
37
38
|
throw new Error("Missing OpenClaw skill snapshot builder");
|
|
38
|
-
const result =
|
|
39
|
+
const result = await buildSnapshot(workspaceDir, { config, agentId });
|
|
39
40
|
if (!result || typeof result !== "object" || !("prompt" in result) || typeof result.prompt !== "string" ||
|
|
40
41
|
!("resolvedSkills" in result) || !Array.isArray(result.resolvedSkills))
|
|
41
42
|
throw new Error("Unsupported OpenClaw skill snapshot result");
|
package/dist/config.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export declare const PLUGIN_ID = "unblock-codex-prompt";
|
|
2
|
+
export declare const DEFAULT_DISABLED_CODEX_SKILLS: string[];
|
|
2
3
|
export declare function parseConfig(raw: unknown): {
|
|
3
4
|
agentId: string;
|
|
4
5
|
agentName: string | undefined;
|
|
5
6
|
frozenContext: boolean;
|
|
6
7
|
promptUrl: string | undefined;
|
|
7
8
|
suppressExplicitDelegationPrompt: boolean;
|
|
9
|
+
disabledCodexSkills: string[];
|
|
8
10
|
};
|
|
9
11
|
export declare function validatePromptUrl(value: string): URL;
|
|
10
12
|
export declare function renderPrompt(template: string, agentName?: string): string;
|
package/dist/config.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export const PLUGIN_ID = "unblock-codex-prompt";
|
|
2
|
+
// ChatGPT Pages and Pets skills reach Codex through remote plugins and do not apply to OpenClaw agents.
|
|
3
|
+
export const DEFAULT_DISABLED_CODEX_SKILLS = ["pages", "work-pets"];
|
|
2
4
|
export function parseConfig(raw) {
|
|
3
5
|
const value = raw ?? {};
|
|
4
6
|
if (typeof value !== "object" || Array.isArray(value))
|
|
5
7
|
throw new Error("Plugin config must be an object");
|
|
6
8
|
for (const key of Object.keys(value)) {
|
|
7
|
-
if (key !== "agentId" && key !== "agentName" && key !== "frozenContext" && key !== "promptUrl" && key !== "suppressExplicitDelegationPrompt")
|
|
9
|
+
if (key !== "agentId" && key !== "agentName" && key !== "frozenContext" && key !== "promptUrl" && key !== "suppressExplicitDelegationPrompt" && key !== "disabledCodexSkills")
|
|
8
10
|
throw new Error(`Unknown plugin option: ${key}`);
|
|
9
11
|
}
|
|
10
12
|
const agentId = "agentId" in value ? value.agentId : "main";
|
|
@@ -32,7 +34,12 @@ export function parseConfig(raw) {
|
|
|
32
34
|
throw new Error("suppressExplicitDelegationPrompt must be boolean");
|
|
33
35
|
if (suppressExplicitDelegationPrompt && !frozenContext)
|
|
34
36
|
throw new Error("suppressExplicitDelegationPrompt requires frozenContext");
|
|
35
|
-
|
|
37
|
+
const disabledCodexSkills = "disabledCodexSkills" in value ? value.disabledCodexSkills : DEFAULT_DISABLED_CODEX_SKILLS;
|
|
38
|
+
if (!Array.isArray(disabledCodexSkills) || disabledCodexSkills.length > 64 ||
|
|
39
|
+
disabledCodexSkills.some(entry => typeof entry !== "string" || !/^[\w.@:-]{1,128}$/u.test(entry)) || new Set(disabledCodexSkills).size !== disabledCodexSkills.length) {
|
|
40
|
+
throw new Error("disabledCodexSkills must be a list of up to 64 unique skill or plugin names");
|
|
41
|
+
}
|
|
42
|
+
return { agentId, agentName: agentName?.trim(), frozenContext, promptUrl, suppressExplicitDelegationPrompt, disabledCodexSkills: disabledCodexSkills };
|
|
36
43
|
}
|
|
37
44
|
export function validatePromptUrl(value) {
|
|
38
45
|
const url = new URL(value);
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,13 @@ import { fetchPrompt, REMOTE_POLICY } from "./remote.js";
|
|
|
10
10
|
import { readOptional } from "./manager.js";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { CATALOG_REFRESH_MS, catalogStatus, refreshCatalog, restoreCatalog, startCatalogRefresh } from "./catalog.js";
|
|
13
|
+
import { codexSkillsStatus, refreshCodexSkills, removeCodexSkills } from "./skills.js";
|
|
13
14
|
async function refresh(target, automatic = false) {
|
|
14
15
|
const failures = [];
|
|
15
16
|
let catalog;
|
|
16
17
|
let prompt;
|
|
17
|
-
|
|
18
|
+
let skills;
|
|
19
|
+
// Attempt each independently, sequentially because they share the enrollment lock.
|
|
18
20
|
if (target.settings.suppressExplicitDelegationPrompt) {
|
|
19
21
|
try {
|
|
20
22
|
catalog = await refreshCatalog(target.agentDir, { automatic });
|
|
@@ -32,9 +34,15 @@ async function refresh(target, automatic = false) {
|
|
|
32
34
|
failures.push(error);
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
try {
|
|
38
|
+
skills = await refreshCodexSkills(target.agentDir, target.settings.disabledCodexSkills);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
failures.push(error);
|
|
42
|
+
}
|
|
35
43
|
if (failures.length)
|
|
36
44
|
throw new AggregateError(failures, "Refresh incomplete; failed components retain their installed data");
|
|
37
|
-
return { catalog, prompt };
|
|
45
|
+
return { catalog, prompt, skills };
|
|
38
46
|
}
|
|
39
47
|
async function resolveTarget(config) {
|
|
40
48
|
const settings = parseConfig(config.plugins?.entries?.[PLUGIN_ID]?.config);
|
|
@@ -83,7 +91,7 @@ async function status(config) {
|
|
|
83
91
|
export default definePluginEntry({
|
|
84
92
|
id: PLUGIN_ID,
|
|
85
93
|
name: "Unblock Codex Prompt",
|
|
86
|
-
description: "Explicitly compile a frozen Codex prompt with a
|
|
94
|
+
description: "Explicitly compile a frozen Codex prompt with a guarded OpenClaw bridge.",
|
|
87
95
|
register(api) {
|
|
88
96
|
let stopCatalogRefresh;
|
|
89
97
|
// Registration/import stays read-only, including plugin inspection and CLI discovery.
|
|
@@ -94,11 +102,20 @@ export default definePluginEntry({
|
|
|
94
102
|
const result = await status(ctx.config);
|
|
95
103
|
ctx.logger.info(`${PLUGIN_ID}: ${JSON.stringify(result)}`);
|
|
96
104
|
const target = await resolveTarget(ctx.config);
|
|
97
|
-
if (
|
|
105
|
+
if (result.enrolled) {
|
|
106
|
+
// Remote Codex plugins can add skills at any time; reconcile before app-servers start.
|
|
107
|
+
try {
|
|
108
|
+
ctx.logger.info(`${PLUGIN_ID}: codex skills ${JSON.stringify(await refreshCodexSkills(target.agentDir, target.settings.disabledCodexSkills))}`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
ctx.logger.error(`${PLUGIN_ID}: codex skills not applied: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (result.enrolled) {
|
|
98
115
|
const catalog = await catalogStatus(target.agentDir);
|
|
99
116
|
const remote = "remotePrompt" in result ? result.remotePrompt : null;
|
|
100
117
|
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);
|
|
118
|
+
const nextAt = Math.min(target.settings.suppressExplicitDelegationPrompt ? catalog.nextRefreshAt ?? 0 : Infinity, target.settings.promptUrl ? checkedAt ? checkedAt + CATALOG_REFRESH_MS : 0 : Infinity, Date.now() + CATALOG_REFRESH_MS);
|
|
102
119
|
stopCatalogRefresh = startCatalogRefresh(async () => {
|
|
103
120
|
const refreshed = await refresh(target, true);
|
|
104
121
|
ctx.logger.info(`${PLUGIN_ID}: daily refresh ${JSON.stringify(refreshed)}`);
|
|
@@ -113,7 +130,8 @@ export default definePluginEntry({
|
|
|
113
130
|
root.command("status").description("Inspect disk state without writing or claiming live adoption")
|
|
114
131
|
.action(async () => {
|
|
115
132
|
const target = await resolveTarget(config);
|
|
116
|
-
console.log(JSON.stringify({ ...await status(config), catalog: await catalogStatus(target.agentDir)
|
|
133
|
+
console.log(JSON.stringify({ ...await status(config), catalog: await catalogStatus(target.agentDir),
|
|
134
|
+
codexSkills: await codexSkillsStatus(target.agentDir, target.settings.disabledCodexSkills) }, null, 2));
|
|
117
135
|
});
|
|
118
136
|
root.command("refresh-catalog").description("Fetch fresh provider metadata and repair the managed delegation-policy catalog")
|
|
119
137
|
.action(async () => {
|
|
@@ -122,7 +140,7 @@ export default definePluginEntry({
|
|
|
122
140
|
throw new Error("Enable suppressExplicitDelegationPrompt before refreshing the catalog");
|
|
123
141
|
console.log(JSON.stringify(await refreshCatalog(target.agentDir), null, 2));
|
|
124
142
|
});
|
|
125
|
-
root.command("refresh").description("Refresh remote prompt
|
|
143
|
+
root.command("refresh").description("Refresh remote prompt, enabled catalog and codex skills without rereading frozen agent context")
|
|
126
144
|
.action(async () => console.log(JSON.stringify(await refresh(await resolveTarget(config)), null, 2)));
|
|
127
145
|
const sync = root.command("sync").description("Explicitly refresh the prompt snapshot and install the reviewed adapter")
|
|
128
146
|
.option("--adopt", "Take ownership of an existing custom prompt pointer")
|
|
@@ -139,12 +157,14 @@ export default definePluginEntry({
|
|
|
139
157
|
const result = await syncPrompt({ ...target, prompt, ...compiled }, { adopt: options.adopt === true });
|
|
140
158
|
const adapter = plan ? await installAdapter(target.agentDir, plan) : undefined;
|
|
141
159
|
const catalog = target.settings.suppressExplicitDelegationPrompt ? await refreshCatalog(target.agentDir) : undefined;
|
|
142
|
-
|
|
160
|
+
const codexSkills = await refreshCodexSkills(target.agentDir, target.settings.disabledCodexSkills);
|
|
161
|
+
console.log(JSON.stringify({ ...result, adapter, skillCount: compiled?.bundle.skillCount, remotePrompt: remote?.source, catalog, codexSkills }, null, 2));
|
|
143
162
|
});
|
|
144
163
|
root.command("restore").description("Restore the original pointer; pause automatic management and retain files")
|
|
145
164
|
.action(async () => {
|
|
146
165
|
const target = await resolveTarget(config);
|
|
147
166
|
await restoreCatalog(target.agentDir);
|
|
167
|
+
await removeCodexSkills(target.agentDir);
|
|
148
168
|
await restoreAdapter(target.agentDir);
|
|
149
169
|
console.log(JSON.stringify(await restorePrompt({ ...target, prompt: "" }), null, 2));
|
|
150
170
|
});
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type CodexSkill = {
|
|
2
|
+
name: string;
|
|
3
|
+
plugin: string | null;
|
|
4
|
+
};
|
|
5
|
+
/** Skills Codex loads from this agent's home: system, user and cached plugin skills (`plugin:skill`). */
|
|
6
|
+
export declare function discoverCodexSkills(agentDir: string): Promise<CodexSkill[]>;
|
|
7
|
+
/** A list entry names a skill exactly, or a plugin to disable all of its skills. */
|
|
8
|
+
export declare function isListed(skill: CodexSkill, list: readonly string[]): boolean;
|
|
9
|
+
/** `null` when nothing discovered is listed, so the config carries no empty block. */
|
|
10
|
+
export declare function renderSkillsBlock(skills: readonly CodexSkill[], disable: readonly string[]): string | null;
|
|
11
|
+
/** Replace, append or (with `null`) remove the managed block, always leaving it last in the file. */
|
|
12
|
+
export declare function setSkillsBlock(source: string, block: string | null): string;
|
|
13
|
+
/** Disable the listed skills in this agent's Codex home. New plugin skills are caught on the next refresh. */
|
|
14
|
+
export declare function refreshCodexSkills(agentDir: string, disable: readonly string[]): Promise<{
|
|
15
|
+
skipped: boolean;
|
|
16
|
+
changed: boolean;
|
|
17
|
+
restartRequired?: never;
|
|
18
|
+
disabled?: never;
|
|
19
|
+
notFound?: never;
|
|
20
|
+
} | {
|
|
21
|
+
changed: boolean;
|
|
22
|
+
restartRequired: boolean;
|
|
23
|
+
disabled: string[];
|
|
24
|
+
notFound: string[];
|
|
25
|
+
skipped?: never;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function removeCodexSkills(agentDir: string): Promise<{
|
|
28
|
+
changed: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
export declare function codexSkillsStatus(agentDir: string, disable: readonly string[]): Promise<{
|
|
31
|
+
managed: boolean;
|
|
32
|
+
configured: readonly string[];
|
|
33
|
+
disabled: string[];
|
|
34
|
+
}>;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { getStaticTOMLValue, parseTOML } from "toml-eslint-parser";
|
|
4
|
+
import { readOptional, replaceFile, withLock } from "./manager.js";
|
|
5
|
+
// Codex loads every discovered skill unless a [[skills.config]] rule turns it off, and remote
|
|
6
|
+
// plugins can add skills at any time. This block disables the listed skills and plugins by name,
|
|
7
|
+
// the selector Codex applies to plugin skills (`plugin:skill`) as well as system skills.
|
|
8
|
+
const BEGIN = "# BEGIN unblock-codex-prompt codex skills (managed; replaced on refresh)";
|
|
9
|
+
const END = "# END unblock-codex-prompt codex skills";
|
|
10
|
+
function files(agentDir) {
|
|
11
|
+
const home = join(resolve(agentDir), "codex-home");
|
|
12
|
+
return { home, config: join(home, "config.toml"), state: join(home, ".unblock-codex-prompt.json") };
|
|
13
|
+
}
|
|
14
|
+
function isMissing(error) {
|
|
15
|
+
return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
16
|
+
}
|
|
17
|
+
async function directories(path) {
|
|
18
|
+
try {
|
|
19
|
+
return (await readdir(path, { withFileTypes: true })).filter(entry => entry.isDirectory() && !entry.name.startsWith("."));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (isMissing(error))
|
|
23
|
+
return [];
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readText(path) {
|
|
28
|
+
try {
|
|
29
|
+
return await readFile(path, "utf8");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (isMissing(error))
|
|
33
|
+
return null;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Codex names a skill by its SKILL.md frontmatter `name`, falling back to the directory name. */
|
|
38
|
+
function skillName(doc, directory) {
|
|
39
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/u.exec(doc)?.[1] ?? "";
|
|
40
|
+
return /^name:\s*["']?(.+?)["']?\s*$/mu.exec(frontmatter)?.[1]?.trim() || directory;
|
|
41
|
+
}
|
|
42
|
+
async function skillsUnder(root, depth) {
|
|
43
|
+
const names = [];
|
|
44
|
+
for (const entry of await directories(root)) {
|
|
45
|
+
const directory = join(root, entry.name);
|
|
46
|
+
const doc = await readText(join(directory, "SKILL.md"));
|
|
47
|
+
if (doc !== null)
|
|
48
|
+
names.push(skillName(doc, entry.name));
|
|
49
|
+
else if (depth > 1)
|
|
50
|
+
names.push(...await skillsUnder(directory, depth - 1));
|
|
51
|
+
}
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
54
|
+
/** Skills Codex loads from this agent's home: system, user and cached plugin skills (`plugin:skill`). */
|
|
55
|
+
export async function discoverCodexSkills(agentDir) {
|
|
56
|
+
const { home } = files(agentDir);
|
|
57
|
+
const found = [];
|
|
58
|
+
for (const name of await skillsUnder(join(home, "skills", ".system"), 1))
|
|
59
|
+
found.push({ name, plugin: null });
|
|
60
|
+
for (const name of await skillsUnder(join(home, "skills"), 1))
|
|
61
|
+
found.push({ name, plugin: null });
|
|
62
|
+
// Cache layout: plugins/cache/<marketplace>/<plugin>/<version>/.codex-plugin/plugin.json
|
|
63
|
+
const cache = join(home, "plugins", "cache");
|
|
64
|
+
for (const marketplace of await directories(cache)) {
|
|
65
|
+
for (const plugin of await directories(join(cache, marketplace.name))) {
|
|
66
|
+
for (const version of await directories(join(cache, marketplace.name, plugin.name))) {
|
|
67
|
+
const root = join(cache, marketplace.name, plugin.name, version.name);
|
|
68
|
+
const manifestText = await readText(join(root, ".codex-plugin", "plugin.json"));
|
|
69
|
+
if (manifestText === null)
|
|
70
|
+
continue;
|
|
71
|
+
let manifest;
|
|
72
|
+
try {
|
|
73
|
+
manifest = JSON.parse(manifestText);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const record = manifest && typeof manifest === "object" ? manifest : {};
|
|
79
|
+
const namespace = typeof record.name === "string" && record.name ? record.name : plugin.name;
|
|
80
|
+
const roots = typeof record.skills === "string" ? [record.skills]
|
|
81
|
+
: Array.isArray(record.skills) ? record.skills.filter((path) => typeof path === "string") : ["./skills/"];
|
|
82
|
+
for (const skillsRoot of roots) {
|
|
83
|
+
for (const name of await skillsUnder(resolve(root, skillsRoot), 3))
|
|
84
|
+
found.push({ name: `${namespace}:${name}`, plugin: namespace });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const unique = new Map(found.map(skill => [skill.name, skill]));
|
|
90
|
+
return [...unique.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
91
|
+
}
|
|
92
|
+
/** A list entry names a skill exactly, or a plugin to disable all of its skills. */
|
|
93
|
+
export function isListed(skill, list) {
|
|
94
|
+
return list.includes(skill.name) || (skill.plugin !== null && list.includes(skill.plugin));
|
|
95
|
+
}
|
|
96
|
+
/** `null` when nothing discovered is listed, so the config carries no empty block. */
|
|
97
|
+
export function renderSkillsBlock(skills, disable) {
|
|
98
|
+
const rules = skills.filter(skill => isListed(skill, disable))
|
|
99
|
+
.map(skill => `[[skills.config]]\nname = ${JSON.stringify(skill.name)}\nenabled = false\n`);
|
|
100
|
+
return rules.length ? [BEGIN, `# Disabled: ${disable.join(", ")}`, ...rules, END].join("\n") + "\n" : null;
|
|
101
|
+
}
|
|
102
|
+
function locateBlock(source) {
|
|
103
|
+
const begin = source.indexOf(BEGIN);
|
|
104
|
+
const end = source.indexOf(END);
|
|
105
|
+
if ((begin === -1) !== (end === -1) || end < begin || source.indexOf(BEGIN, begin + 1) !== -1) {
|
|
106
|
+
throw new Error("The managed codex skills block in config.toml is damaged; restore its BEGIN/END markers or remove it");
|
|
107
|
+
}
|
|
108
|
+
return begin === -1 ? null : { begin, end: end + END.length };
|
|
109
|
+
}
|
|
110
|
+
function skillRules(source) {
|
|
111
|
+
let config;
|
|
112
|
+
try {
|
|
113
|
+
config = getStaticTOMLValue(parseTOML(source, { tomlVersion: "1.0" }));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Parser messages can include source snippets from credential-bearing TOML.
|
|
117
|
+
throw new Error("config.toml would be invalid with the managed skills block; is skills.config defined inline?");
|
|
118
|
+
}
|
|
119
|
+
const skills = config && typeof config === "object" ? config.skills : undefined;
|
|
120
|
+
const rules = skills && typeof skills === "object" ? skills.config : undefined;
|
|
121
|
+
return Array.isArray(rules) ? rules : [];
|
|
122
|
+
}
|
|
123
|
+
/** Replace, append or (with `null`) remove the managed block, always leaving it last in the file. */
|
|
124
|
+
export function setSkillsBlock(source, block) {
|
|
125
|
+
const found = locateBlock(source);
|
|
126
|
+
let rest = source;
|
|
127
|
+
if (found) {
|
|
128
|
+
const after = source.slice(found.end).replace(/^\r?\n/u, "");
|
|
129
|
+
const before = source.slice(0, found.begin).replace(/\n+$/u, "\n");
|
|
130
|
+
rest = before === "\n" ? after : before + after;
|
|
131
|
+
}
|
|
132
|
+
const result = block === null ? rest : `${rest === "" ? "" : `${rest.replace(/\n*$/u, "\n")}\n`}${block}`;
|
|
133
|
+
if (block !== null) {
|
|
134
|
+
const expected = [...block.matchAll(/^name = (".*")\nenabled = (true|false)$/gmu)].map(match => `${JSON.parse(match[1])}=${match[2]}`);
|
|
135
|
+
const actual = skillRules(result).slice(-expected.length).map(rule => `${String(rule.name)}=${String(rule.enabled)}`);
|
|
136
|
+
if (expected.length && actual.join("\n") !== expected.join("\n"))
|
|
137
|
+
throw new Error("Managed skills rules did not parse as the last skills.config entries");
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
async function enrolled(agentDir) {
|
|
142
|
+
const source = await readOptional(files(agentDir).state);
|
|
143
|
+
if (source === null)
|
|
144
|
+
return false;
|
|
145
|
+
const state = JSON.parse(source);
|
|
146
|
+
return !!state && typeof state === "object" && "active" in state && state.active === true;
|
|
147
|
+
}
|
|
148
|
+
/** Disable the listed skills in this agent's Codex home. New plugin skills are caught on the next refresh. */
|
|
149
|
+
export async function refreshCodexSkills(agentDir, disable) {
|
|
150
|
+
return withLock({ agentDir, prompt: "" }, async () => {
|
|
151
|
+
if (!await enrolled(agentDir))
|
|
152
|
+
return { skipped: true, changed: false };
|
|
153
|
+
const path = files(agentDir).config;
|
|
154
|
+
const current = await readOptional(path) ?? "";
|
|
155
|
+
const skills = await discoverCodexSkills(agentDir);
|
|
156
|
+
const changed = await replaceFile(path, current, setSkillsBlock(current, renderSkillsBlock(skills, disable)));
|
|
157
|
+
return {
|
|
158
|
+
changed,
|
|
159
|
+
// Codex reads skills config when its app-server starts; running sessions keep their list.
|
|
160
|
+
restartRequired: changed,
|
|
161
|
+
disabled: skills.filter(skill => isListed(skill, disable)).map(skill => skill.name),
|
|
162
|
+
notFound: disable.filter(entry => !skills.some(skill => isListed(skill, [entry]))),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
export async function removeCodexSkills(agentDir) {
|
|
167
|
+
return withLock({ agentDir, prompt: "" }, async () => {
|
|
168
|
+
const path = files(agentDir).config;
|
|
169
|
+
const current = await readOptional(path);
|
|
170
|
+
if (current === null)
|
|
171
|
+
return { changed: false };
|
|
172
|
+
return { changed: await replaceFile(path, current, setSkillsBlock(current, null)) };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export async function codexSkillsStatus(agentDir, disable) {
|
|
176
|
+
const source = await readOptional(files(agentDir).config) ?? "";
|
|
177
|
+
const found = locateBlock(source);
|
|
178
|
+
const managed = found ? source.slice(found.begin, found.end) : "";
|
|
179
|
+
const disabled = [...managed.matchAll(/^name = (".*")\nenabled = false$/gmu)].map(match => JSON.parse(match[1]));
|
|
180
|
+
return { managed: found !== null, configured: disable, disabled };
|
|
181
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-codex-prompt",
|
|
3
3
|
"name": "Unblock Codex Prompt",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "Maintain an agent-scoped Codex base prompt and its model_instructions_file pointer.",
|
|
6
6
|
"activation": { "onStartup": true },
|
|
7
7
|
"configSchema": {
|
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
"agentName": { "type": "string", "minLength": 1, "maxLength": 100 },
|
|
13
13
|
"frozenContext": { "type": "boolean", "default": false },
|
|
14
14
|
"promptUrl": { "type": "string", "format": "uri", "pattern": "^https://" },
|
|
15
|
-
"suppressExplicitDelegationPrompt": { "type": "boolean", "default": false }
|
|
15
|
+
"suppressExplicitDelegationPrompt": { "type": "boolean", "default": false },
|
|
16
|
+
"disabledCodexSkills": { "type": "array", "default": ["pages", "work-pets"], "maxItems": 64, "uniqueItems": true, "items": { "type": "string", "pattern": "^[\\w.@:-]{1,128}$" } }
|
|
16
17
|
}
|
|
17
18
|
},
|
|
18
19
|
"uiHints": {
|
|
19
20
|
"agentId": { "label": "OpenClaw agent ID", "help": "Only this agent's managed Codex home is modified. Defaults to main." },
|
|
20
21
|
"agentName": { "label": "Agent name", "help": "Optional name in the shared prompt. Omit for a generic opening." },
|
|
21
|
-
"frozenContext": { "label": "Frozen context bridge", "help": "Opt in
|
|
22
|
+
"frozenContext": { "label": "Frozen context bridge", "help": "Opt in on OpenClaw/Codex >=2026.9.2 with a supported context-assembly layout. Initial setup requires explicit sync and activation. Local documents and skills stay frozen until another sync." },
|
|
22
23
|
"promptUrl": { "label": "Fleet prompt endpoint", "help": "Public HTTPS plain-text endpoint with X-Prompt-Revision SHA-256. After initial sync, refreshed daily and when overdue on startup without refreshing agent context. Requires frozenContext; replaces both bundled policy files." },
|
|
23
|
-
"suppressExplicitDelegationPrompt": { "label": "Suppress conflicting Codex delegation policy", "help": "Manage a model_catalog_json override, refreshed from the configured provider daily and when overdue on startup. Requires frozenContext and an available provider env_key. Generated-file edits are replaced on refresh." }
|
|
24
|
+
"suppressExplicitDelegationPrompt": { "label": "Suppress conflicting Codex delegation policy", "help": "Manage a model_catalog_json override, refreshed from the configured provider daily and when overdue on startup. Requires frozenContext and an available provider env_key. Generated-file edits are replaced on refresh." },
|
|
25
|
+
"disabledCodexSkills": { "label": "Codex skills to remove", "help": "Plugin names (all of their skills) or exact skill names to disable in Codex's native skills list. Defaults to ChatGPT Pages and Pets. Applied through a managed [[skills.config]] block at startup, sync and daily refresh; other skills are untouched. Set [] to stop managing Codex skills." }
|
|
24
26
|
}
|
|
25
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-codex-prompt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Manage a shared Codex base prompt for an OpenClaw agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"release:check": "node scripts/check-release-version.mjs",
|
|
18
18
|
"prepack": "npm run build"
|
|
19
19
|
},
|
|
20
|
-
"dependencies": { "toml-eslint-parser": "^1.0.3" },
|
|
20
|
+
"dependencies": { "acorn": "^8.18.0", "toml-eslint-parser": "^1.0.3" },
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@openclaw/plugin-inspector": "^0.3.25",
|
|
23
23
|
"@types/node": "^22.0.0",
|