codex-copilot-dx 0.6.7 → 0.6.9

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
@@ -112,13 +112,17 @@ ccdx doctor --online --profile all
112
112
  ### PM Studio opt-in patch
113
113
 
114
114
  CCDX can add the isolated Claude catalog to PM Studio without switching its
115
- active account or profile. GPT requests continue to use the bearer supplied by
116
- the current PM profile; exact enabled Anthropic model IDs use the isolated
117
- CCDX Claude profile. Neither credential is copied into the other application.
118
-
119
- The current patch recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
120
- macOS. Authorize the isolated Claude profile first, quit PM Studio and its
121
- updater, then run the one-time setup command:
115
+ active account or profile. GPT requests keep PM Studio's native official GitHub
116
+ Copilot transport and the bearer supplied by the current PM profile. Model
117
+ discovery tries the compatible local CCDX merge first, and only exact enabled
118
+ Anthropic model IDs use the isolated CCDX Claude profile. Neither credential is
119
+ copied into the other application.
120
+
121
+ The current patch recipes support only the exact, unmodified PM Studio 2.9.7
122
+ and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
123
+ 2.9.11 and other unknown builds remain unsupported until they receive their own
124
+ verified recipe. Authorize the isolated Claude profile first, quit PM Studio
125
+ and its updater, then run the one-time setup command:
122
126
 
123
127
  ```bash
124
128
  ccdx auth login claude --reauth --github-login <personal-github-login>
@@ -145,22 +149,40 @@ does not attempt a second automatic write: it preserves the verified backup,
145
149
  retains any diagnostic stage still present, and prints recovery paths. Repeating
146
150
  setup for the exact installed recipe is idempotent.
147
151
 
148
- The patch is App-wide: every PM profile sends its Copilot API traffic through
149
- the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
150
- Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
151
- relay explicitly supports only the PM 2.9.7 Copilot paths discovered in the
152
- validated bundle (`GET /models` and `POST /chat/completions`, `/responses`, and
153
- `/embeddings`) and is not a general-purpose proxy.
152
+ The official 2.9.10 artifact can fail macOS's vendor-signature verifier even
153
+ though its signing metadata remains readable. CCDX does not generally ignore
154
+ that failure: the 2.9.10 exception is accepted only when the complete App tree,
155
+ including every file, mode, symlink, and non-volatile extended attribute,
156
+ matches the frozen official fingerprint. The staged and installed CCDX patch
157
+ must still pass strict ad-hoc signature verification and match the complete
158
+ patched-tree record in the backup manifest.
159
+
160
+ The patch uses split origins, not an App-wide proxy. GPT and other non-Claude
161
+ inference requests remain on PM Studio's native official GitHub Copilot origin;
162
+ they do not enter CCDX or depend on port 2026. Model discovery first tries the
163
+ loopback-only `GET /pm-ccdx/models` merge and accepts it only when the relay
164
+ returns the `X-CCDX-PM-Relay: split-origin-v1` compatibility marker. If the
165
+ relay is unavailable or incompatible, model discovery safely falls back to the
166
+ official directory and GPT remains available. Exact enabled Claude chat uses
167
+ `POST /pm-ccdx/chat/completions` and fails closed if CCDX is unavailable; it
168
+ never falls back to the PM enterprise account. The relay does not expose
169
+ `/responses` or `/embeddings` and is not a general-purpose proxy.
170
+
171
+ The loopback relay trusts local callers that present a valid Copilot bearer.
172
+ CCDX uses the PM bearer only to fetch or validate official model access; it is
173
+ never used as the credential for the isolated Claude upstream request. Keep
174
+ port 2026 local to the machine.
154
175
 
155
176
  Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
156
177
  updater is not disabled; an official update may overwrite the patch or may
157
178
  require reinstalling the official App. Setup prints the verified backup and
158
179
  manifest paths plus the version-matched restore procedure. Never restore a
159
- 2.9.7 backup over a newer PM Studio installation.
180
+ backup over a different PM Studio version or build.
160
181
 
161
182
  To restore, first quit PM Studio and its updater. Use the exact verified backup
162
183
  path printed by setup as `BACKUP_APP`, then confirm that both the installed App
163
- and backup still report version/build `2.9.7/2.9.7`:
184
+ and backup still report the same version/build printed by setup (`2.9.7/2.9.7`
185
+ or `2.9.10/2.9.10`):
164
186
 
165
187
  ```bash
166
188
  BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
@@ -170,8 +192,8 @@ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
170
192
  /usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
171
193
  ```
172
194
 
173
- Only when all four values match `2.9.7`, move the patched App aside, restore the
174
- complete vendor-signed backup, and verify it before launch:
195
+ Only when all four values match the one exact recipe, move the patched App
196
+ aside, restore the complete vendor bundle, and verify it before launch:
175
197
 
176
198
  ```bash
177
199
  RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
@@ -179,12 +201,16 @@ PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
179
201
  mv "/Applications/PM Studio.app" "$PATCHED_APP"
180
202
  /usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
181
203
  /usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
204
+ ccdx pms status
182
205
  ```
183
206
 
184
- If the version check, copy, or signature verification fails, do not launch the
185
- restored App. Keep the moved patched bundle at `$PATCHED_APP` and reinstall the official PM Studio
186
- instead of applying a 2.9.7 backup to unknown content. CCDX never runs these
187
- restore commands or `sudo` automatically.
207
+ For 2.9.10, the vendor `codesign` command may report the documented upstream
208
+ signature failure; in that case the App-patch row from `ccdx pms status` must
209
+ still classify the restored bundle as exact supported clean content rather than
210
+ integrity drift. If the version check, copy, or both verification paths fail,
211
+ do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
212
+ and reinstall the official PM Studio instead of applying a version-mismatched
213
+ backup. CCDX never runs these restore commands or `sudo` automatically.
188
214
 
189
215
  To change the model used by Codex Auto-review, run:
190
216
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -73,7 +73,7 @@ function routingTarget(value) {
73
73
  function pmRouteSummary(requests = {}) {
74
74
  const routes = requests.by_route;
75
75
  if (!routes || typeof routes !== "object") return null;
76
- const names = ["pm_models", "pm_chat_completions", "pm_responses", "pm_embeddings"];
76
+ const names = ["pm_models", "pm_chat_completions"];
77
77
  const totals = names.reduce((summary, name) => {
78
78
  const route = routes[name] || {};
79
79
  summary.total += finiteNumber(route.total) || 0;
@@ -81,7 +81,7 @@ function pmRouteSummary(requests = {}) {
81
81
  summary.errors += finiteNumber(route.errors) || 0;
82
82
  return summary;
83
83
  }, { total: 0, active: 0, errors: 0 });
84
- return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}, responses ${count(routes.pm_responses?.total)}, embeddings ${count(routes.pm_embeddings?.total)}`;
84
+ return `PM relay: ${count(totals.total)} requests, ${count(totals.active)} active, ${count(totals.errors)} errors; models ${count(routes.pm_models?.total)}, chat ${count(routes.pm_chat_completions?.total)}`;
85
85
  }
86
86
 
87
87
  function errorCode(error) {
package/src/config.mjs CHANGED
@@ -6,6 +6,20 @@ import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
6
6
  import { adapterBaseUrl } from "./running-adapter.mjs";
7
7
 
8
8
  const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
9
+ const MODEL_CONTEXT_WINDOW = 1_000_000;
10
+ const MODEL_AUTO_COMPACT_TOKEN_LIMIT = 900_000;
11
+
12
+ function setTopLevelTomlDefault(lines, key, value) {
13
+ let end = lines.findIndex((line) => /^\s*\[[^\]]+\]\s*$/.test(line));
14
+ if (end === -1) end = lines.length;
15
+
16
+ const keyRe = new RegExp(`^\\s*${key}\\s*=`);
17
+ if (lines.slice(0, end).some((line) => keyRe.test(line))) return false;
18
+
19
+ while (end > 0 && lines[end - 1].trim() === "") end--;
20
+ lines.splice(end, 0, `${key} = ${value}`);
21
+ return true;
22
+ }
9
23
 
10
24
  function setTomlKey(lines, sectionName, key, value) {
11
25
  const sectionLine = `[${sectionName}]`;
@@ -52,6 +66,8 @@ export function computeUpdatedCodexConfig(content, adapterPort = 2026, adapterHo
52
66
  changed = true;
53
67
  }
54
68
 
69
+ changed = setTopLevelTomlDefault(lines, "model_context_window", MODEL_CONTEXT_WINDOW) || changed;
70
+ changed = setTopLevelTomlDefault(lines, "model_auto_compact_token_limit", MODEL_AUTO_COMPACT_TOKEN_LIMIT) || changed;
55
71
  changed = setTomlKey(lines, "shell_environment_policy.set", "ANTHROPIC_AUTH_TOKEN", "dummy") || changed;
56
72
  changed = setTomlKey(lines, "shell_environment_policy.set", "ANTHROPIC_BASE_URL", anthropicBaseUrl) || changed;
57
73
  changed = setTomlKey(lines, "shell_environment_policy.set", "OPENAI_BASE_URL", baseUrl) || changed;
@@ -64,6 +80,8 @@ function initialCodexConfig(adapterPort, adapterHost) {
64
80
  const anthropicBaseUrl = adapterBaseUrl(adapterHost, adapterPort);
65
81
  const baseUrl = `${anthropicBaseUrl}/v1`;
66
82
  return `openai_base_url = "${baseUrl}"
83
+ model_context_window = ${MODEL_CONTEXT_WINDOW}
84
+ model_auto_compact_token_limit = ${MODEL_AUTO_COMPACT_TOKEN_LIMIT}
67
85
 
68
86
  [shell_environment_policy]
69
87
  inherit = "core"
@@ -17,8 +17,6 @@ const ROUTE_NAMES = Object.freeze([
17
17
  "messages_count_tokens",
18
18
  "pm_models",
19
19
  "pm_chat_completions",
20
- "pm_responses",
21
- "pm_embeddings",
22
20
  "not_found",
23
21
  ]);
24
22
 
@@ -104,8 +102,6 @@ export function classifyAdapterRoute(method, pathname) {
104
102
  if (method === "POST" && pathname === "/v1/messages/count_tokens") return "messages_count_tokens";
105
103
  if (method === "GET" && pathname === "/pm-ccdx/models") return "pm_models";
106
104
  if (method === "POST" && pathname === "/pm-ccdx/chat/completions") return "pm_chat_completions";
107
- if (method === "POST" && pathname === "/pm-ccdx/responses") return "pm_responses";
108
- if (method === "POST" && pathname === "/pm-ccdx/embeddings") return "pm_embeddings";
109
105
  return "not_found";
110
106
  }
111
107
 
@@ -2,8 +2,22 @@ import { createHash } from "node:crypto";
2
2
 
3
3
  export const PM_STUDIO_ORIGIN = "https://api.githubcopilot.com";
4
4
  export const CCDX_PM_STUDIO_ORIGIN = "http://127.0.0.1:2026/pm-ccdx";
5
+ export const PM_STUDIO_SPLIT_ORIGIN_MARKER = "split-origin-v1";
5
6
  export const ELECTRON_ASAR_INTEGRITY_SENTINEL = "AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A";
6
7
 
8
+ const PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH = 2_121;
9
+ const PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256 = "3da6245d676015ab6cd26e4d6a0d2f54894a687c370aa419c5ad170533d086aa";
10
+
11
+ function splitOriginConfigModule() {
12
+ const source = `47024(I,e,t){"use strict";t.d(e,{qn:()=>l});const l={CLIENT_ID:"Iv1.b507a08c87ecfe98",CLIENT_SECRET:void 0,API_ENDPOINT:"${PM_STUDIO_ORIGIN}",DEVICE_CODE_URL:"https://github.com/login/device/code",ACCESS_TOKEN_URL:"https://github.com/login/oauth/access_token",COPILOT_TOKEN_URL:"https://api.github.com/copilot_internal/v2/token",USER_AGENT:"GitHubCopilotChat/0.26.7",EDITOR_VERSION:"vscode/1.99.3",EDITOR_PLUGIN_VERSION:"copilot-chat/0.26.7",INTEGRATION_ID:"vscode-chat",STANDARD_HEADERS:{Accept:"application/json","Content-Type":"application/json","User-Agent":"GitHubCopilotChat/0.26.7","Editor-Version":"vscode/1.99.3","Editor-Plugin-Version":"copilot-chat/0.26.7","Copilot-Integration-Id":"vscode-chat","X-Request-Id":()=>\`req_\${Date.now()}_\${Math.random().toString(36).substr(2,9)}\`}};const c=fetch.bind(globalThis),n="${CCDX_PM_STUDIO_ORIGIN}",a="${PM_STUDIO_SPLIT_ORIGIN_MARKER}",h="X-CCDX-PM-Relay",d=new Set,m=I=>I.headers.get(h)===a,q=I=>AbortSignal.any([I,AbortSignal.timeout(750)].filter(Boolean)),f=(I,e=400)=>Response.json({error:{code:I}},{status:e,headers:{[h]:a}});globalThis.fetch=async(I,e)=>{const t=I.url||I+"";if(t===\`\${l.API_ENDPOINT}/models\`){try{const t=await c(\`\${n}/models\`,{...e,signal:q(e?.signal)});if(t.ok&&m(t))try{let I=await t.clone().json(),e=I.data||I;if(Array.isArray(e)){d.clear();for(const I of e)/anthropic/i.test(I.vendor||I.owned_by)&&d.add(I.id);return t}}catch{}t.body?.cancel?.()?.catch?.(()=>{})}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I}return c(I,e)}const s=t.startsWith(l.API_ENDPOINT)?t.slice(l.API_ENDPOINT.length):"";if("POST"===e?.method&&["/chat/completions","/responses","/embeddings"].includes(s)){let t="";try{t=(JSON.parse(e.body).model||"").trim()}catch{}if(d.has(t)||/^claude-/i.test(t)){if("/chat/completions"!==s)return f("model_not_supported");try{const I=await c(\`\${n}/models\`,{signal:q(e?.signal)}),t=401===I.status&&m(I);await I.text();if(!t)throw 0;const l=await c(\`\${n}\${s}\`,e);if(!m(l)){await l.body?.cancel?.();throw 0}return l}catch(I){if(e?.signal?.aborted)throw e.signal.reason||I;return f("relay_incompatible",503)}}}return c(I,e)}}`;
13
+ if (source.length > PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH) {
14
+ throw new Error("PM Studio split-origin module exceeds its exact source window");
15
+ }
16
+ return `${source.slice(0, -1)}${" ".repeat(PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH - source.length)}}`;
17
+ }
18
+
19
+ export const PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE = splitOriginConfigModule();
20
+
7
21
  export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
8
22
  id: "pm-studio-2.9.7-build-2.9.7",
9
23
  version: "2.9.7",
@@ -21,53 +35,138 @@ export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
21
35
  dataOffset: 3_832_764,
22
36
  sourceAsarSha256: "36fb8bf2fc9326ddf49baab5cc0b54b38751a7db041bda3c42a3b01107b54118",
23
37
  sourceHeaderSha256: "87746d92e00db2d40c03830ea06de22ec7cba112d07729801ccd02e226ffd961",
24
- patchedAsarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
25
- patchedHeaderSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
26
- sourceSentinel: PM_STUDIO_ORIGIN,
27
- patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
38
+ patchedAsarSha256: "d520d115604225c1a3feb749dbe29ad0d2cd175c5233c010de0e1fade527fa0b",
39
+ patchedHeaderSha256: "83e5fba3b6751b3098d8f6901db361a9b620ab3b924e0e551d3df612ff412642",
28
40
  targets: Object.freeze([
29
41
  Object.freeze({
30
42
  path: "dist/main/main.js",
31
43
  offset: 101_621_627,
32
44
  size: 9_909_974,
33
- sentinelOffset: 1_120_480,
34
- absoluteSentinelOffset: 106_574_871,
35
- sentinelCount: 1,
36
45
  blockSize: 4_194_304,
37
46
  sourceSha256: "58383be112886becdb8d8e7cce1b3efce07be6716190aa4940627f456eb8edb2",
38
- patchedSha256: "24f1c47831c30f59e8464a66237c6bc9ed771d79f3a6889019dfdf53bc20fe42",
47
+ patchedSha256: "69156040fb233a835b7be2f86fbe2f653380f03379adfedf46c63ab45de9d39d",
39
48
  sourceBlocks: Object.freeze([
40
49
  "3ce05a21e8992ccf12966cf9b78fcd28b2f66e6786489b5bb0a06f1d4b2a0c1e",
41
50
  "0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
42
51
  "bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
43
52
  ]),
44
53
  patchedBlocks: Object.freeze([
45
- "afc18cc7d56cce6dd6f3bade8661268a534f2de93de5ba638f6609d2638c7436",
54
+ "f0b5585d2ff0d403e5c4d35e9bd0b8cbfec5c41a9b9057b8cc808107e889cf46",
46
55
  "0a4832413d8d8cdda67c8bb318c0e5c25df20c75802d637097ffd045a31cd443",
47
56
  "bcc10582d8a74717f8f81b98182d3551f1eaf23eb9cadd908d160ce5e4ef8cba",
48
57
  ]),
58
+ edit: Object.freeze({
59
+ offset: 1_120_359,
60
+ absoluteOffset: 106_574_750,
61
+ length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
62
+ anchor: "47024(I,e,t){",
63
+ anchorCount: 1,
64
+ sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
65
+ patchedSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
66
+ replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
67
+ }),
49
68
  }),
69
+ ]),
70
+ legacy: Object.freeze({
71
+ kind: "global-origin-replacement",
72
+ asarSha256: "3dd0f53cdaa35a644d2cf56e4fc2dd20f5c90dc2989b6d81a467ef00ebb620a7",
73
+ headerSha256: "86ed1113a563b82ecec05de89aab87a5fd1666f35bee76e2d76e2ed5f6dcb3fa",
74
+ }),
75
+ });
76
+
77
+ export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
78
+ id: "pm-studio-2.9.10-build-2.9.10",
79
+ version: "2.9.10",
80
+ build: "2.9.10",
81
+ bundleIdentifier: "com.pm-studio.app",
82
+ sourceTeamIdentifier: "HL75GKK4W4",
83
+ sourceArtifact: Object.freeze({
84
+ releaseUrl: "https://github.com/gim-home/max-studio/releases/tag/v2.9.10",
85
+ asset: "PM-Studio-2.9.10-mac-arm64.zip",
86
+ sha256: "85654e6ed173ce2565b5ef3694137de2c5f92eba1b749316c9e5b63181ccc3b0",
87
+ }),
88
+ sourceBundleContent: Object.freeze({
89
+ scheme: "ccdx-bundle-content-v2",
90
+ sha256: "478ca7f1f0826b07b7706fd1f410dee01b6dbaae21c40c63ecbdc0942eab63d9",
91
+ entryCount: 1_521,
92
+ regularFileCount: 1_013,
93
+ regularBytes: 494_995_602,
94
+ symlinkCount: 14,
95
+ xattrCount: 735,
96
+ ignoredXattrs: Object.freeze([
97
+ "com.apple.macl",
98
+ "com.apple.provenance",
99
+ "com.apple.quarantine",
100
+ ]),
101
+ }),
102
+ sourceCodeSignature: Object.freeze({
103
+ identifier: "com.pm-studio.app",
104
+ teamIdentifier: "HL75GKK4W4",
105
+ flags: "0x10000(runtime)",
106
+ runtimeVersion: "26.0.0",
107
+ cdHashFull: "86a4d64946123d21d20918041e88f58e8c497ce1a1f75a9f9d8cd592ce86e158",
108
+ notarizationTicket: "stapled",
109
+ }),
110
+ patchedSigningMetadata: Object.freeze({
111
+ identifier: "com.pm-studio.app",
112
+ flags: "0x10002(adhoc,runtime)",
113
+ runtime_version: "26.0.0",
114
+ entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
115
+ }),
116
+ executable: "PM Studio",
117
+ sourceExecutableSha256: "a38f2054abb9770ba35d054d447be0de8e7c9d4fed6b37486bf15cf28de1e122",
118
+ electronFrameworkPath: "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
119
+ sourceElectronFrameworkSha256: "a562a31f8f8d14440974466e5e611b19d93705dcf3ca2f57647578ffc002f5b4",
120
+ embeddedAsarIntegrity: "absent",
121
+ asarPath: "Contents/Resources/app.asar",
122
+ infoPlistPath: "Contents/Info.plist",
123
+ integrityKey: "ElectronAsarIntegrity.Resources/app.asar",
124
+ dataOffset: 3_833_292,
125
+ sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
126
+ sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
127
+ patchedAsarSha256: "ea28d998056b32ca4115d208a4d81ce629c83f3f5f89c6a66d50749849beb6fc",
128
+ patchedHeaderSha256: "348d80f5f2d1ebb92e6089195b1703b87467678f80d8946b8ab83b3ead03e5be",
129
+ targets: Object.freeze([
50
130
  Object.freeze({
51
- path: "dist/renderer/js/main.09f18d95.js",
52
- offset: 137_002_230,
53
- size: 2_469_533,
54
- sentinelOffset: 489_935,
55
- absoluteSentinelOffset: 141_324_929,
56
- sentinelCount: 1,
131
+ path: "dist/main/main.js",
132
+ offset: 101_622_115,
133
+ size: 10_150_157,
57
134
  blockSize: 4_194_304,
58
- sourceSha256: "eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
59
- patchedSha256: "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
135
+ sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
136
+ patchedSha256: "b5b5e37af3d06ffc603bdef620fc1e0254580f0ee325f1a77096af6c78379b79",
60
137
  sourceBlocks: Object.freeze([
61
- "eb63abf35be3db101da179789e8ffdcc5b67131258ac8d7976109713ff0cc994",
138
+ "3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
139
+ "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
140
+ "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
62
141
  ]),
63
142
  patchedBlocks: Object.freeze([
64
- "89bbea9516a914da3fa4f32befd33d0dbb0e63b09545e4d85392bccb52adc145",
143
+ "63aa15d8c44614a7b2d9656ddba23109d40bab66b66b36339154953bd164da75",
144
+ "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
145
+ "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
65
146
  ]),
147
+ edit: Object.freeze({
148
+ offset: 1_241_223,
149
+ absoluteOffset: 106_696_630,
150
+ length: PM_STUDIO_COPILOT_CONFIG_MODULE_LENGTH,
151
+ anchor: "47024(I,e,t){",
152
+ anchorCount: 1,
153
+ sourceSha256: PM_STUDIO_COPILOT_CONFIG_MODULE_SOURCE_SHA256,
154
+ patchedSha256: "a5b9312ed78e8a7fc3bd38f7ea5d4ab008f21bae18ba3dd6aa2d4be6b69191db",
155
+ replacement: PM_STUDIO_SPLIT_ORIGIN_CONFIG_MODULE,
156
+ }),
66
157
  }),
67
158
  ]),
159
+ legacy: Object.freeze({
160
+ kind: "global-origin-replacement",
161
+ asarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
162
+ headerSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
163
+ }),
68
164
  });
69
165
 
70
- export const PM_STUDIO_RECIPES = Object.freeze([PM_STUDIO_2_9_7_RECIPE]);
166
+ export const PM_STUDIO_RECIPES = Object.freeze([
167
+ PM_STUDIO_2_9_7_RECIPE,
168
+ PM_STUDIO_2_9_10_RECIPE,
169
+ ]);
71
170
 
72
171
  export function sha256Hex(value) {
73
172
  return createHash("sha256").update(value).digest("hex");
@@ -235,7 +334,7 @@ function addMismatch(issues, label, actual, expected) {
235
334
  if (actual !== expected) issues.push(`${label}: expected ${expected}, got ${actual}`);
236
335
  }
237
336
 
238
- function inspectTarget(parsed, target, recipe) {
337
+ function inspectTarget(parsed, target) {
239
338
  const entry = findAsarEntry(parsed.header, target.path);
240
339
  if (!entry) return { path: target.path, missing: true };
241
340
 
@@ -253,8 +352,28 @@ function inspectTarget(parsed, target, recipe) {
253
352
  }
254
353
 
255
354
  const bytes = parsed.buffer.subarray(absoluteOffset, absoluteOffset + size);
256
- const sourceSentinel = Buffer.from(recipe.sourceSentinel);
257
- const patchedSentinel = Buffer.from(recipe.patchedSentinel);
355
+ let edit = null;
356
+ if (target.edit) {
357
+ let editOffset;
358
+ let editLength;
359
+ try {
360
+ editOffset = integer(target.edit.offset, `${target.path} edit offset`);
361
+ editLength = integer(target.edit.length, `${target.path} edit length`);
362
+ } catch (error) {
363
+ return { path: target.path, offset, size, absoluteOffset, malformed: error.message };
364
+ }
365
+ if (editLength === 0 || editOffset + editLength > bytes.length) {
366
+ return { path: target.path, offset, size, absoluteOffset, malformed: "edit window is outside the target" };
367
+ }
368
+ const anchor = Buffer.from(target.edit.anchor || "");
369
+ edit = {
370
+ offset: editOffset,
371
+ absoluteOffset: absoluteOffset + editOffset,
372
+ length: editLength,
373
+ sha256: sha256Hex(bytes.subarray(editOffset, editOffset + editLength)),
374
+ anchorPositions: occurrencePositions(bytes, anchor),
375
+ };
376
+ }
258
377
  return {
259
378
  path: target.path,
260
379
  offset,
@@ -264,20 +383,17 @@ function inspectTarget(parsed, target, recipe) {
264
383
  blockSize: entry.integrity?.blockSize,
265
384
  blocks: blockSha256(bytes, target.blockSize),
266
385
  integrity: entry.integrity || null,
267
- sourceSentinelPositions: occurrencePositions(bytes, sourceSentinel),
268
- patchedSentinelPositions: occurrencePositions(bytes, patchedSentinel),
386
+ edit,
269
387
  };
270
388
  }
271
389
 
272
- function targetStateIssues(detail, target, recipe, state) {
390
+ function targetStateIssues(detail, target, state) {
273
391
  const issues = [];
274
392
  if (detail.missing) return [`${target.path}: entry is missing`];
275
393
  if (detail.malformed) return [`${target.path}: ${detail.malformed}`];
276
394
 
277
395
  addMismatch(issues, `${target.path} offset`, detail.offset, target.offset);
278
396
  addMismatch(issues, `${target.path} size`, detail.size, target.size);
279
- addMismatch(issues, `${target.path} absolute sentinel offset`,
280
- detail.absoluteOffset + target.sentinelOffset, target.absoluteSentinelOffset);
281
397
  addMismatch(issues, `${target.path} integrity algorithm`,
282
398
  detail.integrity?.algorithm, "SHA256");
283
399
  addMismatch(issues, `${target.path} block size`, detail.integrity?.blockSize, target.blockSize);
@@ -285,20 +401,23 @@ function targetStateIssues(detail, target, recipe, state) {
285
401
  const patched = state === "patched";
286
402
  const expectedHash = patched ? target.patchedSha256 : target.sourceSha256;
287
403
  const expectedBlocks = patched ? target.patchedBlocks : target.sourceBlocks;
288
- const expectedSentinelPositions = [target.sentinelOffset];
289
- const presentPositions = patched ? detail.patchedSentinelPositions : detail.sourceSentinelPositions;
290
- const absentPositions = patched ? detail.sourceSentinelPositions : detail.patchedSentinelPositions;
291
404
 
292
405
  addMismatch(issues, `${target.path} SHA-256`, detail.sha256, expectedHash);
293
406
  addMismatch(issues, `${target.path} integrity hash`, detail.integrity?.hash, expectedHash);
294
407
  if (!sameArray(detail.blocks, expectedBlocks)) issues.push(`${target.path} computed block hashes do not match ${state} recipe`);
295
408
  if (!sameArray(detail.integrity?.blocks, expectedBlocks)) issues.push(`${target.path} header block hashes do not match ${state} recipe`);
296
- if (!sameArray(presentPositions, expectedSentinelPositions)) {
297
- issues.push(`${target.path} ${state} sentinel position/count does not match recipe`);
298
- }
299
- if (absentPositions.length !== 0) issues.push(`${target.path} contains the opposite-state sentinel`);
300
- if (presentPositions.length !== target.sentinelCount) {
301
- issues.push(`${target.path} ${state} sentinel count is ${presentPositions.length}, expected ${target.sentinelCount}`);
409
+ if (target.edit) {
410
+ addMismatch(issues, `${target.path} edit offset`, detail.edit?.offset, target.edit.offset);
411
+ addMismatch(issues, `${target.path} absolute edit offset`, detail.edit?.absoluteOffset, target.edit.absoluteOffset);
412
+ addMismatch(issues, `${target.path} edit length`, detail.edit?.length, target.edit.length);
413
+ addMismatch(issues, `${target.path} edit SHA-256`, detail.edit?.sha256,
414
+ patched ? target.edit.patchedSha256 : target.edit.sourceSha256);
415
+ if (!sameArray(detail.edit?.anchorPositions, [target.edit.offset])) {
416
+ issues.push(`${target.path} module anchor position/count does not match recipe`);
417
+ }
418
+ if (detail.edit?.anchorPositions?.length !== target.edit.anchorCount) {
419
+ issues.push(`${target.path} module anchor count is ${detail.edit?.anchorPositions?.length || 0}, expected ${target.edit.anchorCount}`);
420
+ }
302
421
  }
303
422
  return issues;
304
423
  }
@@ -307,7 +426,7 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
307
426
  const parsed = parseAsarBuffer(value);
308
427
  const asarSha256 = sha256Hex(parsed.buffer);
309
428
  const headerSha256 = sha256Hex(parsed.headerBytes);
310
- const targets = recipe.targets.map((target) => inspectTarget(parsed, target, recipe));
429
+ const targets = recipe.targets.map((target) => inspectTarget(parsed, target));
311
430
  const commonIssues = [];
312
431
  addMismatch(commonIssues, "ASAR data offset", parsed.dataOffset, recipe.dataOffset);
313
432
 
@@ -319,14 +438,25 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
319
438
  addMismatch(issues, "ASAR header SHA-256", headerSha256,
320
439
  patched ? recipe.patchedHeaderSha256 : recipe.sourceHeaderSha256);
321
440
  for (let index = 0; index < recipe.targets.length; index += 1) {
322
- issues.push(...targetStateIssues(targets[index], recipe.targets[index], recipe, state));
441
+ issues.push(...targetStateIssues(targets[index], recipe.targets[index], state));
323
442
  }
324
443
  return issues;
325
444
  };
326
445
 
327
446
  const cleanIssues = issuesFor("clean");
328
447
  const patchedIssues = issuesFor("patched");
329
- const state = cleanIssues.length === 0 ? "clean" : patchedIssues.length === 0 ? "patched" : "drift";
448
+ const legacyIssues = [...commonIssues];
449
+ if (!recipe.legacy) {
450
+ legacyIssues.push("recipe has no recognized legacy patch");
451
+ } else {
452
+ addMismatch(legacyIssues, "ASAR SHA-256", asarSha256, recipe.legacy.asarSha256);
453
+ addMismatch(legacyIssues, "ASAR header SHA-256", headerSha256, recipe.legacy.headerSha256);
454
+ }
455
+ const state = cleanIssues.length === 0
456
+ ? "clean"
457
+ : patchedIssues.length === 0
458
+ ? "patched"
459
+ : legacyIssues.length === 0 ? "legacy" : "drift";
330
460
  return {
331
461
  recipeId: recipe.id,
332
462
  state,
@@ -336,19 +466,23 @@ export function inspectAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
336
466
  targets,
337
467
  cleanIssues,
338
468
  patchedIssues,
469
+ legacyIssues,
339
470
  };
340
471
  }
341
472
 
342
473
  export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
343
- if (Buffer.byteLength(recipe.sourceSentinel) !== Buffer.byteLength(recipe.patchedSentinel)) {
344
- throw new Error("PM Studio patch requires equal-length origin sentinels");
345
- }
346
474
  const before = inspectAsarBuffer(value, recipe);
347
475
  if (before.state === "patched") {
348
476
  return { changed: false, buffer: Buffer.from(value), before, after: before };
349
477
  }
478
+ if (before.state === "legacy") {
479
+ const error = new Error("Legacy PM Studio global-origin patch must be migrated from a verified clean backup");
480
+ error.code = "PM_STUDIO_ASAR_LEGACY";
481
+ error.inspection = before;
482
+ throw error;
483
+ }
350
484
  if (before.state !== "clean") {
351
- const error = new Error("PM Studio ASAR does not match the clean or patched recipe");
485
+ const error = new Error("PM Studio ASAR does not match the clean, split-origin, or legacy recipe");
352
486
  error.code = "PM_STUDIO_ASAR_DRIFT";
353
487
  error.inspection = before;
354
488
  throw error;
@@ -356,16 +490,25 @@ export function patchAsarBuffer(value, recipe = PM_STUDIO_2_9_7_RECIPE) {
356
490
 
357
491
  const output = Buffer.from(value);
358
492
  const parsed = parseAsarBuffer(output);
359
- const sourceSentinel = Buffer.from(recipe.sourceSentinel);
360
- const patchedSentinel = Buffer.from(recipe.patchedSentinel);
361
493
 
362
494
  for (const target of recipe.targets) {
363
495
  const entry = findAsarEntry(parsed.header, target.path);
364
496
  const offset = entryOffset(entry.offset, `${target.path} offset`);
365
- const absoluteSentinelOffset = parsed.dataOffset + offset + target.sentinelOffset;
366
- const existing = output.subarray(absoluteSentinelOffset, absoluteSentinelOffset + sourceSentinel.length);
367
- if (!existing.equals(sourceSentinel)) throw new Error(`${target.path} source sentinel moved before patching`);
368
- patchedSentinel.copy(output, absoluteSentinelOffset);
497
+ const edit = target.edit;
498
+ if (!edit) throw new Error(`${target.path} has no split-origin edit recipe`);
499
+ const replacement = Buffer.from(edit.replacement);
500
+ if (replacement.length !== edit.length || sha256Hex(replacement) !== edit.patchedSha256) {
501
+ throw new Error(`${target.path} split-origin replacement does not match its recipe`);
502
+ }
503
+ const absoluteEditOffset = parsed.dataOffset + offset + edit.offset;
504
+ if (absoluteEditOffset !== edit.absoluteOffset) {
505
+ throw new Error(`${target.path} split-origin edit moved before patching`);
506
+ }
507
+ const existing = output.subarray(absoluteEditOffset, absoluteEditOffset + edit.length);
508
+ if (sha256Hex(existing) !== edit.sourceSha256) {
509
+ throw new Error(`${target.path} source edit window does not match its recipe`);
510
+ }
511
+ replacement.copy(output, absoluteEditOffset);
369
512
  entry.integrity = {
370
513
  ...entry.integrity,
371
514
  algorithm: "SHA256",