davinci-resolve-mcp 2.223.0 → 2.224.0
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 +49 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/SKILL.md +3 -1
- package/docs/contributing.md +1 -1
- package/docs/install.md +2 -2
- package/docs/reference/api-coverage.md +3 -1
- package/docs/reference/resolve211-encryption.md +48 -0
- package/install.py +2 -2
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/resolve_211.py +13 -1
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +17 -5
- package/src/utils/destructive_hook.py +1 -0
- package/src/utils/execution_lifecycle.py +1 -0
- package/src/utils/resolve211_encryption.py +59 -0
- package/src/utils/resolve_versions.py +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
|
|
4
4
|
|
|
5
|
+
## What's New in v2.224.0 — native Resolve 21.1 DCTL encryption
|
|
6
|
+
|
|
7
|
+
Contributed by @legionsound (#216), live-validated on Studio 21.1.0.14.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`dctl encrypt_native`**, with the granular twin `encrypt_dctl_native`,
|
|
12
|
+
calling native 21.1 DCTL encryption. The caller supplies an existing `.dctl`
|
|
13
|
+
input, a new `.dctle` output path and an optional expiry; the wrapper reports
|
|
14
|
+
the actual final path, size and hash. Tool count 376 → 377.
|
|
15
|
+
- **A destination is never replaced.** Resolve writes into isolated staging and
|
|
16
|
+
only a verified non-empty regular file is published. The publish uses
|
|
17
|
+
`O_EXCL` creation at `0o600`, so a file that appears *during* encryption
|
|
18
|
+
cannot be clobbered, and the existence check uses `lexists` so a **dangling
|
|
19
|
+
symlink** counts as an occupied destination rather than a free one — the case
|
|
20
|
+
a plain existence test silently gets wrong. The cross-volume fallback removes
|
|
21
|
+
its own partial output if the copy fails.
|
|
22
|
+
- Source bytes are preserved, and the action neither installs nor applies the
|
|
23
|
+
shader.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Classified **LOW** risk rather than MEDIUM, with a destructive-action hook
|
|
28
|
+
entry and dry-run refusal coverage. LOW is the honest rating here: the action
|
|
29
|
+
only ever creates a new file and is incapable of overwriting one, so grouping
|
|
30
|
+
it with operations that rewrite existing work would make the rating mean
|
|
31
|
+
less. Verified: `dctl.encrypt_native` classifies LOW / destructive /
|
|
32
|
+
recognised, and is in the destructive registry.
|
|
33
|
+
- Two native boundaries handled explicitly rather than papered over: Resolve
|
|
34
|
+
appends `.dctle` itself, so a fixed staging stem prevents a doubled suffix on
|
|
35
|
+
a user-supplied name; and an empty expiry string is normalized to null,
|
|
36
|
+
because an isolated probe measured the native call returning false for `""`
|
|
37
|
+
and true for null or omission. Other expiry strings pass through untouched.
|
|
38
|
+
|
|
39
|
+
### Validation
|
|
40
|
+
|
|
41
|
+
- Full suite green: 3,478 passed, 1 skipped. Drift guards, api-parity and read/write
|
|
42
|
+
symmetry all clean with the 377 count.
|
|
43
|
+
- Live evidence is @legionsound's on Studio 21.1.0.14, through both interfaces
|
|
44
|
+
with synthetic identity code: each exported a non-empty file, reported
|
|
45
|
+
correct size and hash, preserved the source bytes, produced owner-only
|
|
46
|
+
permissions, and refused a repeat export without altering the destination.
|
|
47
|
+
Not reproduced here; this machine is Studio 19.1.3.7, below the 21.1 floor.
|
|
48
|
+
- **Not claimed**: that an encrypted shader is accepted by a render, and
|
|
49
|
+
nothing at all about cipher strength. Observed file sizes are recorded as
|
|
50
|
+
observations, not format guarantees.
|
|
51
|
+
- This PR was branched from current `main` and merged **without adaptation** —
|
|
52
|
+
the first in the 21.1 series to need none.
|
|
53
|
+
|
|
5
54
|
## What's New in v2.223.0 — native Resolve 21.1 DCTL validation
|
|
6
55
|
|
|
7
56
|
Contributed by @legionsound (#215), live-validated on Studio 21.1.0.14.
|
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
|
-
[-blue.svg)](#server-modes)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#server-modes)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
@@ -133,7 +133,7 @@ The command starts a loopback-only server and opens the control panel in your br
|
|
|
133
133
|
| Mode | Entry point | Tools | Best for |
|
|
134
134
|
|------|-------------|-------|----------|
|
|
135
135
|
| Compound | `src/server.py` | 36 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
|
|
136
|
-
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` |
|
|
136
|
+
| Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 377 | Power users who want one MCP tool per Resolve API method. |
|
|
137
137
|
|
|
138
138
|
The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
|
|
139
139
|
|
|
@@ -365,7 +365,7 @@ The default server is a local stdio process launched by your MCP client; it does
|
|
|
365
365
|
|
|
366
366
|
| Metric | Value |
|
|
367
367
|
|--------|-------|
|
|
368
|
-
| MCP Tools | **36** compound / **
|
|
368
|
+
| MCP Tools | **36** compound / **377** granular (live server) |
|
|
369
369
|
| Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
|
|
370
370
|
| Kernel Actions | **136** guarded workflow actions across 9 compound tools |
|
|
371
371
|
| API Methods Covered | **361/361** (100%) |
|
package/README.zh-CN.md
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
|
-
[-blue.svg)](#服务器模式)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#服务器模式)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
15
|
+
> 本翻译对应 v2.224.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -88,7 +88,7 @@ venv/bin/python -m src.control_panel
|
|
|
88
88
|
| 模式 | 入口 | 工具数 | 适合谁 |
|
|
89
89
|
|------|------|--------|--------|
|
|
90
90
|
| Compound(复合) | `src/server.py` | 36 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
|
|
91
|
-
| Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` |
|
|
91
|
+
| Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 377 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
|
|
92
92
|
|
|
93
93
|
除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
|
|
94
94
|
|
|
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
|
|
|
226
226
|
|
|
227
227
|
| 指标 | 数值 |
|
|
228
228
|
|------|------|
|
|
229
|
-
| MCP 工具 | **36** 复合 / **
|
|
229
|
+
| MCP 工具 | **36** 复合 / **377** 细粒度(实时服务器) |
|
|
230
230
|
| Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
|
|
231
231
|
| 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
|
|
232
232
|
| API 方法覆盖 | **361/361**(100%) |
|
package/docs/SKILL.md
CHANGED
|
@@ -345,7 +345,7 @@ to the user as verified.
|
|
|
345
345
|
| Mode | Entry point | Tool count | Use when |
|
|
346
346
|
|---|---|---|---|
|
|
347
347
|
| Compound (default) | `src/server.py` | 36 tools | Most workflows — keeps context lean |
|
|
348
|
-
| Granular (full) | `src/server.py --full` |
|
|
348
|
+
| Granular (full) | `src/server.py --full` | 377 tools | Power users needing one tool per API method |
|
|
349
349
|
|
|
350
350
|
Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
|
|
351
351
|
for edition, presets, audio formats/codecs, normalization modes, speed, fades
|
|
@@ -2492,3 +2492,5 @@ Native audio normalization: [21.1 controls](reference/resolve211-normalization.m
|
|
|
2492
2492
|
Native timecode/waveform alignment: [21.1 controls](reference/resolve211-alignment.md), including linked-item selection semantics and rendered video/audio evidence.
|
|
2493
2493
|
|
|
2494
2494
|
Resolve-native DCTL validation: [21.1 controls](reference/resolve211-dctl-validation.md), separate from static validation and shader rendering.
|
|
2495
|
+
|
|
2496
|
+
Native DCTL encryption: [21.1 controls](reference/resolve211-encryption.md), with explicit destination handling and export-evidence limits.
|
package/docs/contributing.md
CHANGED
|
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
|
|
|
64
64
|
├── install.py # Universal installer (macOS/Windows/Linux)
|
|
65
65
|
├── src/
|
|
66
66
|
│ ├── server.py # Compound MCP server — 36 tools (default)
|
|
67
|
-
│ ├── resolve_mcp_server.py # Thin full-server entrypoint —
|
|
67
|
+
│ ├── resolve_mcp_server.py # Thin full-server entrypoint — 377 tools
|
|
68
68
|
│ ├── granular/ # Modular full-server implementation
|
|
69
69
|
│ └── utils/ # Platform detection, Resolve connection helpers
|
|
70
70
|
├── tests/ # offline suite (test_*.py) + live harnesses (live_*.py):
|
package/docs/install.md
CHANGED
|
@@ -144,7 +144,7 @@ The MCP server comes in two modes:
|
|
|
144
144
|
| Mode | File | Tools | Best For |
|
|
145
145
|
|------|------|-------|----------|
|
|
146
146
|
| **Compound** (default) | `src/server.py` | 36 | Most users — fast, clean, low context usage |
|
|
147
|
-
| **Full** | `src/resolve_mcp_server.py` |
|
|
147
|
+
| **Full** | `src/resolve_mcp_server.py` | 377 | Power users who want one tool per API method |
|
|
148
148
|
|
|
149
149
|
The compound server's `timeline_item` tool includes dedicated actions for common workflows:
|
|
150
150
|
|
|
@@ -159,7 +159,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
|
|
|
159
159
|
|
|
160
160
|
The installer uses the compound server by default. To use the full server:
|
|
161
161
|
```bash
|
|
162
|
-
python src/server.py --full # Launch full
|
|
162
|
+
python src/server.py --full # Launch full 377-tool server
|
|
163
163
|
# Or point your MCP config directly at src/resolve_mcp_server.py
|
|
164
164
|
```
|
|
165
165
|
|
|
@@ -25,7 +25,7 @@ Every non-deprecated method in the bundled legacy README is represented. This
|
|
|
25
25
|
does not claim complete coverage of the newer Resolve 21.1 typed API. The
|
|
26
26
|
default compound server exposes **36 tools** that group related operations by
|
|
27
27
|
action parameter, keeping LLM context windows lean. The full granular server
|
|
28
|
-
provides **
|
|
28
|
+
provides **377 individual tools** for power users. The legacy coverage spans
|
|
29
29
|
13 API object classes. MCP-level kernel actions are tracked separately in
|
|
30
30
|
[Kernel Action Coverage](../kernels/README.md).
|
|
31
31
|
|
|
@@ -642,3 +642,5 @@ Native audio normalization: [21.1 controls](resolve211-normalization.md), with i
|
|
|
642
642
|
Native timecode/waveform alignment: [21.1 controls](resolve211-alignment.md), including linked-item selection semantics and rendered video/audio evidence.
|
|
643
643
|
|
|
644
644
|
Resolve-native DCTL validation: [21.1 controls](resolve211-dctl-validation.md), separate from static validation and shader rendering.
|
|
645
|
+
|
|
646
|
+
Native DCTL encryption: [21.1 controls](resolve211-encryption.md), with explicit destination handling and export-evidence limits.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Native Resolve 21.1 DCTL encryption
|
|
2
|
+
|
|
3
|
+
Compound `dctl encrypt_native` and granular `encrypt_dctl_native` accept
|
|
4
|
+
`input_path`, `output_path` and optional `expiry`. Input must be an existing .dctl;
|
|
5
|
+
output must be a new .dctle. Existing destinations, including dangling symlinks,
|
|
6
|
+
are refused. The source is never modified by the wrapper.
|
|
7
|
+
|
|
8
|
+
The native Name and OutputFolder options are derived from an isolated staging
|
|
9
|
+
file, then the completed file is published to the requested output path. This
|
|
10
|
+
avoids native filename surprises: Resolve appends .dctle even when Name already
|
|
11
|
+
ends with that suffix. Expiry null/omission means no expiry. Empty string is
|
|
12
|
+
normalized to null because native empty-string expiry returned false in the
|
|
13
|
+
contributor fixture while null succeeded. Other strings pass through as native
|
|
14
|
+
ISO 8601 expiry requests; native failure is preserved.
|
|
15
|
+
|
|
16
|
+
Resolve-facing staging uses the existing safe-directory helper. A successful
|
|
17
|
+
native return must be accompanied by a non-empty regular encrypted file. An
|
|
18
|
+
atomic no-replace hard link publishes it; filesystems without that support use
|
|
19
|
+
exclusive file creation and copy. A failed copy removes only the new partial
|
|
20
|
+
output. An output appearing during encryption is not overwritten. New files
|
|
21
|
+
use owner-only permissions where the platform supports them. The result reports
|
|
22
|
+
actual path, byte length and SHA-256, not a guessed native filename.
|
|
23
|
+
|
|
24
|
+
The method has a 21.1 floor. The dctl tool has the destructive-action hook, and
|
|
25
|
+
encrypt_native is registered/rated LOW because it only creates new output and
|
|
26
|
+
never replaces existing content. Explicit compound dry runs refuse before the
|
|
27
|
+
handler. The granular tool declares a write. No DCTL is installed or applied by
|
|
28
|
+
this operation.
|
|
29
|
+
|
|
30
|
+
## Contributor evidence and limits
|
|
31
|
+
|
|
32
|
+
Contributor-validated on macOS Studio 21.1.0.14 via official MCP probes and both
|
|
33
|
+
actual community wrappers, using a synthetic multiline identity shader. Native
|
|
34
|
+
omitted/null expiry and a future date succeeded; empty string returned false in
|
|
35
|
+
the isolated probe. Both wrappers created 2032-byte files in the tested runs,
|
|
36
|
+
returned matching file hashes/sizes, left source bytes unchanged and refused a
|
|
37
|
+
repeat export without changing the destination. These byte lengths are observed,
|
|
38
|
+
not format requirements. Ciphertext equality across separate encryptions is not
|
|
39
|
+
assumed.
|
|
40
|
+
|
|
41
|
+
Tests exercise output races, missing native output, native false, cross-volume
|
|
42
|
+
fallback, failed-copy cleanup, dangling symlinks, bad arguments and write gates.
|
|
43
|
+
This is encryption/export evidence, not shader application or rendered-image
|
|
44
|
+
acceptance, and not a security assessment of Blackmagic's encryption scheme.
|
|
45
|
+
|
|
46
|
+
`tests/live_resolve211_encryption.py SOURCE_DCTL OUTPUT_DIR` uses a synthetic
|
|
47
|
+
identity source and a fresh output directory. It does not install the results or
|
|
48
|
+
change a project. Never substitute someone else's shader without authorization.
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.
|
|
40
|
+
VERSION = "2.224.0"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
|
@@ -1543,7 +1543,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
|
|
|
1543
1543
|
|
|
1544
1544
|
def print_banner():
|
|
1545
1545
|
title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
|
|
1546
|
-
subtitle = "36 compound ·
|
|
1546
|
+
subtitle = "36 compound · 377 full · 3 platforms"
|
|
1547
1547
|
print()
|
|
1548
1548
|
print(bold(" ╔══════════════════════════════════════════════════════╗"))
|
|
1549
1549
|
print(bold(f" ║{title:^54}║"))
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.
|
|
90
|
+
VERSION = "2.224.0"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -3,11 +3,12 @@ from src.utils.resolve211_dctl import native_dctl_result
|
|
|
3
3
|
from src.utils.resolve211_alignment import auto_align
|
|
4
4
|
from src.utils.resolve211_normalization import normalize_audio
|
|
5
5
|
from src.utils.resolve211_blanking import validate_blanking
|
|
6
|
+
from src.utils.resolve211_encryption import encrypt_dctl
|
|
6
7
|
from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
|
|
7
8
|
from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
|
|
8
9
|
from src.granular.common import (
|
|
9
10
|
mcp, READ_ONLY_TOOL, WRITE_TOOL, DESTRUCTIVE_TOOL, get_resolve, get_current_project,
|
|
10
|
-
_get_timeline, _get_timeline_item, _find_clip_by_id, _requires_method, has_method,
|
|
11
|
+
_get_timeline, _get_timeline_item, _resolve_safe_dir, _find_clip_by_id, _requires_method, has_method,
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
|
|
@@ -319,3 +320,14 @@ def validate_dctl_native(source: str) -> dict:
|
|
|
319
320
|
if missing:
|
|
320
321
|
return missing
|
|
321
322
|
return native_dctl_result(r, source)
|
|
323
|
+
|
|
324
|
+
@mcp.tool(annotations=WRITE_TOOL)
|
|
325
|
+
def encrypt_dctl_native(input_path: str, output_path: str, expiry: str | None = None) -> dict:
|
|
326
|
+
"""Encrypt a .dctl to a new .dctle using Resolve 21.1. Never overwrites. Empty/null expiry means no expiry; ISO 8601 strings pass to Resolve. Source is unchanged."""
|
|
327
|
+
r = get_resolve()
|
|
328
|
+
if r is None:
|
|
329
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
330
|
+
missing = _requires_method(r, "EncryptDCTL", "21.1")
|
|
331
|
+
if missing:
|
|
332
|
+
return missing
|
|
333
|
+
return encrypt_dctl(r, input_path, output_path, expiry, _resolve_safe_dir)
|
|
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
|
|
|
34
34
|
if __name__ == "__main__":
|
|
35
35
|
try:
|
|
36
36
|
start_background_update_check(VERSION, project_dir, logger)
|
|
37
|
-
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (
|
|
37
|
+
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (377 granular tools)")
|
|
38
38
|
run_fastmcp_stdio(mcp)
|
|
39
39
|
except KeyboardInterrupt:
|
|
40
40
|
logger.info("Server shutdown requested")
|
package/src/server.py
CHANGED
|
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
|
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
python src/server.py # Start the MCP server
|
|
11
|
-
python src/server.py --full # Start the
|
|
11
|
+
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.224.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -42,6 +42,7 @@ for p in [current_dir, project_dir]:
|
|
|
42
42
|
if p not in sys.path:
|
|
43
43
|
sys.path.insert(0, p)
|
|
44
44
|
|
|
45
|
+
from src.utils.resolve211_encryption import encrypt_dctl
|
|
45
46
|
from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
|
|
46
47
|
from src.utils.resolve211_blanking import validate_blanking
|
|
47
48
|
from src.utils.resolve211_alignment import auto_align
|
|
@@ -31114,6 +31115,7 @@ _DCTL_VALID_CATEGORIES = ("lut", "aces_idt", "aces_odt")
|
|
|
31114
31115
|
|
|
31115
31116
|
@mcp.tool()
|
|
31116
31117
|
@_guard_missing_params
|
|
31118
|
+
@_destructive_op("dctl")
|
|
31117
31119
|
def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
31118
31120
|
"""Author and install DCTL files (Color page custom shaders + ACES transforms).
|
|
31119
31121
|
|
|
@@ -31142,6 +31144,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
|
|
|
31142
31144
|
— ext: '.dctl' (default) or '.dctle' (encrypted)
|
|
31143
31145
|
remove(name, category?, subdir?, ext?) -> {success}
|
|
31144
31146
|
read(name, category?, subdir?, ext?) -> {source, encrypted}
|
|
31147
|
+
encrypt_native(input_path, output_path, expiry?) -> {success, path?, bytes?, sha256?} — native 21.1; never overwrites.
|
|
31145
31148
|
validate(source) -> {valid, errors, warnings, checker}
|
|
31146
31149
|
validate_native(source) -> {valid, diagnostic, checker} — Resolve 21.1 validation; source and diagnostic unchanged.
|
|
31147
31150
|
template(kind, name, options?) -> {source, kind, name, suggested_category}
|
|
@@ -31309,6 +31312,15 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
|
|
|
31309
31312
|
return missing
|
|
31310
31313
|
return native_dctl_result(r, source)
|
|
31311
31314
|
|
|
31315
|
+
if action == "encrypt_native":
|
|
31316
|
+
r = get_resolve()
|
|
31317
|
+
if r is None:
|
|
31318
|
+
return _not_connected_error()
|
|
31319
|
+
missing = _requires_method(r, "EncryptDCTL", "21.1")
|
|
31320
|
+
if missing:
|
|
31321
|
+
return missing
|
|
31322
|
+
return encrypt_dctl(r, p.get("input_path"), p.get("output_path"), p.get("expiry"), _resolve_safe_dir)
|
|
31323
|
+
|
|
31312
31324
|
if action == "validate":
|
|
31313
31325
|
source = p.get("source")
|
|
31314
31326
|
if not isinstance(source, str):
|
|
@@ -31335,7 +31347,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
|
|
|
31335
31347
|
}
|
|
31336
31348
|
|
|
31337
31349
|
return _unknown(action, ["path", "list", "install", "remove", "read",
|
|
31338
|
-
"validate_native", "validate", "template", "list_templates"])
|
|
31350
|
+
"encrypt_native", "validate_native", "validate", "template", "list_templates"])
|
|
31339
31351
|
|
|
31340
31352
|
|
|
31341
31353
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -32751,9 +32763,9 @@ if __name__ == "__main__":
|
|
|
32751
32763
|
start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
|
|
32752
32764
|
_install_threaded_tool_dispatch(mcp)
|
|
32753
32765
|
|
|
32754
|
-
# Support --full flag to run the
|
|
32766
|
+
# Support --full flag to run the 377-tool granular server instead
|
|
32755
32767
|
if "--full" in sys.argv:
|
|
32756
|
-
logger.info("Starting full
|
|
32768
|
+
logger.info("Starting full 377-tool granular server...")
|
|
32757
32769
|
sys.argv = [arg for arg in sys.argv if arg != "--full"]
|
|
32758
32770
|
from src.granular import mcp as granular_mcp
|
|
32759
32771
|
|
|
@@ -63,6 +63,7 @@ SAFE_MODE_BLOCKED_RISK_LEVELS: FrozenSet[str] = frozenset({
|
|
|
63
63
|
# replace_clip/link_*). The test_destructive_registry_drift guard asserts every
|
|
64
64
|
# string here is a real handler so this can't regress.
|
|
65
65
|
DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
66
|
+
"dctl": frozenset({"encrypt_native"}),
|
|
66
67
|
"media_pool": frozenset({
|
|
67
68
|
"delete_clips",
|
|
68
69
|
"delete_folders",
|
|
@@ -193,6 +193,7 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
193
193
|
#: them unrecognised, i.e. it warns that the risk is unestablished for the
|
|
194
194
|
#: actions whose risk is the best established of any we dispatch.
|
|
195
195
|
_LOW_RISK_ACTIONS: Set[Tuple[str, str]] = {
|
|
196
|
+
("dctl", "encrypt_native"), # Creates a new file; never replaces existing content.
|
|
196
197
|
("timeline_markers", "add"),
|
|
197
198
|
("timeline_markers", "update_custom_data"),
|
|
198
199
|
("timeline_item_markers", "add"),
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Stage native DCTL encryption, then publish without replacing an existing file."""
|
|
2
|
+
import errno
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def encrypt_dctl(r, input_path, output_path, expiry, safe_dir):
|
|
11
|
+
if not isinstance(input_path,str) or not input_path or not isinstance(output_path,str) or not output_path:
|
|
12
|
+
return {'error':'input_path and output_path must be non-empty strings'}
|
|
13
|
+
if expiry is not None and not isinstance(expiry,str):
|
|
14
|
+
return {'error':'expiry must be an ISO 8601 string or null'}
|
|
15
|
+
source=Path(input_path).expanduser().absolute()
|
|
16
|
+
target=Path(output_path).expanduser().absolute()
|
|
17
|
+
if source.suffix.lower()!='.dctl' or not source.is_file():
|
|
18
|
+
return {'error':'input_path must name an existing .dctl file'}
|
|
19
|
+
if target.suffix.lower()!='.dctle':
|
|
20
|
+
return {'error':'output_path must end in .dctle'}
|
|
21
|
+
if os.path.lexists(target):
|
|
22
|
+
return {'error':'Output already exists; refusing to overwrite','success':False}
|
|
23
|
+
try:
|
|
24
|
+
target.parent.mkdir(parents=True,exist_ok=True)
|
|
25
|
+
staging_root=Path(safe_dir(str(target.parent.resolve())))
|
|
26
|
+
staging_root.mkdir(parents=True,exist_ok=True)
|
|
27
|
+
with tempfile.TemporaryDirectory(prefix='resolve-mcp-dctl-',dir=staging_root) as stage:
|
|
28
|
+
# Native Name is a stem: Resolve appends .dctle itself. Use a fixed
|
|
29
|
+
# staging name, so user filename characters never become native paths.
|
|
30
|
+
options={'Name':'encrypted','OutputFolder':stage,'Expiry':None if expiry=='' else expiry}
|
|
31
|
+
if not r.EncryptDCTL(str(source),options):
|
|
32
|
+
return {'success':False}
|
|
33
|
+
encrypted=Path(stage)/'encrypted.dctle'
|
|
34
|
+
if encrypted.is_symlink() or not encrypted.is_file() or encrypted.stat().st_size==0:
|
|
35
|
+
return {'success':False,'error':'Resolve reported success without a non-empty encrypted file'}
|
|
36
|
+
os.chmod(encrypted,0o600)
|
|
37
|
+
digest=hashlib.sha256(encrypted.read_bytes()).hexdigest()
|
|
38
|
+
size=encrypted.stat().st_size
|
|
39
|
+
try:
|
|
40
|
+
os.link(encrypted,target)
|
|
41
|
+
except OSError as exc:
|
|
42
|
+
if exc.errno not in (errno.EXDEV,errno.EPERM,errno.EOPNOTSUPP,errno.ENOSYS):
|
|
43
|
+
raise
|
|
44
|
+
# Cross-volume/no-hardlink filesystem: exclusive creation still
|
|
45
|
+
# prevents overwriting an existing target, including a symlink.
|
|
46
|
+
fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
|
47
|
+
try:
|
|
48
|
+
with os.fdopen(fd,'wb') as out, encrypted.open('rb') as src:
|
|
49
|
+
shutil.copyfileobj(src,out)
|
|
50
|
+
out.flush()
|
|
51
|
+
os.fsync(out.fileno())
|
|
52
|
+
except BaseException:
|
|
53
|
+
target.unlink(missing_ok=True)
|
|
54
|
+
raise
|
|
55
|
+
return {'success':True,'path':str(target),'bytes':size,'sha256':digest}
|
|
56
|
+
except FileExistsError:
|
|
57
|
+
return {'success':False,'error':'Output appeared during encryption; refusing to overwrite'}
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
return {'success':False,'error':'Encryption/export failed: '+(str(exc) or type(exc).__name__)}
|
|
@@ -227,6 +227,8 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
|
|
|
227
227
|
CODE_FLOORS: Dict[str, str] = {
|
|
228
228
|
"Timeline.AutoAlignClips": "21.1",
|
|
229
229
|
"Resolve.ValidateDCTL": "21.1",
|
|
230
|
+
|
|
231
|
+
"Resolve.EncryptDCTL": "21.1",
|
|
230
232
|
"MediaPool.CreateMulticamClip": "21.1",
|
|
231
233
|
"TimelineItem.FlattenMulticam": "21.1",
|
|
232
234
|
"Timeline.SetOutputBlanking": "21.1",
|