premiere-pro-mcp 1.16.1 → 1.16.2

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/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.16.2] - 2026-09-18
10
+
11
+ ### Fixed
12
+
13
+ - Direct UXP `.ccx` packages now use a plugin-id bundle root and Unix 644/755
14
+ permission bits so Creative Cloud / UPI can extract plugin metadata. (#566)
15
+ - Path-based UXP commands now resolve native paths through the granted
16
+ workspace folder instead of advertising them as unsupported on every host.
17
+ (#567)
18
+
19
+ Automated checks do not establish licensed Premiere or After Effects playback or
20
+ rendered-output verification.
21
+
9
22
  ## [1.16.1] - 2026-09-17
10
23
 
11
24
  ### Added
package/README.md CHANGED
@@ -108,10 +108,10 @@ The current source exposes 381 core tools for supported workflow steps spanning
108
108
 
109
109
  <a id="latest-release"></a>
110
110
 
111
- ### Latest release: 1.16.1
111
+ ### Latest release: 1.16.2
112
112
 
113
- The published v1.16.0 npm artifact contains **373 core tools**, 371 in its default profile,
114
- and 466 with a compatible UXP connection. The development catalog above can include
113
+ The published v1.16.1 npm artifact contains **381 core tools**, 379 in its default profile,
114
+ and 474 with a compatible UXP connection. The development catalog above can include
115
115
  unreleased work. See the [versioned facts and package provenance](https://premiere-pro-mcp.com/facts/).
116
116
 
117
117
  ### Try a bounded workflow
@@ -160,7 +160,7 @@ if the connection is unavailable.
160
160
  local Premiere processes. See the generated [supported action catalog](docs/supported-actions.md)
161
161
  for individual capability and verification contracts.
162
162
 
163
- See the [v1.16.1 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.16.1)
163
+ See the [v1.16.2 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.16.2)
164
164
  for complete details. Live installation in Premiere Pro still requires host verification.
165
165
 
166
166
  ### Current MCP protocol support
@@ -208,7 +208,7 @@ their bins, media rules, and organization rules before a facility uses one.
208
208
  > ### Install the published package (verify the name)
209
209
  >
210
210
  > ```bash
211
- > npm i -g premiere-pro-mcp@1.16.1
211
+ > npm i -g premiere-pro-mcp@1.16.2
212
212
  > ```
213
213
  >
214
214
  > This repository publishes only **`premiere-pro-mcp`**. A differently named package (`adobe-premiere-pro-mcp`) may also declare a `premiere-pro-mcp` executable. Before configuring a client, confirm:
@@ -216,7 +216,7 @@ their bins, media rules, and organization rules before a facility uses one.
216
216
  > | Check | Expected |
217
217
  > | --- | --- |
218
218
  > | Package name | `premiere-pro-mcp` (not `adobe-premiere-pro-mcp`) |
219
- > | Version | `1.16.1` |
219
+ > | Version | `1.16.2` |
220
220
  > | Homepage / repo | https://premiere-pro-mcp.com/ · https://github.com/leancoderkavy/premiere-pro-mcp |
221
221
  >
222
222
  > ```bash
@@ -229,9 +229,9 @@ their bins, media rules, and organization rules before a facility uses one.
229
229
 
230
230
  ### Easiest supported path: Claude Desktop
231
231
 
232
- 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.16.1/premiere-pro-mcp-1.16.1.mcpb).
232
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.16.2/premiere-pro-mcp-1.16.2.mcpb).
233
233
  2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
234
- 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.16.1/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
234
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.16.2/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
235
235
  4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
236
236
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
237
237
 
@@ -760,7 +760,7 @@ The bounded UXP migration surface also exposes guarded static Color effect-param
760
760
 
761
761
  The bounded UXP migration surface also exposes guarded typed Project metadata schema-field creation. It requires the exact inspected active-project identity and 12 KiB-bounded panel XML, an explicit confirmation and operation ID, and serializes this bridge's schema and panel-replacement calls per project. Adobe provides no atomic compare-and-set or field-level schema getter: native acceptance and a changed panel XML are evidence only, so the command always returns `committed_unverified` and does not claim field presence, persistence, UI results, Undo, cancellation, or licensed-host validation.
762
762
 
763
- The panel now requests access to one operator-selected workspace instead of declaring full filesystem access. Choose the folder in the panel before invoking a path-based UXP workflow. Media, relink, preset, export, and Source Monitor file paths must remain inside it; the persistent capability token and native root path are never returned over MCP. Lexical containment alone cannot exclude symlink, junction, or reparse-point escapes, and Adobe's request-scoped UXP filesystem API does not document canonical-path resolution. Builds without a host-supplied canonical resolver therefore advertise path-based UXP commands as unsupported and fail closed at invocation; use the existing CEP fallback for those operations.
763
+ The panel now requests access to one operator-selected workspace instead of declaring full filesystem access. Choose the folder in the panel before invoking a path-based UXP workflow. Media, relink, preset, export, and Source Monitor file paths must remain inside it; the persistent capability token and native root path are never returned over MCP. After a folder is granted, path-based commands resolve each native path through that folder's Entry tree and compare the host `nativePath` against the approved root, so symlink or junction targets outside the workspace still fail closed. If the host Entry cannot be walked and no resolver is supplied, those commands stay unsupported and the CEP fallback remains the path-based compatibility route.
764
764
 
765
765
  Native transcript editing starts with a read-only, revision-locked planning flow. Use
766
766
  `get_clip_transcript_uxp` to export the transcript Premiere generated for a source
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.16.1" ExtensionBundleName="MCP for Adobe After Effects">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.16.2" ExtensionBundleName="MCP for Adobe After Effects">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.16.1"/>
4
+ <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.16.2"/>
5
5
  </ExtensionList>
6
6
  <ExecutionEnvironment>
7
7
  <HostList>
Binary file
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.16.1" ExtensionBundleName="MCP for Adobe Premiere Pro">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.16.2" ExtensionBundleName="MCP for Adobe Premiere Pro">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.premiere.bridge.panel" Version="1.16.1"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.16.1"/>
4
+ <Extension Id="com.mcp.premiere.bridge.panel" Version="1.16.2"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.16.2"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -81,7 +81,7 @@
81
81
  <section class="update-section" aria-live="polite">
82
82
  <div class="update-copy">
83
83
  <span class="section-label">MCP updates</span>
84
- <strong id="updateTitle">Version 1.16.1</strong>
84
+ <strong id="updateTitle">Version 1.16.2</strong>
85
85
  <span id="updateDetail">Checking the global MCP server and connector release…</span>
86
86
  </div>
87
87
  <button id="btnUpdate" class="button button-update" onclick="handleUpdateClick()" type="button" aria-describedby="updateDetail" disabled>
@@ -7,7 +7,7 @@
7
7
  })(this, function () {
8
8
  "use strict";
9
9
 
10
- var CURRENT_VERSION = "1.16.1";
10
+ var CURRENT_VERSION = "1.16.2";
11
11
  var PACKAGE_NAME = "premiere-pro-mcp";
12
12
  var LATEST_PACKAGE_API = "https://registry.npmjs.org/" + PACKAGE_NAME;
13
13
  var LATEST_RELEASE_API =
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "premiere-pro-mcp",
3
3
  "mcpName": "io.github.leancoderkavy/premiere-pro",
4
- "version": "1.16.1",
4
+ "version": "1.16.2",
5
5
  "description": "MCP server for Adobe Premiere Pro with 381 core AI video editing tools, a guarded After Effects MOGRT studio, and a capability-aware UXP bridge for supported Premiere workflows.",
6
6
  "main": "dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "name": "MCP for Adobe Premiere Pro",
11
11
  "mcpName": "io.github.leancoderkavy/premiere-pro",
12
12
  "npmPackage": "premiere-pro-mcp",
13
- "version": "1.16.1",
13
+ "version": "1.16.2",
14
14
  "repository": "https://github.com/leancoderkavy/premiere-pro-mcp",
15
15
  "homepage": "https://premiere-pro-mcp.com/",
16
16
  "license": "MIT",
@@ -41,6 +41,27 @@ function u32(value) {
41
41
  return value;
42
42
  }
43
43
 
44
+ function unixExternalAttributes(kind) {
45
+ const mode = kind === "directory" ? 0o040755 : 0o100644;
46
+ const dos = kind === "directory" ? 0x10 : 0x20;
47
+ return u32(((mode * 0x10000) + dos) >>> 0);
48
+ }
49
+
50
+ function withBundleRoot(files, pluginId) {
51
+ const prefix = `${pluginId}/`;
52
+ const directories = new Set([prefix]);
53
+ const prefixed = files.map((file) => {
54
+ const name = `${prefix}${file.name.replaceAll("\\", "/")}`;
55
+ const segments = name.split("/");
56
+ for (let index = 1; index < segments.length; index += 1) {
57
+ directories.add(`${segments.slice(0, index).join("/")}/`);
58
+ }
59
+ return { name, data: file.data, kind: "file" };
60
+ });
61
+ const dirEntries = [...directories].map((name) => ({ name, data: Buffer.alloc(0), kind: "directory" }));
62
+ return [...dirEntries, ...prefixed].sort((left, right) => left.name.localeCompare(right.name));
63
+ }
64
+
44
65
  async function collectFiles(directory, relative = "") {
45
66
  const entries = await readdir(directory, { withFileTypes: true });
46
67
  const files = [];
@@ -66,8 +87,10 @@ function buildStoredZip(files) {
66
87
  let offset = 0;
67
88
 
68
89
  for (const file of files) {
90
+ const isDirectory = file.kind === "directory" || file.name.endsWith("/");
91
+ const data = isDirectory ? Buffer.alloc(0) : file.data;
69
92
  const name = Buffer.from(file.name, "utf8");
70
- const checksum = crc32(file.data);
93
+ const checksum = crc32(data);
71
94
  const local = Buffer.alloc(30);
72
95
  local.writeUInt32LE(0x04034b50, 0);
73
96
  local.writeUInt16LE(20, 4);
@@ -76,8 +99,8 @@ function buildStoredZip(files) {
76
99
  local.writeUInt16LE(0, 10);
77
100
  local.writeUInt16LE(0x0021, 12);
78
101
  local.writeUInt32LE(u32(checksum), 14);
79
- local.writeUInt32LE(u32(file.data.length), 18);
80
- local.writeUInt32LE(u32(file.data.length), 22);
102
+ local.writeUInt32LE(u32(data.length), 18);
103
+ local.writeUInt32LE(u32(data.length), 22);
81
104
  local.writeUInt16LE(u16(name.length), 26);
82
105
  local.writeUInt16LE(0, 28);
83
106
 
@@ -90,19 +113,19 @@ function buildStoredZip(files) {
90
113
  central.writeUInt16LE(0, 12);
91
114
  central.writeUInt16LE(0x0021, 14);
92
115
  central.writeUInt32LE(u32(checksum), 16);
93
- central.writeUInt32LE(u32(file.data.length), 20);
94
- central.writeUInt32LE(u32(file.data.length), 24);
116
+ central.writeUInt32LE(u32(data.length), 20);
117
+ central.writeUInt32LE(u32(data.length), 24);
95
118
  central.writeUInt16LE(u16(name.length), 28);
96
119
  central.writeUInt16LE(0, 30);
97
120
  central.writeUInt16LE(0, 32);
98
121
  central.writeUInt16LE(0, 34);
99
122
  central.writeUInt16LE(0, 36);
100
- central.writeUInt32LE(0, 38);
123
+ central.writeUInt32LE(unixExternalAttributes(isDirectory ? "directory" : "file"), 38);
101
124
  central.writeUInt32LE(u32(offset), 42);
102
125
 
103
- localRecords.push(local, name, file.data);
126
+ localRecords.push(local, name, data);
104
127
  centralRecords.push(central, name);
105
- offset += local.length + name.length + file.data.length;
128
+ offset += local.length + name.length + data.length;
106
129
  }
107
130
 
108
131
  const centralDirectory = Buffer.concat(centralRecords);
@@ -118,7 +141,7 @@ function buildStoredZip(files) {
118
141
  return Buffer.concat([...localRecords, centralDirectory, end]);
119
142
  }
120
143
 
121
- function verifyStoredZip(archive, expectedNames) {
144
+ function verifyStoredZip(archive, expectedNames, pluginId) {
122
145
  const endOffset = archive.length - 22;
123
146
  assert(endOffset >= 0 && archive.readUInt32LE(endOffset) === 0x06054b50, "archive is missing a ZIP end record");
124
147
  assert(archive.readUInt16LE(endOffset + 20) === 0, "archive must not contain a ZIP comment");
@@ -128,20 +151,33 @@ function verifyStoredZip(archive, expectedNames) {
128
151
  assert(cursor + centralSize === endOffset, "archive central directory has an unexpected length");
129
152
  assert(entryCount === expectedNames.length, "archive entry count does not match the staged plugin files");
130
153
  const names = [];
154
+ const bundleRoot = `${pluginId}/`;
131
155
 
132
156
  for (let index = 0; index < entryCount; index += 1) {
133
157
  assert(archive.readUInt32LE(cursor) === 0x02014b50, "archive central directory entry is invalid");
134
158
  assert(archive.readUInt16LE(cursor + 10) === 0, "archive must use stored ZIP entries");
159
+ assert((archive.readUInt16LE(cursor + 4) >>> 8) === 3, "archive entries must declare a Unix origin");
135
160
  const checksum = archive.readUInt32LE(cursor + 16);
136
161
  const compressedSize = archive.readUInt32LE(cursor + 20);
137
162
  const uncompressedSize = archive.readUInt32LE(cursor + 24);
138
163
  const nameLength = archive.readUInt16LE(cursor + 28);
139
164
  const extraLength = archive.readUInt16LE(cursor + 30);
140
165
  const commentLength = archive.readUInt16LE(cursor + 32);
166
+ const externalAttributes = archive.readUInt32LE(cursor + 38);
141
167
  const localOffset = archive.readUInt32LE(cursor + 42);
142
168
  const name = archive.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
143
169
  assert(!names.includes(name), `archive contains duplicate entry ${name}`);
144
170
  names.push(name);
171
+ assert(name === bundleRoot || name.startsWith(bundleRoot), `archive entry ${name} must stay under ${bundleRoot}`);
172
+ const unixMode = externalAttributes >>> 16;
173
+ const isDirectory = name.endsWith("/");
174
+ if (isDirectory) {
175
+ assert((unixMode & 0o170000) === 0o040000, `archive directory ${name} must use Unix directory bits`);
176
+ assert((unixMode & 0o777) === 0o755, `archive directory ${name} must be mode 755`);
177
+ } else {
178
+ assert((unixMode & 0o170000) === 0o100000, `archive file ${name} must use Unix regular-file bits`);
179
+ assert((unixMode & 0o777) === 0o644, `archive file ${name} must be mode 644`);
180
+ }
145
181
  assert(archive.readUInt32LE(localOffset) === 0x04034b50, `archive local entry is invalid for ${name}`);
146
182
  const localNameLength = archive.readUInt16LE(localOffset + 26);
147
183
  const localExtraLength = archive.readUInt16LE(localOffset + 28);
@@ -157,6 +193,7 @@ function verifyStoredZip(archive, expectedNames) {
157
193
  cursor += 46 + nameLength + extraLength + commentLength;
158
194
  }
159
195
 
196
+ assert(names.includes(bundleRoot), `archive must contain the ${bundleRoot} directory`);
160
197
  assert(
161
198
  JSON.stringify(names.sort()) === JSON.stringify([...expectedNames].sort()),
162
199
  "archive contents do not match the staged plugin files",
@@ -193,11 +230,13 @@ async function main() {
193
230
  data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
194
231
  };
195
232
  files.sort((left, right) => left.name.localeCompare(right.name));
233
+ const packaged = withBundleRoot(files, pluginId);
196
234
 
197
- const archive = buildStoredZip(files);
235
+ const archive = buildStoredZip(packaged);
198
236
  verifyStoredZip(
199
237
  archive,
200
- files.map((file) => file.name),
238
+ packaged.map((file) => file.name),
239
+ pluginId,
201
240
  );
202
241
  await mkdir(artifacts, { recursive: true });
203
242
  const output = path.join(
@@ -21,14 +21,20 @@ This writes:
21
21
  artifacts/premiere-pro-mcp-uxp-<version>-direct.ccx
22
22
  ```
23
23
 
24
- The builder uses a fixed ZIP timestamp, lexical file order, and stored entries,
25
- then reads the generated central directory and checks every entry and CRC. The
26
- result is reproducible for identical source bytes. It also rejects symlinks,
27
- requires the UXP v5 Premiere manifest, checks the version against
28
- `package.json`. Adobe's Premiere 26.3 runtime requires the compatible `domains: "all"`
24
+ The builder uses a fixed ZIP timestamp, lexical file order, and stored entries
25
+ under a single bundle root (`<plugin-id>/manifest.json`), with Unix file mode
26
+ 644 and directory mode 755. It then reads the generated central directory and
27
+ checks every entry, CRC, bundle prefix, and permission bits. The result is
28
+ reproducible for identical source bytes. It also rejects symlinks, requires
29
+ the UXP v5 Premiere manifest, and checks the version against `package.json`.
30
+ Adobe's Premiere 26.3 runtime requires the compatible `domains: "all"`
29
31
  manifest form for the WebSocket connection; `workspace.cjs` remains the enforced authority and
30
32
  accepts only `ws://127.0.0.1:<port>/uxp` or `ws://localhost:<port>/uxp`.
31
33
 
34
+ Adobe's Unified Plugin Installer accepts `--install <path>`. The
35
+ `--install="<path>"` form can exit 0 without installing; use the space-separated
36
+ form when diagnosing a local CCX.
37
+
32
38
  For an Adobe Creative Cloud Marketplace submission, Adobe Developer
33
39
  Distribution must first create the Marketplace plugin ID. Build a separate
34
40
  package without changing the checked-in direct manifest:
@@ -239,7 +239,7 @@
239
239
  if (definition.conditionalWorkspace) commands[name].workspaceRequired = "path_variant_only";
240
240
  if (definition.targetCapabilityProbe) commands[name].targetCapabilityProbe = "invocation";
241
241
  if (!apiSupported) commands[name].reason = "Required Premiere UXP API is unavailable in this host";
242
- else if (!pathValidationSupported) commands[name].reason = "This UXP host cannot canonically validate native paths; use the CEP fallback for path-based workflows";
242
+ else if (!pathValidationSupported) commands[name].reason = "canonical path validation is not implemented in this build; use the CEP fallback for path-based workflows";
243
243
  }
244
244
  return {
245
245
  backend: "uxp", protocolVersion: Protocol.PROTOCOL_VERSION, hostMinVersion: "25.6.0",
@@ -2,7 +2,7 @@
2
2
  "manifestVersion": 5,
3
3
  "id": "com.ppmcp.premiere.uxp",
4
4
  "name": "MCP for Adobe Premiere Pro",
5
- "version": "1.16.1",
5
+ "version": "1.16.2",
6
6
  "main": "index.html",
7
7
  "host": { "app": "premierepro", "minVersion": "25.6.0" },
8
8
  "entrypoints": [
@@ -74,6 +74,45 @@
74
74
  return url;
75
75
  }
76
76
 
77
+ function nativePathOf(fs, entry) {
78
+ if (entry && typeof entry.nativePath === "string" && entry.nativePath) return entry.nativePath;
79
+ if (entry && fs && typeof fs.getNativePath === "function") return fs.getNativePath(entry);
80
+ return "";
81
+ }
82
+
83
+ function toFileUrl(nativePath) {
84
+ const parsed = parseAbsolutePath(nativePath, "path");
85
+ if (parsed.kind === "windows") return "file:/" + parsed.normalized;
86
+ return "file:" + parsed.normalized;
87
+ }
88
+
89
+ function createCanonicalPathResolver(deps) {
90
+ const fs = deps && deps.fs;
91
+ if (!fs || typeof fs.getEntryWithUrl !== "function") return undefined;
92
+ return async function resolveCanonicalPath(value, options) {
93
+ const label = options && options.label || "path";
94
+ const kind = options && options.kind || "file";
95
+ let entry;
96
+ try {
97
+ entry = await fs.getEntryWithUrl(toFileUrl(value));
98
+ } catch (error) {
99
+ if (error && /^UXP_[A-Z0-9_]+$/.test(error.code || "")) throw error;
100
+ throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
101
+ }
102
+ const native = nativePathOf(fs, entry);
103
+ if (!native) {
104
+ throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
105
+ }
106
+ if (kind === "directory" && entry && entry.isFile) {
107
+ throw workspaceError("UXP_INVALID_ARGUMENT", label + " must be a directory");
108
+ }
109
+ if (kind === "file" && entry && entry.isFolder) {
110
+ throw workspaceError("UXP_INVALID_ARGUMENT", label + " must be a file");
111
+ }
112
+ return parseAbsolutePath(native, label).normalized;
113
+ };
114
+ }
115
+
77
116
  function createWorkspaceBroker(deps) {
78
117
  const fs = deps && deps.fs;
79
118
  const resolveCanonicalPath = deps && deps.resolveCanonicalPath;
@@ -82,9 +121,7 @@
82
121
  let initialized = false;
83
122
 
84
123
  function nativePathFor(entry) {
85
- if (entry && typeof entry.nativePath === "string" && entry.nativePath) return entry.nativePath;
86
- if (entry && fs && typeof fs.getNativePath === "function") return fs.getNativePath(entry);
87
- return "";
124
+ return nativePathOf(fs, entry);
88
125
  }
89
126
 
90
127
  async function dataFolder() {
@@ -168,6 +205,10 @@
168
205
  return status();
169
206
  }
170
207
 
208
+ function canWalkGrantedFolder() {
209
+ return !!(rootEntry && typeof rootEntry.getEntry === "function");
210
+ }
211
+
171
212
  function status() {
172
213
  return {
173
214
  configured: !!rootEntry,
@@ -175,10 +216,46 @@
175
216
  rootName: rootEntry && typeof rootEntry.name === "string" ? rootEntry.name : null,
176
217
  persistent: !!persistentToken,
177
218
  pathDisclosure: "redacted",
178
- canonicalPathValidation: typeof resolveCanonicalPath === "function" ? "available" : "unavailable"
219
+ canonicalPathValidation: typeof resolveCanonicalPath === "function" || canWalkGrantedFolder() ? "available" : "unavailable"
179
220
  };
180
221
  }
181
222
 
223
+ function relativeSegments(rootPath, candidatePath, label) {
224
+ const root = parseAbsolutePath(rootPath, "workspace root");
225
+ const candidate = parseAbsolutePath(candidatePath, label);
226
+ if (candidate.comparison === root.comparison) return [];
227
+ if (candidate.comparison.indexOf(root.comparison + "/") !== 0) {
228
+ throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " must stay inside the approved workspace folder");
229
+ }
230
+ return candidate.normalized.slice(root.normalized.length).replace(/^\//, "").split("/").filter(Boolean);
231
+ }
232
+
233
+ async function resolveThroughGrantedFolder(candidatePath, options) {
234
+ const label = options && options.label || "path";
235
+ if (!canWalkGrantedFolder()) {
236
+ throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "Canonical path validation is not implemented in this build; use the CEP fallback for path-based workflows");
237
+ }
238
+ const rootPath = nativePathFor(rootEntry);
239
+ const segments = relativeSegments(rootPath, candidatePath, label);
240
+ let entry = rootEntry;
241
+ for (let index = 0; index < segments.length; index += 1) {
242
+ if (!entry || typeof entry.getEntry !== "function") {
243
+ throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "Canonical path validation is not implemented in this build; use the CEP fallback for path-based workflows");
244
+ }
245
+ try {
246
+ entry = await entry.getEntry(segments[index]);
247
+ } catch (error) {
248
+ if (error && /^UXP_[A-Z0-9_]+$/.test(error.code || "")) throw error;
249
+ throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
250
+ }
251
+ }
252
+ const native = nativePathFor(entry);
253
+ if (!native) {
254
+ throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
255
+ }
256
+ return parseAbsolutePath(native, label).normalized;
257
+ }
258
+
182
259
  async function assertPathAllowed(value, options) {
183
260
  const label = options && options.label || "path";
184
261
  const kind = options && options.kind || "file";
@@ -190,17 +267,18 @@
190
267
  if (!isContained(rootPath, candidate.normalized, kind === "directory")) {
191
268
  throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " must stay inside the approved workspace folder");
192
269
  }
193
- // A lexical prefix check cannot detect symlinks, Windows junctions, or
194
- // other reparse points. Adobe's request-scoped UXP filesystem API does
195
- // not expose a documented realpath/link-inspection primitive, so raw
196
- // native paths must fail closed unless the embedding host supplies one.
197
- if (typeof resolveCanonicalPath !== "function") {
198
- throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host cannot prove that " + label + " stays inside the approved workspace after resolving filesystem links");
199
- }
270
+ // Lexical containment cannot detect symlink, junction, or reparse-point
271
+ // escapes. Prefer an embedder-supplied resolver; otherwise walk from the
272
+ // granted folder Entry so nativePath reflects the host's resolved target.
200
273
  let canonicalRootValue, canonicalCandidateValue;
201
274
  try {
202
- canonicalRootValue = await resolveCanonicalPath(rootPath, { label: "workspace root", kind: "directory" });
203
- canonicalCandidateValue = await resolveCanonicalPath(candidate.normalized, { label, kind });
275
+ if (typeof resolveCanonicalPath === "function") {
276
+ canonicalRootValue = await resolveCanonicalPath(rootPath, { label: "workspace root", kind: "directory" });
277
+ canonicalCandidateValue = await resolveCanonicalPath(candidate.normalized, { label, kind });
278
+ } else {
279
+ canonicalRootValue = await resolveThroughGrantedFolder(rootPath, { label: "workspace root", kind: "directory" });
280
+ canonicalCandidateValue = await resolveThroughGrantedFolder(candidate.normalized, { label, kind });
281
+ }
204
282
  } catch (error) {
205
283
  if (error && /^UXP_[A-Z0-9_]+$/.test(error.code || "")) throw error;
206
284
  throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
@@ -216,5 +294,5 @@
216
294
  return { initialize, requestRoot, revoke, status, assertPathAllowed };
217
295
  }
218
296
 
219
- return { createWorkspaceBroker, parseAbsolutePath, isContained, validateLoopbackBridgeUrl, workspaceError };
297
+ return { createWorkspaceBroker, createCanonicalPathResolver, parseAbsolutePath, isContained, validateLoopbackBridgeUrl, workspaceError };
220
298
  });