codex-copilot-dx 0.6.7 → 0.6.8

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
@@ -116,9 +116,11 @@ active account or profile. GPT requests continue to use the bearer supplied by
116
116
  the current PM profile; exact enabled Anthropic model IDs use the isolated
117
117
  CCDX Claude profile. Neither credential is copied into the other application.
118
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:
119
+ The current patch recipes support only the exact, unmodified PM Studio 2.9.7
120
+ and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
121
+ 2.9.11 and other unknown builds remain unsupported until they receive their own
122
+ verified recipe. Authorize the isolated Claude profile first, quit PM Studio
123
+ and its updater, then run the one-time setup command:
122
124
 
123
125
  ```bash
124
126
  ccdx auth login claude --reauth --github-login <personal-github-login>
@@ -145,22 +147,31 @@ does not attempt a second automatic write: it preserves the verified backup,
145
147
  retains any diagnostic stage still present, and prints recovery paths. Repeating
146
148
  setup for the exact installed recipe is idempotent.
147
149
 
150
+ The official 2.9.10 artifact can fail macOS's vendor-signature verifier even
151
+ though its signing metadata remains readable. CCDX does not generally ignore
152
+ that failure: the 2.9.10 exception is accepted only when the complete App tree,
153
+ including every file, mode, symlink, and non-volatile extended attribute,
154
+ matches the frozen official fingerprint. The staged and installed CCDX patch
155
+ must still pass strict ad-hoc signature verification and match the complete
156
+ patched-tree record in the backup manifest.
157
+
148
158
  The patch is App-wide: every PM profile sends its Copilot API traffic through
149
159
  the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
150
160
  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
161
+ relay explicitly supports only the Copilot paths discovered in the validated
162
+ 2.9.7 and 2.9.10 bundles (`GET /models` and `POST /chat/completions`, `/responses`, and
153
163
  `/embeddings`) and is not a general-purpose proxy.
154
164
 
155
165
  Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
156
166
  updater is not disabled; an official update may overwrite the patch or may
157
167
  require reinstalling the official App. Setup prints the verified backup and
158
168
  manifest paths plus the version-matched restore procedure. Never restore a
159
- 2.9.7 backup over a newer PM Studio installation.
169
+ backup over a different PM Studio version or build.
160
170
 
161
171
  To restore, first quit PM Studio and its updater. Use the exact verified backup
162
172
  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`:
173
+ and backup still report the same version/build printed by setup (`2.9.7/2.9.7`
174
+ or `2.9.10/2.9.10`):
164
175
 
165
176
  ```bash
166
177
  BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
@@ -170,8 +181,8 @@ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
170
181
  /usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
171
182
  ```
172
183
 
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:
184
+ Only when all four values match the one exact recipe, move the patched App
185
+ aside, restore the complete vendor bundle, and verify it before launch:
175
186
 
176
187
  ```bash
177
188
  RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
@@ -179,12 +190,16 @@ PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
179
190
  mv "/Applications/PM Studio.app" "$PATCHED_APP"
180
191
  /usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
181
192
  /usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
193
+ ccdx pms status
182
194
  ```
183
195
 
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.
196
+ For 2.9.10, the vendor `codesign` command may report the documented upstream
197
+ signature failure; in that case the App-patch row from `ccdx pms status` must
198
+ still classify the restored bundle as exact supported clean content rather than
199
+ integrity drift. If the version check, copy, or both verification paths fail,
200
+ do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
201
+ and reinstall the official PM Studio instead of applying a version-mismatched
202
+ backup. CCDX never runs these restore commands or `sudo` automatically.
188
203
 
189
204
  To change the model used by Codex Auto-review, run:
190
205
 
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.8",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
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"
@@ -67,7 +67,106 @@ export const PM_STUDIO_2_9_7_RECIPE = Object.freeze({
67
67
  ]),
68
68
  });
69
69
 
70
- export const PM_STUDIO_RECIPES = Object.freeze([PM_STUDIO_2_9_7_RECIPE]);
70
+ export const PM_STUDIO_2_9_10_RECIPE = Object.freeze({
71
+ id: "pm-studio-2.9.10-build-2.9.10",
72
+ version: "2.9.10",
73
+ build: "2.9.10",
74
+ bundleIdentifier: "com.pm-studio.app",
75
+ sourceTeamIdentifier: "HL75GKK4W4",
76
+ sourceArtifact: Object.freeze({
77
+ releaseUrl: "https://github.com/gim-home/max-studio/releases/tag/v2.9.10",
78
+ asset: "PM-Studio-2.9.10-mac-arm64.zip",
79
+ sha256: "85654e6ed173ce2565b5ef3694137de2c5f92eba1b749316c9e5b63181ccc3b0",
80
+ }),
81
+ sourceBundleContent: Object.freeze({
82
+ scheme: "ccdx-bundle-content-v2",
83
+ sha256: "478ca7f1f0826b07b7706fd1f410dee01b6dbaae21c40c63ecbdc0942eab63d9",
84
+ entryCount: 1_521,
85
+ regularFileCount: 1_013,
86
+ regularBytes: 494_995_602,
87
+ symlinkCount: 14,
88
+ xattrCount: 735,
89
+ ignoredXattrs: Object.freeze([
90
+ "com.apple.macl",
91
+ "com.apple.provenance",
92
+ "com.apple.quarantine",
93
+ ]),
94
+ }),
95
+ sourceCodeSignature: Object.freeze({
96
+ identifier: "com.pm-studio.app",
97
+ teamIdentifier: "HL75GKK4W4",
98
+ flags: "0x10000(runtime)",
99
+ runtimeVersion: "26.0.0",
100
+ cdHashFull: "86a4d64946123d21d20918041e88f58e8c497ce1a1f75a9f9d8cd592ce86e158",
101
+ notarizationTicket: "stapled",
102
+ }),
103
+ patchedSigningMetadata: Object.freeze({
104
+ identifier: "com.pm-studio.app",
105
+ flags: "0x10002(adhoc,runtime)",
106
+ runtime_version: "26.0.0",
107
+ entitlements_sha256: "9d4ccbda4fe0c81a70df3db93b3e61fe0500f67f14cdcbee4dea230e6512d05c",
108
+ }),
109
+ executable: "PM Studio",
110
+ sourceExecutableSha256: "a38f2054abb9770ba35d054d447be0de8e7c9d4fed6b37486bf15cf28de1e122",
111
+ electronFrameworkPath: "Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework",
112
+ sourceElectronFrameworkSha256: "a562a31f8f8d14440974466e5e611b19d93705dcf3ca2f57647578ffc002f5b4",
113
+ embeddedAsarIntegrity: "absent",
114
+ asarPath: "Contents/Resources/app.asar",
115
+ infoPlistPath: "Contents/Info.plist",
116
+ integrityKey: "ElectronAsarIntegrity.Resources/app.asar",
117
+ dataOffset: 3_833_292,
118
+ sourceAsarSha256: "d243860770e8b1d8044213924f9704d1fc52f900d6c33461eff6358962330b78",
119
+ sourceHeaderSha256: "5fc77d5b61e9647fba10fd2bd5752d0889c14da5a00b21d31bc48fdcccf74a69",
120
+ patchedAsarSha256: "49f72d999a6085102341c2d551577e164db6f22e4707d2112efa3fd280e7315e",
121
+ patchedHeaderSha256: "b264ece5b8e469cd3885a7a91f904f1902e1991f214ecbf228dfe91059ac7008",
122
+ sourceSentinel: PM_STUDIO_ORIGIN,
123
+ patchedSentinel: CCDX_PM_STUDIO_ORIGIN,
124
+ targets: Object.freeze([
125
+ Object.freeze({
126
+ path: "dist/main/main.js",
127
+ offset: 101_622_115,
128
+ size: 10_150_157,
129
+ sentinelOffset: 1_241_344,
130
+ absoluteSentinelOffset: 106_696_751,
131
+ sentinelCount: 1,
132
+ blockSize: 4_194_304,
133
+ sourceSha256: "03607bb1edc068396b99f02b9c9e0d8e10388fb658afc112e06e875c123dbc27",
134
+ patchedSha256: "e400300575e2d1c5ad089613f14e138612114547ff5060e94cb81e8de587a75a",
135
+ sourceBlocks: Object.freeze([
136
+ "3910a9bc5459f493956aafee7b3644e8d242027758f0dcdea97d7c10a324c6bf",
137
+ "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
138
+ "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
139
+ ]),
140
+ patchedBlocks: Object.freeze([
141
+ "74022f5ad06c7879b37875efe0a090b23ccfbcf269afd26b10b77690413dca63",
142
+ "49fa64c68425d35b1baff0d5745d856d377e014a7a7829322cd1b39aade9532e",
143
+ "bd7cc14a1d2e02abe66c0ec6fc4e97b263cfc1f67c4ae39e956c06d3bcc2fe67",
144
+ ]),
145
+ }),
146
+ Object.freeze({
147
+ path: "dist/renderer/js/main.5ad30c24.js",
148
+ offset: 137_266_992,
149
+ size: 2_520_547,
150
+ sentinelOffset: 528_919,
151
+ absoluteSentinelOffset: 141_629_203,
152
+ sentinelCount: 1,
153
+ blockSize: 4_194_304,
154
+ sourceSha256: "3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
155
+ patchedSha256: "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
156
+ sourceBlocks: Object.freeze([
157
+ "3d123f7a3eb91777f723fe2c4d412677e6722b6348373e4b4bfda41dda8b60ad",
158
+ ]),
159
+ patchedBlocks: Object.freeze([
160
+ "dc4a9d550f4beb3a2cd7ad0fa6fc82286106762bdf789189d8f88abd0a90f619",
161
+ ]),
162
+ }),
163
+ ]),
164
+ });
165
+
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");
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { randomUUID } from "node:crypto";
4
+ import { createHash, randomUUID } from "node:crypto";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import {
7
7
  AUTH_PROFILE_CLAUDE,
@@ -19,6 +19,7 @@ import {
19
19
  export const DEFAULT_PM_STUDIO_APP_PATH = "/Applications/PM Studio.app";
20
20
  export const PM_STUDIO_CLAUDE_AUTH_COMMAND = "ccdx auth login claude --reauth --github-login <personal-login>";
21
21
  const MIN_FREE_MARGIN_BYTES = 16 * 1024 * 1024;
22
+ const BUNDLE_CONTENT_SCHEME = "ccdx-bundle-content-v2";
22
23
 
23
24
  function setupError(code, message, details) {
24
25
  const error = new Error(message);
@@ -31,7 +32,7 @@ function defaultProcessRunner(command, args) {
31
32
  return spawnSync(command, args, {
32
33
  encoding: "utf8",
33
34
  windowsHide: true,
34
- maxBuffer: 8 * 1024 * 1024,
35
+ maxBuffer: 32 * 1024 * 1024,
35
36
  });
36
37
  }
37
38
 
@@ -53,6 +54,166 @@ function runChecked(processRunner, command, args, label) {
53
54
  return result;
54
55
  }
55
56
 
57
+ function hashRegularFile(filePath, expected) {
58
+ const hash = createHash("sha256");
59
+ const file = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
60
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
61
+ try {
62
+ const before = fs.fstatSync(file, { bigint: true });
63
+ if (before.dev !== BigInt(expected.dev)
64
+ || before.ino !== BigInt(expected.ino)
65
+ || before.size !== BigInt(expected.size)) {
66
+ throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while reading ${filePath}`);
67
+ }
68
+ for (;;) {
69
+ const length = fs.readSync(file, buffer, 0, buffer.length, null);
70
+ if (length === 0) break;
71
+ hash.update(buffer.subarray(0, length));
72
+ }
73
+ const after = fs.fstatSync(file, { bigint: true });
74
+ if (before.dev !== after.dev
75
+ || before.ino !== after.ino
76
+ || before.size !== after.size
77
+ || before.mtimeNs !== after.mtimeNs
78
+ || before.ctimeNs !== after.ctimeNs) {
79
+ throw setupError("PM_STUDIO_BUNDLE_CHANGED", `PM Studio bundle changed while hashing ${filePath}`);
80
+ }
81
+ } finally {
82
+ fs.closeSync(file);
83
+ }
84
+ return hash.digest("hex");
85
+ }
86
+
87
+ function parseXattrHex(output, root, ignoredXattrs) {
88
+ const records = [];
89
+ const ignored = new Set(ignoredXattrs);
90
+ const seen = new Set();
91
+ let current = null;
92
+ const flush = () => {
93
+ if (!current) return;
94
+ if (!ignored.has(current.name)) {
95
+ const key = `${current.path}\0${current.name}`;
96
+ if (seen.has(key)) throw setupError("PM_STUDIO_XATTR_INVALID", `Duplicate extended attribute ${current.name}`);
97
+ seen.add(key);
98
+ const value = Buffer.from(current.hex.join(""), "hex");
99
+ records.push(["xattr", current.path, current.name, value.length, sha256Hex(value)]);
100
+ }
101
+ current = null;
102
+ };
103
+
104
+ for (const line of String(output || "").split(/\r?\n/)) {
105
+ if (!line) continue;
106
+ const separator = line.lastIndexOf(": ");
107
+ if ((line === `${root}:` || line.startsWith(`${root}: `) || line.startsWith(`${root}${path.sep}`))
108
+ && separator >= root.length && line.endsWith(":")) {
109
+ flush();
110
+ const target = line.slice(0, separator);
111
+ const relative = target === root ? "." : path.relative(root, target).split(path.sep).join("/");
112
+ if (!relative || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) {
113
+ throw setupError("PM_STUDIO_XATTR_INVALID", `Extended attribute path escapes PM Studio: ${target}`);
114
+ }
115
+ current = { path: relative, name: line.slice(separator + 2, -1), hex: [] };
116
+ continue;
117
+ }
118
+ if (!current) throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attributes");
119
+ const data = line.includes(" |") ? line.slice(0, line.indexOf(" |")) : line;
120
+ const tokens = data.trim().split(/\s+/);
121
+ if (!/^[0-9a-f]{8}$/i.test(tokens[0]) || tokens.slice(1).some((token) => !/^[0-9a-f]{2}$/i.test(token))) {
122
+ throw setupError("PM_STUDIO_XATTR_INVALID", "Could not parse PM Studio extended attribute bytes");
123
+ }
124
+ current.hex.push(...tokens.slice(1));
125
+ }
126
+ flush();
127
+ return records.sort((left, right) => Buffer.compare(
128
+ Buffer.from(`${left[1]}\0${left[2]}`),
129
+ Buffer.from(`${right[1]}\0${right[2]}`),
130
+ ));
131
+ }
132
+
133
+ export function inspectBundleContent(appPath, {
134
+ xattrOutput = "",
135
+ ignoredXattrs = [],
136
+ } = {}) {
137
+ const root = path.resolve(appPath);
138
+ const rootStat = fs.lstatSync(root);
139
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
140
+ throw setupError("PM_STUDIO_APP_INVALID", `PM Studio path is not a regular app bundle: ${appPath}`);
141
+ }
142
+ const records = [];
143
+ const normalizedPaths = new Map();
144
+ let regularFileCount = 0;
145
+ let regularBytes = 0;
146
+ let symlinkCount = 0;
147
+
148
+ const rememberPath = (relative) => {
149
+ const normalized = relative.normalize("NFC");
150
+ const existing = normalizedPaths.get(normalized);
151
+ if (existing && existing !== relative) {
152
+ throw setupError("PM_STUDIO_BUNDLE_PATH_COLLISION", `PM Studio contains colliding paths: ${existing} and ${relative}`);
153
+ }
154
+ normalizedPaths.set(normalized, relative);
155
+ };
156
+ rememberPath(".");
157
+ records.push(["directory", ".", rootStat.mode & 0o7777]);
158
+ const visit = (directory) => {
159
+ const names = fs.readdirSync(directory).sort((left, right) =>
160
+ Buffer.compare(Buffer.from(left), Buffer.from(right)));
161
+ for (const name of names) {
162
+ const fullPath = path.join(directory, name);
163
+ const stat = fs.lstatSync(fullPath);
164
+ const relative = path.relative(root, fullPath).split(path.sep).join("/");
165
+ const mode = stat.mode & 0o7777;
166
+ rememberPath(relative);
167
+ if (stat.isDirectory() && !stat.isSymbolicLink()) {
168
+ records.push(["directory", relative, mode]);
169
+ visit(fullPath);
170
+ } else if (stat.isSymbolicLink()) {
171
+ const target = fs.readlinkSync(fullPath);
172
+ const resolved = path.resolve(path.dirname(fullPath), target);
173
+ if (path.isAbsolute(target)
174
+ || (resolved !== root && !resolved.startsWith(`${root}${path.sep}`))
175
+ || !fs.existsSync(resolved)) {
176
+ throw setupError("PM_STUDIO_BUNDLE_SYMLINK_INVALID", `PM Studio contains an unsafe symlink: ${relative}`);
177
+ }
178
+ symlinkCount += 1;
179
+ records.push(["symlink", relative, mode, target]);
180
+ } else if (stat.isFile()) {
181
+ if (stat.nlink !== 1) {
182
+ throw setupError("PM_STUDIO_BUNDLE_HARDLINK_INVALID", `PM Studio contains a hard-linked file: ${relative}`);
183
+ }
184
+ regularFileCount += 1;
185
+ regularBytes += stat.size;
186
+ records.push(["file", relative, mode, stat.size, hashRegularFile(fullPath, stat)]);
187
+ } else {
188
+ throw setupError("PM_STUDIO_BUNDLE_ENTRY_INVALID", `PM Studio contains an unsupported entry: ${relative}`);
189
+ }
190
+ }
191
+ };
192
+ visit(root);
193
+
194
+ const xattrRecords = parseXattrHex(xattrOutput, root, ignoredXattrs);
195
+ const hash = createHash("sha256");
196
+ for (const record of [...records, ...xattrRecords]) hash.update(`${JSON.stringify(record)}\n`);
197
+ return {
198
+ scheme: BUNDLE_CONTENT_SCHEME,
199
+ sha256: hash.digest("hex"),
200
+ entryCount: records.length,
201
+ regularFileCount,
202
+ regularBytes,
203
+ symlinkCount,
204
+ xattrCount: xattrRecords.length,
205
+ };
206
+ }
207
+
208
+ function defaultInspectBundleContent({ appPath, recipe, processRunner }) {
209
+ const xattrs = runChecked(processRunner, "/usr/bin/xattr", ["-r", "-s", "-x", "-l", appPath],
210
+ "Reading PM Studio extended attributes");
211
+ return inspectBundleContent(appPath, {
212
+ xattrOutput: xattrs.stdout,
213
+ ignoredXattrs: recipe.sourceBundleContent?.ignoredXattrs || [],
214
+ });
215
+ }
216
+
56
217
  function plistRead(processRunner, plistPath, key) {
57
218
  return runChecked(processRunner, "/usr/bin/plutil", ["-extract", key, "raw", "-o", "-", plistPath],
58
219
  `Reading ${path.basename(plistPath)}`).stdout.trim();
@@ -91,30 +252,39 @@ function defaultInspectCodeSign({ appPath, processRunner }) {
91
252
  const verify = normalizedProcessResult(processRunner("/usr/bin/codesign", [
92
253
  "--verify", "--deep", "--strict", "--verbose=2", appPath,
93
254
  ]));
94
- if (verify.error || verify.status !== 0) return { valid: false, adHoc: false };
95
255
  const display = normalizedProcessResult(processRunner("/usr/bin/codesign", [
96
256
  "--display", "--verbose=4", appPath,
97
257
  ]));
98
- if (display.error || display.status !== 0) return { valid: false, adHoc: false };
99
258
  const details = `${display.stdout}\n${display.stderr}`;
100
259
  const entitlementsResult = normalizedProcessResult(processRunner("/usr/bin/codesign", [
101
260
  "--display", "--entitlements", "-", "--xml", appPath,
102
261
  ]));
103
- if (entitlementsResult.error || entitlementsResult.status !== 0) return { valid: false, adHoc: false };
104
262
  const entitlementOutput = `${entitlementsResult.stdout}\n${entitlementsResult.stderr}`;
105
263
  const plistStart = entitlementOutput.indexOf("<?xml");
106
264
  const plistEnd = entitlementOutput.lastIndexOf("</plist>");
107
- if (plistStart < 0 || plistEnd < plistStart) return { valid: false, adHoc: false };
108
- const entitlements = entitlementOutput.slice(plistStart, plistEnd + "</plist>".length);
265
+ const hasEntitlements = plistStart >= 0 && plistEnd >= plistStart;
266
+ const entitlements = hasEntitlements
267
+ ? entitlementOutput.slice(plistStart, plistEnd + "</plist>".length)
268
+ : "";
109
269
  return {
110
- valid: true,
270
+ valid: !verify.error && verify.status === 0
271
+ && !display.error && display.status === 0
272
+ && !entitlementsResult.error && entitlementsResult.status === 0
273
+ && hasEntitlements,
274
+ verifyValid: !verify.error && verify.status === 0,
275
+ displayValid: !display.error && display.status === 0,
276
+ entitlementsState: hasEntitlements
277
+ ? "xml"
278
+ : /invalid entitlements blob/i.test(entitlementOutput) ? "invalid" : "unavailable",
111
279
  adHoc: /(?:^|\n)Signature=adhoc(?:\n|$)/i.test(details)
112
280
  || /(?:^|\n)TeamIdentifier=not set(?:\n|$)/i.test(details),
113
281
  identifier: details.match(/(?:^|\n)Identifier=([^\n]+)/)?.[1]?.trim() || "",
114
282
  teamIdentifier: details.match(/(?:^|\n)TeamIdentifier=([^\n]+)/)?.[1]?.trim() || "",
115
283
  flags: details.match(/\bflags=([^\s]+)/)?.[1] || "",
116
284
  runtimeVersion: details.match(/(?:^|\n)Runtime Version=([^\n]+)/)?.[1]?.trim() || "",
117
- entitlementsSha256: sha256Hex(entitlements),
285
+ cdHashFull: details.match(/(?:^|\n)CandidateCDHashFull sha256=([0-9a-f]+)/i)?.[1]?.toLowerCase() || "",
286
+ notarizationTicket: details.match(/(?:^|\n)Notarization Ticket=([^\n]+)/)?.[1]?.trim() || "",
287
+ entitlementsSha256: hasEntitlements ? sha256Hex(entitlements) : "",
118
288
  };
119
289
  }
120
290
 
@@ -240,6 +410,7 @@ export function createPmStudioSetupOperations(overrides = {}) {
240
410
  readAsarIntegrity: defaultReadAsarIntegrity,
241
411
  writeAsarIntegrity: defaultWriteAsarIntegrity,
242
412
  inspectCodeSign: defaultInspectCodeSign,
413
+ inspectBundleContent: defaultInspectBundleContent,
243
414
  inspectExecutableIntegrity: defaultInspectExecutableIntegrity,
244
415
  signApp: defaultSignApp,
245
416
  listBlockingProcesses: defaultListBlockingProcesses,
@@ -271,12 +442,34 @@ function integrityMatches(actual, algorithm, hash) {
271
442
  return actual?.algorithm === algorithm && actual?.hash === hash;
272
443
  }
273
444
 
445
+ function bundleContentMatches(actual, expected) {
446
+ return actual?.scheme === expected?.scheme
447
+ && actual?.sha256 === expected?.sha256
448
+ && actual?.entryCount === expected?.entryCount
449
+ && actual?.regularFileCount === expected?.regularFileCount
450
+ && actual?.regularBytes === expected?.regularBytes
451
+ && actual?.symlinkCount === expected?.symlinkCount
452
+ && actual?.xattrCount === expected?.xattrCount;
453
+ }
454
+
455
+ function sourceCodeSignatureMatches(actual, expected) {
456
+ return actual?.adHoc === false
457
+ && actual?.displayValid === true
458
+ && actual?.identifier === expected?.identifier
459
+ && actual?.teamIdentifier === expected?.teamIdentifier
460
+ && actual?.flags === expected?.flags
461
+ && actual?.runtimeVersion === expected?.runtimeVersion
462
+ && actual?.cdHashFull === expected?.cdHashFull
463
+ && actual?.notarizationTicket === expected?.notarizationTicket;
464
+ }
465
+
274
466
  export function inspectPmStudioApp({
275
467
  appPath,
276
468
  recipe = PM_STUDIO_2_9_7_RECIPE,
277
469
  operations = createPmStudioSetupOperations(),
278
470
  } = {}) {
279
471
  const paths = appPaths(appPath, recipe);
472
+ const issues = [];
280
473
  const metadata = operations.readBundleMetadata({ ...paths, recipe, processRunner: operations.processRunner });
281
474
  const asar = inspectAsarBuffer(fs.readFileSync(paths.asarPath), recipe);
282
475
  const plistIntegrity = operations.readAsarIntegrity({ ...paths, recipe, processRunner: operations.processRunner });
@@ -284,7 +477,16 @@ export function inspectPmStudioApp({
284
477
  ...paths, recipe, processRunner: operations.processRunner,
285
478
  });
286
479
  const codeSign = operations.inspectCodeSign({ ...paths, recipe, processRunner: operations.processRunner });
287
- const issues = [];
480
+ let bundleContent = null;
481
+ if (recipe.sourceBundleContent) {
482
+ try {
483
+ bundleContent = operations.inspectBundleContent({
484
+ ...paths, recipe, processRunner: operations.processRunner,
485
+ });
486
+ } catch (error) {
487
+ issues.push(`bundle content inspection failed: ${error.message}`);
488
+ }
489
+ }
288
490
 
289
491
  if (String(metadata.version) !== recipe.version) {
290
492
  issues.push(`version is ${metadata.version}, expected ${recipe.version}`);
@@ -301,19 +503,33 @@ export function inspectPmStudioApp({
301
503
  issues.push("Electron Framework has an unsupported embedded ASAR integrity slot");
302
504
  }
303
505
 
506
+ const exactSourceBundle = !recipe.sourceBundleContent
507
+ || bundleContentMatches(bundleContent, recipe.sourceBundleContent);
508
+ const strictVendorSignature = codeSign?.valid === true
509
+ && codeSign?.adHoc === false
510
+ && codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
511
+ const exactInvalidVendorSignature = recipe.sourceBundleContent
512
+ && exactSourceBundle
513
+ && codeSign?.valid === false
514
+ && codeSign?.verifyValid === false
515
+ && codeSign?.entitlementsState === "invalid"
516
+ && sourceCodeSignatureMatches(codeSign, recipe.sourceCodeSignature);
517
+ const sourceSignatureAccepted = strictVendorSignature || exactInvalidVendorSignature;
518
+ const patchedSignatureAccepted = codeSign?.valid === true
519
+ && codeSign?.adHoc === true
520
+ && (!recipe.patchedSigningMetadata
521
+ || JSON.stringify(signingMetadata(codeSign)) === JSON.stringify(recipe.patchedSigningMetadata));
304
522
  const clean = issues.length === 0
305
523
  && asar.state === "clean"
306
524
  && integrityMatches(plistIntegrity, "SHA256", recipe.sourceHeaderSha256)
307
525
  && executableIntegrity.executableSha256 === recipe.sourceExecutableSha256
308
526
  && executableIntegrity.frameworkSha256 === recipe.sourceElectronFrameworkSha256
309
- && codeSign?.valid === true
310
- && codeSign?.adHoc === false
311
- && codeSign?.teamIdentifier === recipe.sourceTeamIdentifier;
527
+ && exactSourceBundle
528
+ && sourceSignatureAccepted;
312
529
  const patched = issues.length === 0
313
530
  && asar.state === "patched"
314
531
  && integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)
315
- && codeSign?.valid === true
316
- && codeSign?.adHoc === true;
532
+ && patchedSignatureAccepted;
317
533
 
318
534
  if (!clean && !patched) {
319
535
  if (asar.state === "drift") issues.push("app.asar is unknown drift");
@@ -324,7 +540,12 @@ export function inspectPmStudioApp({
324
540
  && !integrityMatches(plistIntegrity, "SHA256", recipe.patchedHeaderSha256)) {
325
541
  issues.push("patched ASAR does not match ElectronAsarIntegrity");
326
542
  }
327
- if (!codeSign?.valid) issues.push("codesign verification failed");
543
+ if (asar.state === "clean" && recipe.sourceBundleContent && !exactSourceBundle) {
544
+ issues.push("bundle content does not match the exact clean recipe");
545
+ }
546
+ if (!codeSign?.valid && !(asar.state === "clean" && exactInvalidVendorSignature)) {
547
+ issues.push("codesign verification failed");
548
+ }
328
549
  else if (asar.state === "clean" && codeSign.adHoc) issues.push("clean bundle has an unexpected ad-hoc signature");
329
550
  else if (asar.state === "patched" && !codeSign.adHoc) issues.push("patched bundle is not ad-hoc signed");
330
551
  else if (asar.state === "clean" && codeSign.teamIdentifier !== recipe.sourceTeamIdentifier) {
@@ -338,6 +559,11 @@ export function inspectPmStudioApp({
338
559
  && executableIntegrity.frameworkSha256 !== recipe.sourceElectronFrameworkSha256) {
339
560
  issues.push("Electron Framework hash does not match the clean recipe");
340
561
  }
562
+ if (asar.state === "patched" && codeSign?.valid && codeSign?.adHoc
563
+ && recipe.patchedSigningMetadata
564
+ && !patchedSignatureAccepted) {
565
+ issues.push("patched bundle signing metadata does not match the recipe");
566
+ }
341
567
  }
342
568
 
343
569
  return {
@@ -351,6 +577,10 @@ export function inspectPmStudioApp({
351
577
  plistIntegrity,
352
578
  executableIntegrity,
353
579
  codeSign,
580
+ bundleContent,
581
+ sourceVerification: clean
582
+ ? exactInvalidVendorSignature ? "exact-bundle-content" : "codesign"
583
+ : "",
354
584
  paths,
355
585
  issues,
356
586
  };
@@ -393,7 +623,8 @@ function selectRecipe(metadata, recipes) {
393
623
  }
394
624
 
395
625
  function backupName(recipe) {
396
- return `${recipe.id}-${recipe.sourceAsarSha256.slice(0, 12)}`;
626
+ const sourceHash = recipe.sourceBundleContent?.sha256 || recipe.sourceAsarSha256;
627
+ return `${recipe.id}-${sourceHash.slice(0, 12)}`;
397
628
  }
398
629
 
399
630
  export function pmStudioBackupRoot(home = os.homedir()) {
@@ -411,7 +642,7 @@ export function pmStudioPatchManifestPath({
411
642
 
412
643
  function backupManifest(recipe, createdAt) {
413
644
  return {
414
- schema_version: 1,
645
+ schema_version: recipe.sourceBundleContent ? 2 : 1,
415
646
  kind: "ccdx-pm-studio-backup",
416
647
  recipe_id: recipe.id,
417
648
  created_at: createdAt.toISOString(),
@@ -430,12 +661,15 @@ function backupManifest(recipe, createdAt) {
430
661
  electron_framework_sha256: recipe.sourceElectronFrameworkSha256,
431
662
  embedded_asar_integrity: recipe.embeddedAsarIntegrity,
432
663
  },
664
+ ...(recipe.sourceBundleContent ? { bundle_content: recipe.sourceBundleContent } : {}),
665
+ ...(recipe.sourceArtifact ? { artifact: recipe.sourceArtifact } : {}),
433
666
  },
434
667
  patched: {
435
668
  asar_sha256: recipe.patchedAsarSha256,
436
669
  asar_header_sha256: recipe.patchedHeaderSha256,
437
670
  electron_asar_integrity: { algorithm: "SHA256", hash: recipe.patchedHeaderSha256 },
438
671
  binaries: null,
672
+ ...(recipe.sourceBundleContent ? { bundle_content: null } : {}),
439
673
  },
440
674
  backup_bundle: "PM Studio.app",
441
675
  restore_rule: `Restore only while the installed app is PM Studio ${recipe.version} build ${recipe.build}.`,
@@ -451,7 +685,9 @@ function validateBackup({ backupDir, recipe, operations }) {
451
685
  } catch {
452
686
  throw setupError("PM_STUDIO_BACKUP_INVALID", `Existing PM Studio backup manifest is invalid: ${manifestPath}`);
453
687
  }
454
- if (manifest?.kind !== "ccdx-pm-studio-backup"
688
+ const expectedSchema = recipe.sourceBundleContent ? 2 : 1;
689
+ if (manifest?.schema_version !== expectedSchema
690
+ || manifest?.kind !== "ccdx-pm-studio-backup"
455
691
  || manifest?.recipe_id !== recipe.id
456
692
  || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
457
693
  || manifest?.app?.version !== recipe.version
@@ -462,6 +698,10 @@ function validateBackup({ backupDir, recipe, operations }) {
462
698
  || manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
463
699
  || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
464
700
  || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
701
+ || (recipe.sourceBundleContent
702
+ && JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent))
703
+ || (recipe.sourceArtifact
704
+ && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
465
705
  || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
466
706
  || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
467
707
  || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256)) {
@@ -508,13 +748,14 @@ function ensureBackup({ appPath, backupRoot, recipe, operations }) {
508
748
  }
509
749
  }
510
750
 
511
- function writePatchedBinaryRecord({ backup, inspection, operations }) {
751
+ function writePatchedBinaryRecord({ backup, inspection, operations, recipe }) {
512
752
  const manifest = JSON.parse(fs.readFileSync(backup.manifestPath, "utf8"));
513
753
  manifest.patched.binaries = {
514
754
  main_executable_sha256: inspection.executableIntegrity.executableSha256,
515
755
  electron_framework_sha256: inspection.executableIntegrity.frameworkSha256,
516
756
  };
517
757
  manifest.patched.signing_metadata = signingMetadata(inspection.codeSign);
758
+ if (recipe.sourceBundleContent) manifest.patched.bundle_content = inspection.bundleContent;
518
759
  const temporaryPath = `${backup.manifestPath}.${operations.uuid()}.tmp`;
519
760
  try {
520
761
  fs.writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
@@ -532,12 +773,24 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
532
773
  } catch {
533
774
  throw setupError("PM_STUDIO_PATCH_RECORD_INVALID", "PM Studio patched-binary record is missing or invalid");
534
775
  }
535
- if (recipe && (manifest?.schema_version !== 1
776
+ const expectedSchema = recipe?.sourceBundleContent ? 2 : 1;
777
+ if (recipe && (manifest?.schema_version !== expectedSchema
536
778
  || manifest?.kind !== "ccdx-pm-studio-backup"
537
779
  || manifest?.recipe_id !== recipe.id
538
780
  || manifest?.app?.bundle_identifier !== recipe.bundleIdentifier
539
781
  || manifest?.app?.version !== recipe.version
540
782
  || manifest?.app?.build !== recipe.build
783
+ || (recipe.sourceBundleContent && (
784
+ manifest?.source?.asar_sha256 !== recipe.sourceAsarSha256
785
+ || manifest?.source?.asar_header_sha256 !== recipe.sourceHeaderSha256
786
+ || !integrityMatches(manifest?.source?.electron_asar_integrity, "SHA256", recipe.sourceHeaderSha256)
787
+ || manifest?.source?.binaries?.main_executable_sha256 !== recipe.sourceExecutableSha256
788
+ || manifest?.source?.binaries?.electron_framework_sha256 !== recipe.sourceElectronFrameworkSha256
789
+ || manifest?.source?.binaries?.embedded_asar_integrity !== recipe.embeddedAsarIntegrity
790
+ || JSON.stringify(manifest?.source?.bundle_content) !== JSON.stringify(recipe.sourceBundleContent)
791
+ ))
792
+ || (recipe.sourceArtifact
793
+ && JSON.stringify(manifest?.source?.artifact) !== JSON.stringify(recipe.sourceArtifact))
541
794
  || manifest?.patched?.asar_sha256 !== recipe.patchedAsarSha256
542
795
  || manifest?.patched?.asar_header_sha256 !== recipe.patchedHeaderSha256
543
796
  || !integrityMatches(manifest?.patched?.electron_asar_integrity, "SHA256", recipe.patchedHeaderSha256))) {
@@ -552,6 +805,10 @@ export function assertPatchedBinaryRecord({ inspection, manifestPath, recipe })
552
805
  if (JSON.stringify(manifest?.patched?.signing_metadata) !== JSON.stringify(signingMetadata(inspection.codeSign))) {
553
806
  throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio signing metadata differs from the installed patch record");
554
807
  }
808
+ if (recipe?.sourceBundleContent
809
+ && JSON.stringify(manifest?.patched?.bundle_content) !== JSON.stringify(inspection.bundleContent)) {
810
+ throw setupError("PM_STUDIO_BUNDLE_DRIFT", "PM Studio bundle content differs from the installed patch record");
811
+ }
555
812
  }
556
813
 
557
814
  function signingMetadata(codeSign) {
@@ -570,7 +827,15 @@ function flagsWithoutAdHoc(value) {
570
827
  return `0x${(BigInt(`0x${match[1]}`) & ~2n).toString(16)}`;
571
828
  }
572
829
 
573
- function assertSigningMetadataPreserved(source, staged) {
830
+ function assertSigningMetadataPreserved(source, staged, recipe) {
831
+ if (recipe.patchedSigningMetadata) {
832
+ if (JSON.stringify(signingMetadata(staged.codeSign))
833
+ !== JSON.stringify(recipe.patchedSigningMetadata)) {
834
+ throw setupError("PM_STUDIO_SIGNING_METADATA_CHANGED",
835
+ "Ad-hoc signing did not produce the exact PM Studio signing metadata required by the recipe");
836
+ }
837
+ return;
838
+ }
574
839
  const before = signingMetadata(source.codeSign);
575
840
  const after = signingMetadata(staged.codeSign);
576
841
  before.flags = flagsWithoutAdHoc(before.flags);
@@ -665,6 +930,10 @@ export async function runPmStudioSetup({
665
930
  `PM Studio ${recipe.version} build ${recipe.build} does not match the clean or patched recipe; no files were changed`, source.issues);
666
931
  }
667
932
 
933
+ if (source.sourceVerification === "exact-bundle-content") {
934
+ emit(`[WARN] macOS rejected the vendor signature; setup accepted only the exact PM Studio ${recipe.version} official bundle content fingerprint.`);
935
+ }
936
+
668
937
  const backupExists = fs.existsSync(finalBackupDir);
669
938
  assertSpace({ appPath, backupRoot, backupExists, operations });
670
939
 
@@ -674,6 +943,12 @@ export async function runPmStudioSetup({
674
943
  let preserveStage = false;
675
944
  try {
676
945
  operations.copyBundle({ source: appPath, destination: stagePath, processRunner: operations.processRunner });
946
+ const stagedSource = inspectPmStudioApp({ appPath: stagePath, recipe, operations });
947
+ if (stagedSource.state !== "clean"
948
+ || (recipe.sourceBundleContent
949
+ && stagedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
950
+ throw setupError("PM_STUDIO_STAGE_INVALID", "PM Studio staging copy does not match the exact clean source bundle", stagedSource.issues);
951
+ }
677
952
  const stageAsarPath = path.join(stagePath, recipe.asarPath);
678
953
  const patched = patchAsarBuffer(fs.readFileSync(stageAsarPath), recipe);
679
954
  if (!patched.changed) throw setupError("PM_STUDIO_STAGE_INVALID", "Staging copy was unexpectedly already patched");
@@ -691,14 +966,16 @@ export async function runPmStudioSetup({
691
966
  if (staged.state !== "patched") {
692
967
  throw setupError("PM_STUDIO_STAGE_VERIFY_FAILED", "Patched PM Studio staging bundle failed verification", staged.issues);
693
968
  }
694
- assertSigningMetadataPreserved(source, staged);
695
- writePatchedBinaryRecord({ backup, inspection: staged, operations });
969
+ assertSigningMetadataPreserved(source, staged, recipe);
970
+ writePatchedBinaryRecord({ backup, inspection: staged, operations, recipe });
696
971
 
697
972
  assertNoBlockingProcesses(operations, appPath);
698
973
  const unchangedSource = inspectPmStudioApp({ appPath, recipe, operations });
699
974
  if (unchangedSource.state !== "clean"
700
975
  || unchangedSource.asar.asarSha256 !== source.asar.asarSha256
701
- || unchangedSource.asar.headerSha256 !== source.asar.headerSha256) {
976
+ || unchangedSource.asar.headerSha256 !== source.asar.headerSha256
977
+ || (recipe.sourceBundleContent
978
+ && unchangedSource.bundleContent?.sha256 !== source.bundleContent?.sha256)) {
702
979
  throw setupError("PM_STUDIO_SOURCE_CHANGED", "PM Studio changed during setup; the verified staging bundle was not installed");
703
980
  }
704
981