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 +47 -21
- package/package.json +1 -1
- package/src/cli-status.mjs +2 -2
- package/src/config.mjs +18 -0
- package/src/observability.mjs +0 -4
- package/src/pm-studio-asar.mjs +193 -50
- package/src/pm-studio-relay.mjs +40 -30
- package/src/pm-studio-setup.mjs +398 -54
- package/src/pm-studio-status.mjs +67 -7
- package/src/responses-request.mjs +16 -2
- package/src/running-adapter.mjs +3 -1
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
174
|
-
complete vendor
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
package/src/cli-status.mjs
CHANGED
|
@@ -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"
|
|
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)}
|
|
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"
|
package/src/observability.mjs
CHANGED
|
@@ -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
|
|
package/src/pm-studio-asar.mjs
CHANGED
|
@@ -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: "
|
|
25
|
-
patchedHeaderSha256: "
|
|
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: "
|
|
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
|
-
"
|
|
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/
|
|
52
|
-
offset:
|
|
53
|
-
size:
|
|
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: "
|
|
59
|
-
patchedSha256: "
|
|
135
|
+
sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
|
|
136
|
+
patchedSha256: "b5b5e37af3d06ffc603bdef620fc1e0254580f0ee325f1a77096af6c78379b79",
|
|
60
137
|
sourceBlocks: Object.freeze([
|
|
61
|
-
"
|
|
138
|
+
"3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
|
|
139
|
+
"49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
|
|
140
|
+
"bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
|
|
62
141
|
]),
|
|
63
142
|
patchedBlocks: Object.freeze([
|
|
64
|
-
"
|
|
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([
|
|
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
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
268
|
-
patchedSentinelPositions: occurrencePositions(bytes, patchedSentinel),
|
|
386
|
+
edit,
|
|
269
387
|
};
|
|
270
388
|
}
|
|
271
389
|
|
|
272
|
-
function targetStateIssues(detail, target,
|
|
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 (
|
|
297
|
-
issues
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
|
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],
|
|
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
|
|
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
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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",
|