@unblocklabs/unblock-codex-prompt 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # Unblock Codex Prompt
2
2
 
3
3
  A small, explicit prompt-management bridge for dedicated OpenClaw Mac agents.
4
- **Version 0.2.0.** The frozen bridge supports OpenClaw/Codex 2026.9.2 and newer
4
+ **Version 0.2.1.** The frozen bridge supports OpenClaw/Codex 2026.9.2 and newer
5
5
  stable releases with a compatible context-assembly layout.
6
6
 
7
7
  ## What you control
@@ -29,7 +29,8 @@ files use private permissions. Missing identity documents are recorded as absent
29
29
  ## Compatibility
30
30
 
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
32
+ 2026.9.2, 2026.9.3, 2026.9.4 and 2026.9.8, including parent-local context and split
33
+ `dist/.setup/` bundles. Bundle names
33
34
  may change; fingerprints of the context-assembly functions must still match.
34
35
  Changed assembly is refused pending review, rather than patched blindly.
35
36
  - Managed local stdio Codex, agent-scoped home, no custom command/arguments.
@@ -37,8 +38,11 @@ files use private permissions. Missing identity documents are recorded as absent
37
38
  children may inherit the parent's frozen context.
38
39
 
39
40
  This bridge uses two private upstream seams because public prompt hooks cannot
40
- fix the dropped-context path: one skill-catalog builder and two entry points in
41
- one Codex adapter bundle (plus the parent-local context entry point on newer builds).
41
+ fix the dropped-context path: one skill-catalog builder and guarded Codex context
42
+ carriers. Newer builds also require parent-local suppression and removal of the
43
+ separate thread-level skills append. The installer backs up and tracks every
44
+ patched bundle; old single-file receipts remain restorable. Runtime module paths
45
+ come from the loader's selected entry, never its temporary execution capture.
42
46
  It validates private seams and is not a promise of compatibility with unseen layouts.
43
47
  The catalog uses OpenClaw's eligibility, allowlist, visibility and size-limit
44
48
  logic. Session-specific and execution-directory skills are not snapshotted.
@@ -50,7 +54,7 @@ Remote-node-only skills are not added; this is for dedicated local Mac agents.
50
54
  npm ci
51
55
  npm run preflight
52
56
  npm pack
53
- openclaw plugins install ./unblocklabs-unblock-codex-prompt-0.2.0.tgz --force --accept-capabilities
57
+ openclaw plugins install ./unblocklabs-unblock-codex-prompt-0.2.1.tgz --force --accept-capabilities
54
58
  ```
55
59
 
56
60
  Include the plugin in `plugins.allow` if applicable, then configure:
@@ -236,7 +240,7 @@ without restoring leaves the frozen base but re-enables live injections.
236
240
 
237
241
  ```sh
238
242
  npm run preflight
239
- npm run release:check -- v0.2.0
243
+ npm run release:check -- v0.2.1
240
244
  ```
241
245
 
242
246
  Tests cover compilation, explicit refresh, native/private-body exclusion,
@@ -246,7 +250,7 @@ proof and limits.
246
250
 
247
251
  Install with
248
252
  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`.
253
+ `openclaw plugins install npm:@unblocklabs/unblock-codex-prompt@0.2.1 --accept-capabilities`.
250
254
  The existing [GitHub Release workflow](RELEASING.md) publishes npm only when a
251
255
  GitHub Release is published. Normal pushes do not release the package.
252
256
 
package/dist/adapter.d.ts CHANGED
@@ -1,24 +1,37 @@
1
- type Receipt = {
2
- schema: 1;
3
- packageDir: string;
1
+ type AdapterFile = {
4
2
  path: string;
5
3
  originalSha256: string;
6
4
  patchedSha256: string;
5
+ previousPatchedSha256?: string;
6
+ };
7
+ type Receipt = ({
8
+ schema: 1;
9
+ packageDir: string;
10
+ } & AdapterFile) | {
11
+ schema: 2;
12
+ packageDir: string;
13
+ files: AdapterFile[];
7
14
  };
8
15
  export declare function readReceipt(agentDir: string): Promise<Receipt | null>;
9
- export declare function patchAdapterSource(source: string, agentDir: string, agentId: string, runtimePath: string): string;
10
- export declare function prepareAdapter(agentDir: string, agentId: string, suppliedDir?: string): Promise<{
11
- path: string;
12
- current: string;
13
- original: string;
14
- patched: string;
16
+ export declare function patchAdapterSources(sources: string[], agentDir: string, agentId: string, runtimePath: string): string[];
17
+ export declare function prepareAdapter(agentDir: string, agentId: string, suppliedDir: string | undefined, runtimePath: string): Promise<{
18
+ files: {
19
+ patched: string;
20
+ path: string;
21
+ current: string;
22
+ original: string;
23
+ }[];
15
24
  receipt: {
16
- schema: 1;
25
+ schema: 2;
17
26
  packageDir: string;
18
- path: string;
19
- originalSha256: string;
20
- patchedSha256: string;
27
+ files: {
28
+ previousPatchedSha256?: string;
29
+ path: string;
30
+ originalSha256: string;
31
+ patchedSha256: string;
32
+ }[];
21
33
  };
34
+ previousReceipt: string | null;
22
35
  }>;
23
36
  export declare function installAdapter(agentDir: string, plan: Awaited<ReturnType<typeof prepareAdapter>>): Promise<{
24
37
  adapterChanged: boolean;
@@ -27,10 +40,12 @@ export declare function installAdapter(agentDir: string, plan: Awaited<ReturnTyp
27
40
  export declare function adapterStatus(agentDir: string): Promise<{
28
41
  installed: boolean;
29
42
  path?: never;
43
+ paths?: never;
30
44
  minimumVersion?: never;
31
45
  } | {
32
46
  installed: boolean;
33
47
  path: string;
48
+ paths: string[];
34
49
  minimumVersion: string;
35
50
  }>;
36
51
  export declare function restoreAdapter(agentDir: string): Promise<void>;
package/dist/adapter.js CHANGED
@@ -1,62 +1,93 @@
1
- import { readFile, readdir } from "node:fs/promises";
1
+ import { lstat, readFile, readdir } from "node:fs/promises";
2
2
  import { basename, isAbsolute, join, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
3
  import { parse } from "acorn";
5
4
  import { readOptional, replaceFile, sha256 } from "./manager.js";
6
5
  import { openclawRoot } from "./compiler.js";
7
6
  import { assertSupportedVersion, MINIMUM_OPENCLAW } from "./compatibility.js";
8
7
  const MARKER = "// unblock-codex-prompt bridge v1";
9
- const BUNDLE_NAME = /^thread-lifecycle-[\w-]+\.m?js$/u;
8
+ const BUNDLE_NAME = /^thread-(lifecycle|requests)-[\w-]+\.m?js$/u;
10
9
  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.
10
+ // Reviewed context carriers, independent of release-specific bundle names.
13
11
  const CONTEXT_HASHES = {
14
12
  legacy: "1fb45c46c50a63f68e8b6bdfbc5d7ae2d323470c00db69779d159592868940bb",
15
13
  parent: "5ee80edf3f3887e841def5d64896a3d27c35063f6d4803dceeb8b8bf07b654f0",
14
+ splitParent: "6515e383260e0edd4379a2e694d9b066ddb02a79b55fd7494bf7f85affee72c6",
16
15
  collaboration: "aea7e95ca43f0997b0a3f616f327132bfdd057bb731e6eead132fba21de3f981",
16
+ legacyThread: "703a32a0b18eacd45a9fd3d68c04a13527bdbeaae50a11a6b76768b84b99a9ed",
17
+ thread: "3aa4c414cb449cdb9167e6ed2df9397559176310ce7cff882652bd669ac8cee8",
17
18
  };
18
19
  function receiptPath(agentDir) { return join(agentDir, "codex-home", ".unblock-codex-prompt-adapter.json"); }
19
- function backupPath(agentDir) { return join(agentDir, "codex-home", ".unblock-codex-prompt-adapter-original.js"); }
20
- export async function readReceipt(agentDir) {
21
- const source = await readOptional(receiptPath(agentDir));
20
+ function receiptFiles(receipt) { return receipt.schema === 1 ? [receipt] : receipt.files; }
21
+ function backupPath(agentDir, file, legacy = false) {
22
+ return join(agentDir, "codex-home", legacy ? ".unblock-codex-prompt-adapter-original.js" : ".unblock-codex-prompt-adapter-original-" + basename(file.path));
23
+ }
24
+ function ownsSource(file, source) {
25
+ if (source === null)
26
+ return false;
27
+ const hash = sha256(source);
28
+ return hash === file.originalSha256 || hash === file.patchedSha256 || hash === file.previousPatchedSha256;
29
+ }
30
+ function parseReceipt(source) {
22
31
  if (source === null)
23
32
  return null;
24
33
  const value = JSON.parse(source);
25
- if (!value || typeof value !== "object" || !("schema" in value) || value.schema !== 1 ||
26
- !("packageDir" in value) || typeof value.packageDir !== "string" ||
27
- !("path" in value) || typeof value.path !== "string" ||
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)))
34
+ if (!value || typeof value !== "object" || !("schema" in value) || (value.schema !== 1 && value.schema !== 2) ||
35
+ !("packageDir" in value) || typeof value.packageDir !== "string" || !isAbsolute(value.packageDir))
36
+ throw new Error("Invalid adapter receipt");
37
+ const packageDir = value.packageDir;
38
+ const entries = value.schema === 1 ? [value] : "files" in value && Array.isArray(value.files) ? value.files : [];
39
+ if (!entries.length)
32
40
  throw new Error("Invalid adapter receipt");
33
- return { schema: 1, packageDir: value.packageDir, path: value.path, originalSha256: value.originalSha256, patchedSha256: value.patchedSha256 };
41
+ const files = entries.map((entry) => {
42
+ if (!entry || typeof entry !== "object" || !("path" in entry) || typeof entry.path !== "string" ||
43
+ !BUNDLE_NAME.test(basename(entry.path)) ||
44
+ ![join(packageDir, "dist", basename(entry.path)), ...(value.schema === 2 ? [join(packageDir, "dist", ".setup", basename(entry.path))] : [])].includes(entry.path) ||
45
+ (value.schema === 1 && !basename(entry.path).startsWith("thread-lifecycle-")) ||
46
+ !("originalSha256" in entry) || typeof entry.originalSha256 !== "string" || !HASH.test(entry.originalSha256) ||
47
+ !("patchedSha256" in entry) || typeof entry.patchedSha256 !== "string" || !HASH.test(entry.patchedSha256))
48
+ throw new Error("Invalid adapter receipt");
49
+ const previous = "previousPatchedSha256" in entry ? entry.previousPatchedSha256 : undefined;
50
+ if (previous !== undefined && (typeof previous !== "string" || !HASH.test(previous)))
51
+ throw new Error("Invalid adapter receipt");
52
+ return { path: entry.path, originalSha256: entry.originalSha256, patchedSha256: entry.patchedSha256,
53
+ ...(typeof previous === "string" ? { previousPatchedSha256: previous } : {}) };
54
+ });
55
+ if (new Set(files.map(file => basename(file.path))).size !== files.length)
56
+ throw new Error("Invalid adapter receipt");
57
+ return value.schema === 1 ? { schema: 1, packageDir, ...files[0] } : { schema: 2, packageDir, files };
34
58
  }
35
- export function patchAdapterSource(source, agentDir, agentId, runtimePath) {
36
- if (source.includes(MARKER))
59
+ export async function readReceipt(agentDir) { return parseReceipt(await readOptional(receiptPath(agentDir))); }
60
+ export function patchAdapterSources(sources, agentDir, agentId, runtimePath) {
61
+ if (sources.some(source => source.includes(MARKER)))
37
62
  throw new Error("Unsupported Codex adapter shape: already patched");
38
- const functions = parse(source, { ecmaVersion: "latest", sourceType: "module" }).body.filter(node => node.type === "FunctionDeclaration");
63
+ const functions = sources.flatMap((source, bundle) => parse(source, { ecmaVersion: "latest", sourceType: "module" }).body
64
+ .filter(node => node.type === "FunctionDeclaration").map(node => ({ node, bundle })));
39
65
  const find = (name) => {
40
- const matches = functions.filter(node => node.id?.name === name);
66
+ const matches = functions.filter(fn => fn.node.id?.name === name);
41
67
  if (matches.length !== 1)
42
- throw new Error(`Unsupported Codex adapter shape: ${name}`);
68
+ throw new Error("Unsupported Codex adapter shape: " + name);
43
69
  return matches[0];
44
70
  };
45
71
  const developer = find("buildDeveloperInstructions");
46
72
  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)))
73
+ const parent = functions.some(fn => fn.node.id?.name === "buildCodexParentLocalInstructions") ? find("buildCodexParentLocalInstructions") : null;
74
+ const thread = functions.some(fn => fn.node.id?.name === "buildCodexThreadConfiguration") ? find("buildCodexThreadConfiguration") : null;
75
+ for (const { node, bundle } of [developer, collaboration, ...(parent ? [parent] : []), ...(thread ? [thread] : [])]) {
76
+ if (node.async || node.generator || node.params.length !== 2 || node.params[0]?.type !== "Identifier" || node.params[0].name !== "params" ||
77
+ !/^(options|options\s*=\s*\{\})$/u.test(sources[bundle].slice(node.params[1].start, node.params[1].end)))
51
78
  throw new Error("Unsupported Codex adapter shape: parameters");
52
79
  }
53
80
  for (const name of ["buildDefaultCollaborationInstructions", "buildCronCollaborationInstructions"]) {
54
- if (find(name).params.length !== 0)
81
+ const helper = find(name);
82
+ if (helper.node.params.length !== 0 || helper.bundle !== collaboration.bundle)
55
83
  throw new Error("Unsupported Codex adapter shape: mode helpers");
56
84
  }
57
- const fingerprint = (fn) => sha256(source.slice(fn.start, fn.end));
85
+ const fingerprint = (fn) => sha256(sources[fn.bundle].slice(fn.node.start, fn.node.end));
86
+ const split = parent !== null && fingerprint(parent) === CONTEXT_HASHES.splitParent;
58
87
  if (fingerprint(collaboration) !== (parent ? CONTEXT_HASHES.collaboration : CONTEXT_HASHES.legacy) ||
59
- (parent && fingerprint(parent) !== CONTEXT_HASHES.parent))
88
+ (parent && (parent.bundle !== collaboration.bundle || ![CONTEXT_HASHES.parent, CONTEXT_HASHES.splitParent].includes(fingerprint(parent)))) ||
89
+ (thread && thread.bundle !== developer.bundle) ||
90
+ (split ? !thread || fingerprint(thread) !== CONTEXT_HASHES.thread : thread !== null && fingerprint(thread) !== CONTEXT_HASHES.legacyThread))
60
91
  throw new Error("Unsupported Codex context assembly; review the changed upstream functions before syncing");
61
92
  const prefix = `${MARKER}
62
93
  import { createRequire as unblockPromptCreateRequire } from "node:module";
@@ -73,18 +104,27 @@ function unblockFrozenPolicy(params) {
73
104
  }
74
105
  `;
75
106
  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;' }] : []),
107
+ { fn: developer, 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");' },
108
+ { fn: collaboration, text: '\n\tif (unblockFrozenPolicy(params) !== null) return params.trigger === "cron" ? buildCronCollaborationInstructions() : buildDefaultCollaborationInstructions();' },
109
+ ...(parent ? [{ fn: parent, text: '\n\tif (unblockFrozenPolicy(params) !== null) return params.trigger === "cron" ? buildCronCollaborationInstructions() : null;' }] : []),
110
+ // 2026.9.8 moved skills to the start/resume/fork carrier, outside developer policy.
111
+ ...(split && thread ? [{ fn: thread, text: '\n\tif (unblockFrozenPolicy(params) !== null) options = { ...options, skillsInstructions: undefined };' }] : []),
79
112
  ];
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;
113
+ return sources.map((source, bundle) => {
114
+ const edits = insertions.filter(edit => edit.fn.bundle === bundle).sort((a, b) => b.fn.node.body.start - a.fn.node.body.start);
115
+ for (const { fn, text } of edits) {
116
+ const at = fn.node.body.start + 1;
117
+ source = source.slice(0, at) + text + source.slice(at);
118
+ }
119
+ return edits.length ? prefix + source : source;
120
+ });
84
121
  }
85
- export async function prepareAdapter(agentDir, agentId, suppliedDir) {
122
+ export async function prepareAdapter(agentDir, agentId, suppliedDir, runtimePath) {
86
123
  await openclawRoot();
87
- const receipt = await readReceipt(agentDir);
124
+ if (!isAbsolute(runtimePath) || await readOptional(runtimePath) === null)
125
+ throw new Error("Missing durable plugin runtime");
126
+ const previousReceipt = await readOptional(receiptPath(agentDir));
127
+ const receipt = parseReceipt(previousReceipt);
88
128
  const packageDir = suppliedDir ? resolve(suppliedDir) : receipt?.packageDir;
89
129
  if (!packageDir)
90
130
  throw new Error("First frozen sync requires --codex-plugin-dir from openclaw plugins inspect codex --json");
@@ -94,50 +134,88 @@ export async function prepareAdapter(agentDir, agentId, suppliedDir) {
94
134
  if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== "@openclaw/codex")
95
135
  throw new Error("Expected the official @openclaw/codex package");
96
136
  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)
137
+ const dist = join(packageDir, "dist");
138
+ if (!(await lstat(dist)).isDirectory())
99
139
  throw new Error("Unsupported Codex bundle layout");
100
- const path = join(packageDir, "dist", files[0]);
101
- const current = await readOptional(path);
102
- if (current === null)
103
- throw new Error("Missing Codex adapter");
104
- const original = receipt ? await readOptional(backupPath(agentDir)) : current;
105
- if (receipt && receipt.path !== path)
140
+ const entries = await readdir(dist, { withFileTypes: true });
141
+ const setup = entries.find(entry => entry.name === ".setup");
142
+ if (setup && !setup.isDirectory())
143
+ throw new Error("Unsupported Codex bundle layout");
144
+ const paths = entries.filter(entry => BUNDLE_NAME.test(entry.name)).map(entry => join(dist, entry.name));
145
+ if (setup)
146
+ paths.push(...(await readdir(join(dist, ".setup"))).filter(name => BUNDLE_NAME.test(name)).map(name => join(dist, ".setup", name)));
147
+ paths.sort();
148
+ if (!paths.length || (receipt && receiptFiles(receipt).some(file => !paths.includes(file.path))))
149
+ throw new Error("Unsupported Codex bundle layout; restore before changing installations");
150
+ const bundles = await Promise.all(paths.map(async (path) => {
151
+ const current = await readOptional(path);
152
+ if (current === null)
153
+ throw new Error("Missing Codex adapter");
154
+ const file = receipt && receiptFiles(receipt).find(file => file.path === path);
155
+ const original = file ? await readOptional(backupPath(agentDir, file, receipt?.schema === 1)) : current;
156
+ if (original === null || (file && sha256(original) !== file.originalSha256))
157
+ throw new Error("Codex adapter backup invalid; refusing to patch");
158
+ if (file && !ownsSource(file, current))
159
+ throw new Error("Codex adapter changed by another writer; refusing to overwrite");
160
+ return { path, current, original };
161
+ }));
162
+ const patches = patchAdapterSources(bundles.map(bundle => bundle.original), agentDir, agentId, runtimePath);
163
+ const files = bundles.flatMap((bundle, index) => {
164
+ const patched = patches[index];
165
+ return patched === bundle.original ? [] : [{ ...bundle, patched }];
166
+ });
167
+ if (receipt && (files.length !== receiptFiles(receipt).length || files.some(file => !receiptFiles(receipt).some(previous => previous.path === file.path))))
106
168
  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");
109
- if (receipt && sha256(current) !== receipt.patchedSha256 && current !== original)
110
- throw new Error("Codex adapter changed by another writer; refusing to overwrite");
111
- const patched = patchAdapterSource(original, agentDir, agentId, fileURLToPath(new URL("./runtime.js", import.meta.url)));
112
- return { path, current, original, patched, receipt: { schema: 1, packageDir, path, originalSha256: sha256(original), patchedSha256: sha256(patched) } };
169
+ const nextReceipt = { schema: 2, packageDir, files: files.map(file => ({
170
+ path: file.path, originalSha256: sha256(file.original), patchedSha256: sha256(file.patched),
171
+ // Receipt-first upgrades must remain recoverable if source swapping is interrupted.
172
+ ...(file.current !== file.original && file.current !== file.patched ? { previousPatchedSha256: sha256(file.current) } : {}),
173
+ })) };
174
+ return { files, receipt: nextReceipt, previousReceipt };
113
175
  }
114
176
  export async function installAdapter(agentDir, plan) {
115
- const backup = await readOptional(backupPath(agentDir));
116
- if (backup !== null && backup !== plan.original)
117
- throw new Error("Foreign adapter backup");
118
- await replaceFile(backupPath(agentDir), backup, plan.original);
119
- // Receipt first allows crash recovery; source swap is atomic and guarded.
120
- await replaceFile(receiptPath(agentDir), await readOptional(receiptPath(agentDir)), `${JSON.stringify(plan.receipt, null, 2)}\n`);
121
- return { adapterChanged: await replaceFile(plan.path, plan.current, plan.patched), restartRequired: true };
177
+ // Validate every target before any bundle changes; retain exact backups for recovery.
178
+ const backups = await Promise.all(plan.files.map(async (file) => {
179
+ const path = backupPath(agentDir, file);
180
+ const backup = await readOptional(path);
181
+ if (backup !== null && backup !== file.original)
182
+ throw new Error("Foreign adapter backup");
183
+ if (await readOptional(file.path) !== file.current)
184
+ throw new Error("Concurrent adapter edit; refusing to overwrite");
185
+ return { path, backup, original: file.original };
186
+ }));
187
+ for (const backup of backups)
188
+ await replaceFile(backup.path, backup.backup, backup.original);
189
+ await replaceFile(receiptPath(agentDir), plan.previousReceipt, JSON.stringify(plan.receipt, null, 2) + "\n");
190
+ let adapterChanged = false;
191
+ for (const file of plan.files)
192
+ adapterChanged = await replaceFile(file.path, file.current, file.patched) || adapterChanged;
193
+ return { adapterChanged, restartRequired: true };
122
194
  }
123
195
  export async function adapterStatus(agentDir) {
124
196
  const receipt = await readReceipt(agentDir);
125
197
  if (!receipt)
126
198
  return { installed: false };
127
- const current = await readOptional(receipt.path);
128
- return { installed: current !== null && sha256(current) === receipt.patchedSha256, path: receipt.path, minimumVersion: MINIMUM_OPENCLAW };
199
+ const files = receiptFiles(receipt);
200
+ const installed = (await Promise.all(files.map(async (file) => {
201
+ const current = await readOptional(file.path);
202
+ return current !== null && sha256(current) === file.patchedSha256;
203
+ }))).every(Boolean);
204
+ return { installed, path: files[0].path, paths: files.map(file => file.path), minimumVersion: MINIMUM_OPENCLAW };
129
205
  }
130
206
  export async function restoreAdapter(agentDir) {
131
207
  const receipt = await readReceipt(agentDir);
132
208
  if (!receipt)
133
209
  return;
134
- const current = await readOptional(receipt.path);
135
- const original = await readOptional(backupPath(agentDir));
136
- if (original === null || sha256(original) !== receipt.originalSha256)
137
- throw new Error("Adapter backup invalid");
138
- if (current === original)
139
- return;
140
- if (current === null || sha256(current) !== receipt.patchedSha256)
141
- throw new Error("Foreign adapter edit; refusing restore");
142
- await replaceFile(receipt.path, current, original);
210
+ const files = await Promise.all(receiptFiles(receipt).map(async (file) => {
211
+ const current = await readOptional(file.path);
212
+ const original = await readOptional(backupPath(agentDir, file, receipt.schema === 1));
213
+ if (original === null || sha256(original) !== file.originalSha256)
214
+ throw new Error("Adapter backup invalid");
215
+ if (!ownsSource(file, current))
216
+ throw new Error("Foreign adapter edit; refusing restore");
217
+ return { path: file.path, current, original };
218
+ }));
219
+ for (const file of files)
220
+ await replaceFile(file.path, file.current, file.original);
143
221
  }
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { buildSkillsCatalog, compilePrompt } from "./compiler.js";
8
8
  import { readFrozenPolicy } from "./runtime.js";
9
9
  import { fetchPrompt, REMOTE_POLICY } from "./remote.js";
10
10
  import { readOptional } from "./manager.js";
11
- import { join } from "node:path";
11
+ import { basename, dirname, isAbsolute, join } from "node:path";
12
12
  import { CATALOG_REFRESH_MS, catalogStatus, refreshCatalog, restoreCatalog, startCatalogRefresh } from "./catalog.js";
13
13
  import { codexSkillsStatus, refreshCodexSkills, removeCodexSkills } from "./skills.js";
14
14
  async function refresh(target, automatic = false) {
@@ -148,7 +148,12 @@ export default definePluginEntry({
148
148
  sync.action(async () => {
149
149
  const target = await resolveTarget(config);
150
150
  const options = sync.opts();
151
- const plan = target.settings.frozenContext ? await prepareAdapter(target.agentDir, target.settings.agentId, options.codexPluginDir) : null;
151
+ // Captured import.meta.url paths expire with the CLI. Older hosts expose the
152
+ // published dist/index.js via source; newer hosts select it via runtimeSource.
153
+ const source = "runtimeSource" in api && typeof api.runtimeSource === "string" ? api.runtimeSource : api.source;
154
+ if (target.settings.frozenContext && (!isAbsolute(source) || basename(source) !== "index.js"))
155
+ throw new Error("Frozen sync requires the packaged plugin runtime entry");
156
+ const plan = target.settings.frozenContext ? await prepareAdapter(target.agentDir, target.settings.agentId, options.codexPluginDir, join(dirname(source), "runtime.js")) : null;
152
157
  const remote = target.settings.promptUrl ? await fetchPrompt(target.settings.promptUrl) : null;
153
158
  const prompt = remote?.prompt ?? await bundledPrompt(target.settings.agentName);
154
159
  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;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-codex-prompt",
3
3
  "name": "Unblock Codex Prompt",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "description": "Maintain an agent-scoped Codex base prompt and its model_instructions_file pointer.",
6
6
  "activation": { "onStartup": true },
7
7
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unblocklabs/unblock-codex-prompt",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Manage a shared Codex base prompt for an OpenClaw agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",