davinci-resolve-mcp 3.0.0 → 3.0.1
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 +94 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/authoring/script-plugin-authoring.md +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/install_resolve_bridge.py +9 -4
- package/src/granular/common.py +1 -1
- package/src/server.py +24 -2
- package/src/utils/destructive_hook.py +29 -2
- package/src/utils/execution_lifecycle.py +22 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,100 @@
|
|
|
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 v3.0.1 — project deletion is gated, and the open project is refused by default
|
|
6
|
+
|
|
7
|
+
A security fix, published as [GHSA-gmp7-qjp9-m7gm](https://github.com/samuelgursky/davinci-resolve-mcp/security/advisories/GHSA-gmp7-qjp9-m7gm).
|
|
8
|
+
One behaviour change existing callers may notice is called out below.
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **`project_manager delete` permanently deleted any named project — including
|
|
13
|
+
the one open in Resolve — without confirmation, and no gate saw it.** It runs
|
|
14
|
+
through `delete_project_safely`, a *reliability* helper that works around
|
|
15
|
+
DeleteProject's flakiness and session lock by closing the open project and
|
|
16
|
+
then deleting it. The `project_manager` tool carried no `@_destructive_op`,
|
|
17
|
+
`delete` was not registered, and the CRITICAL risk rule written for project
|
|
18
|
+
deletion named `delete_project`, an action no tool dispatches. So it matched
|
|
19
|
+
nothing: safe mode, dry-run refusal and the security audit log never saw a
|
|
20
|
+
project deletion.
|
|
21
|
+
- **Thirteen further deletes that the classifier already rated HIGH were not
|
|
22
|
+
enforced, for the same reason.** They are the three `render` deletes,
|
|
23
|
+
`render_presets.delete_burnin`, `gallery_stills.delete_stills`, `fusion_comp`
|
|
24
|
+
`delete_tool` and `delete_keyframe`, `timeline_item.delete_keyframe`, the three
|
|
25
|
+
`media_pool_item_markers` deletes, `project_settings.delete_color_group` and
|
|
26
|
+
`resolve_control.delete_user_preferences_preset`. Safe mode is enforced only
|
|
27
|
+
by the decorator, and only for registered actions, so these ratings were a
|
|
28
|
+
promise nothing kept.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **The raw `delete` now refuses the currently open project unless
|
|
33
|
+
`close_current=True`**, matching `safe_project_delete`. This is the one change
|
|
34
|
+
existing callers may notice: a call that used to close and delete the open
|
|
35
|
+
project now returns an error, until it passes `close_current=True`.
|
|
36
|
+
- All of the actions above are registered and their tools decorated. The project
|
|
37
|
+
delete is CRITICAL, and `safe_project_delete` and the thirteen others are HIGH,
|
|
38
|
+
so all are blocked while safe mode is on. An explicit dry run is refused
|
|
39
|
+
unless the action honours it natively; only `safe_project_delete` does, and
|
|
40
|
+
keeps working. Every call is audited. None of them archives the timeline,
|
|
41
|
+
except the keyframe deletes on `fusion_comp` and `timeline_item`, which change
|
|
42
|
+
timeline items and so still do.
|
|
43
|
+
- **`remove_motion_blur`** (on `folder` and `media_pool_item`) is **re-rated
|
|
44
|
+
MEDIUM**. It renders new media and never touches the source, and was already
|
|
45
|
+
confirm-gated for exactly that reason, but the `remove_` name-prefix rule had
|
|
46
|
+
rated it HIGH on its name alone. It is now audited, and not blocked by safe
|
|
47
|
+
mode.
|
|
48
|
+
- **The name-prefix rule is now a fallback for unlisted actions**: an explicit
|
|
49
|
+
lower rating wins. Before, it fired ahead of the LOW and MEDIUM tables and
|
|
50
|
+
could not be overridden. No existing rating changed except `remove_motion_blur`
|
|
51
|
+
— no LOW, MEDIUM or graph-LUT entry started with `delete_` or `remove_`.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- **Nine risk rules named actions that no tool dispatches, and so protected
|
|
56
|
+
nothing.** On `project_manager`: `delete_project`, now repointed at the real
|
|
57
|
+
`delete`, plus `close_project_without_saving` and `save_project_as`. On
|
|
58
|
+
`edit_engine`: `auto_cut_silence` and `ripple_trim`. On `timeline`: `cut_clip`,
|
|
59
|
+
`delete_clip_by_id`, `delete_markers` and `ripple_delete` — which is a CutList
|
|
60
|
+
entry kind inside `apply_cuts`, not an action. The rest were removed; since
|
|
61
|
+
they never matched, removing them changes nothing at runtime. An existing test
|
|
62
|
+
even asserted that the dead `delete_project` rule classified as CRITICAL — true
|
|
63
|
+
of a name no tool uses, and part of how it survived. It now tests the real
|
|
64
|
+
`delete`.
|
|
65
|
+
- **The bridge installer's Lua canary gave only the pre-21.1 diagnosis** — that
|
|
66
|
+
Resolve cannot find a Python 3. On free 21.1 that is wrong, because Python
|
|
67
|
+
scripting moved to Studio (#203). Its comments and printed output now give both
|
|
68
|
+
causes, the newer first (#219). The printed post-install guidance was already
|
|
69
|
+
corrected in v2.224.1.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **`tests/test_write_enforcement_ratchet.py`** fails the suite in three cases:
|
|
74
|
+
- an action is rated destructive but not enforced;
|
|
75
|
+
- a risk rule names an action no tool dispatches;
|
|
76
|
+
- a new write-style action appears with neither a rating nor a registry entry.
|
|
77
|
+
|
|
78
|
+
The 144 unrated write-style actions that exist today are frozen as a backlog.
|
|
79
|
+
Rating one forces its removal from the list, so it can only shrink. This is
|
|
80
|
+
the second instance of this gap in two days, and the first time it cannot
|
|
81
|
+
come back unnoticed.
|
|
82
|
+
- `tests/test_project_delete_guard.py` pins the delete guard, its rating and its
|
|
83
|
+
enforcement.
|
|
84
|
+
|
|
85
|
+
### Documentation
|
|
86
|
+
|
|
87
|
+
- Removed a stale tool count from `docs/authoring/script-plugin-authoring.md`.
|
|
88
|
+
|
|
89
|
+
### Validation
|
|
90
|
+
|
|
91
|
+
- Full suite green: 3,482 passed, 1 skipped. Every static and drift gate is clean, including
|
|
92
|
+
the native-dry-run scan. That scan requires the native-dry-run list to match,
|
|
93
|
+
exactly, the registered actions whose handlers read `dry_run`, which is how
|
|
94
|
+
`safe_project_delete` was confirmed as the only one.
|
|
95
|
+
- No live Resolve run. The gating is decorator-level and verified offline. The
|
|
96
|
+
delete guard is tested against a fake project manager, deliberately:
|
|
97
|
+
exercising it live means deleting a real project.
|
|
98
|
+
|
|
5
99
|
## What's New in v3.0.0 — the server no longer executes caller-supplied code, and every plugin write is gated
|
|
6
100
|
|
|
7
101
|
**A breaking release.** Two public actions are removed. The rest of the change
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v3.0.
|
|
15
|
+
> 本翻译对应 v3.0.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -19,7 +19,7 @@ automation.
|
|
|
19
19
|
| Custom workflow you want as a permanent menu item | `script_plugin('install', ...)`, then the user runs it from Workspace → Scripts |
|
|
20
20
|
| Image-processing node for the Fusion page | `fuse_plugin` |
|
|
21
21
|
| Color-page programmable transform | `dctl` |
|
|
22
|
-
| Anything the
|
|
22
|
+
| Anything the wrapped Resolve API tools already cover | The wrapped tool — no scripting needed |
|
|
23
23
|
|
|
24
24
|
## Two template kinds
|
|
25
25
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "3.0.
|
|
40
|
+
VERSION = "3.0.1"
|
|
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
|
package/package.json
CHANGED
|
@@ -288,7 +288,9 @@ _CANARY_NAME = "resolve_bridge_canary.lua"
|
|
|
288
288
|
|
|
289
289
|
_LUA_CANARY = """-- Installed by davinci-resolve-mcp as an enumeration canary.
|
|
290
290
|
-- If THIS appears under Workspace > Scripts but resolve_bridge_probe does not,
|
|
291
|
-
-- Resolve is listing Lua and silently skipping Python
|
|
291
|
+
-- Resolve is listing Lua and silently skipping Python. On Resolve 21.1+ FREE
|
|
292
|
+
-- that is expected: Python scripting moved to Studio (issue #203), and nothing
|
|
293
|
+
-- below will change it. On Studio, or 21.0.x and earlier, it cannot find a Python 3.
|
|
292
294
|
-- It looks at PYTHON3HOME, then /usr/local/bin/python3 -- and nowhere else, which
|
|
293
295
|
-- is why Homebrew, pyenv, uv and conda interpreters go unseen. Either point it at
|
|
294
296
|
-- the one you have (no sudo, but does NOT survive a reboot):
|
|
@@ -299,9 +301,12 @@ _LUA_CANARY = """-- Installed by davinci-resolve-mcp as an enumeration canary.
|
|
|
299
301
|
-- already looks, which persists:
|
|
300
302
|
-- sudo ln -s "$(command -v python3)" /usr/local/bin/python3
|
|
301
303
|
-- A python.org build creates that symlink for you. Restart Resolve after.
|
|
302
|
-
print("Resolve is enumerating scripts. If the Python probe is missing
|
|
303
|
-
print("
|
|
304
|
-
print("
|
|
304
|
+
print("Resolve is enumerating scripts. If the Python probe is missing:")
|
|
305
|
+
print("- Resolve 21.1+ FREE: Python scripting moved to Studio, so .py scripts")
|
|
306
|
+
print(" no longer list at all (issue #203). No Python setting changes that.")
|
|
307
|
+
print("- Studio, or 21.0.x and earlier: Resolve cannot find a Python 3. Set")
|
|
308
|
+
print(" PYTHON3HOME with launchctl setenv, or install a python.org build.")
|
|
309
|
+
print(" Homebrew/pyenv/uv/conda are not looked at directly.")
|
|
305
310
|
"""
|
|
306
311
|
|
|
307
312
|
|
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 = "3.0.
|
|
90
|
+
VERSION = "3.0.1"
|
|
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()}")
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "3.0.
|
|
14
|
+
VERSION = "3.0.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -16575,6 +16575,7 @@ def setup(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
16575
16575
|
|
|
16576
16576
|
@mcp.tool()
|
|
16577
16577
|
@_guard_missing_params
|
|
16578
|
+
@_destructive_op("resolve_control")
|
|
16578
16579
|
def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
16579
16580
|
"""App-level DaVinci Resolve operations.
|
|
16580
16581
|
|
|
@@ -18072,6 +18073,7 @@ def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
|
|
|
18072
18073
|
|
|
18073
18074
|
@mcp.tool()
|
|
18074
18075
|
@_guard_missing_params
|
|
18076
|
+
@_destructive_op("render_presets")
|
|
18075
18077
|
def render_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
18076
18078
|
"""Import/export render and burn-in presets.
|
|
18077
18079
|
|
|
@@ -19030,6 +19032,7 @@ def _project_lint_live(r, pm) -> Dict[str, Any]:
|
|
|
19030
19032
|
|
|
19031
19033
|
@mcp.tool()
|
|
19032
19034
|
@_guard_missing_params
|
|
19035
|
+
@_destructive_op("project_manager")
|
|
19033
19036
|
def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
19034
19037
|
"""Manage DaVinci Resolve projects.
|
|
19035
19038
|
|
|
@@ -19047,7 +19050,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19047
19050
|
render and wedges Resolve's pipeline until restart (stuck
|
|
19048
19051
|
IsRenderingInProgress, 0% jobs, refused Quit). stop_render=true stops
|
|
19049
19052
|
the render, waits for the flag to clear, then closes.
|
|
19050
|
-
delete(name) -> {success}
|
|
19053
|
+
delete(name, close_current?) -> {success}
|
|
19051
19054
|
import_project(path, name?) -> {success}
|
|
19052
19055
|
export_project(name, path, with_stills_and_luts?) -> {success}
|
|
19053
19056
|
archive(name, path, src_media?, render_cache?, proxy_media?) -> {success}
|
|
@@ -19180,6 +19183,18 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19180
19183
|
elif action == "delete":
|
|
19181
19184
|
if not p.get("name"):
|
|
19182
19185
|
return _err("delete requires name")
|
|
19186
|
+
# Refuse the open project unless the caller says so, as
|
|
19187
|
+
# safe_project_delete does. delete_project_safely closes and deletes the
|
|
19188
|
+
# current project without asking — the call that loses a project someone
|
|
19189
|
+
# is working in. If the current project cannot be read, DeleteProject on
|
|
19190
|
+
# an open project fails anyway (the session holds its lock).
|
|
19191
|
+
try:
|
|
19192
|
+
_open = pm.GetCurrentProject()
|
|
19193
|
+
_open_name = _open.GetName() if _open else None
|
|
19194
|
+
except Exception:
|
|
19195
|
+
_open_name = None
|
|
19196
|
+
if _open_name == p["name"] and not p.get("close_current", False):
|
|
19197
|
+
return _err("Refusing to delete the currently open project; pass close_current=True")
|
|
19183
19198
|
from src.utils.project_cleanup import delete_project_safely
|
|
19184
19199
|
deleted = delete_project_safely(pm, p["name"])
|
|
19185
19200
|
return {"success": bool(deleted.get("success")), "delete_detail": deleted}
|
|
@@ -19364,6 +19379,7 @@ def _setting_limitation(name: Any, obj: str = "Project") -> Optional[Dict[str, A
|
|
|
19364
19379
|
|
|
19365
19380
|
@mcp.tool()
|
|
19366
19381
|
@_guard_missing_params
|
|
19382
|
+
@_destructive_op("project_settings")
|
|
19367
19383
|
def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
19368
19384
|
"""Project metadata, settings, and color groups.
|
|
19369
19385
|
|
|
@@ -20340,6 +20356,7 @@ def _export_render_boundary_report(proj, p: Dict[str, Any]):
|
|
|
20340
20356
|
|
|
20341
20357
|
@mcp.tool()
|
|
20342
20358
|
@_guard_missing_params
|
|
20359
|
+
@_destructive_op("render")
|
|
20343
20360
|
def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
20344
20361
|
"""Render pipeline: jobs, presets, formats, codecs, and rendering.
|
|
20345
20362
|
|
|
@@ -21319,6 +21336,7 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
21319
21336
|
|
|
21320
21337
|
@mcp.tool()
|
|
21321
21338
|
@_guard_missing_params
|
|
21339
|
+
@_destructive_op("folder")
|
|
21322
21340
|
def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
21323
21341
|
"""Operations on Media Pool folders.
|
|
21324
21342
|
|
|
@@ -21491,6 +21509,7 @@ def _keyed_get(getter, key):
|
|
|
21491
21509
|
|
|
21492
21510
|
@mcp.tool()
|
|
21493
21511
|
@_guard_missing_params
|
|
21512
|
+
@_destructive_op("media_pool_item")
|
|
21494
21513
|
def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
21495
21514
|
"""Operations on a media pool clip. Identify clip by clip_id.
|
|
21496
21515
|
|
|
@@ -21955,6 +21974,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
21955
21974
|
|
|
21956
21975
|
@mcp.tool()
|
|
21957
21976
|
@_guard_missing_params
|
|
21977
|
+
@_destructive_op("media_pool_item_markers")
|
|
21958
21978
|
def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
21959
21979
|
"""Markers and flags on media pool clips. Identify clip by clip_id.
|
|
21960
21980
|
|
|
@@ -28805,6 +28825,7 @@ def gallery(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, A
|
|
|
28805
28825
|
|
|
28806
28826
|
@mcp.tool()
|
|
28807
28827
|
@_guard_missing_params
|
|
28828
|
+
@_destructive_op("gallery_stills")
|
|
28808
28829
|
def gallery_stills(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
28809
28830
|
"""Manage stills in gallery albums (best results on Color page).
|
|
28810
28831
|
|
|
@@ -30261,6 +30282,7 @@ def _fusion_get_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
30261
30282
|
|
|
30262
30283
|
@mcp.tool()
|
|
30263
30284
|
@_guard_missing_params
|
|
30285
|
+
@_destructive_op("fusion_comp")
|
|
30264
30286
|
def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
30265
30287
|
"""Fusion composition node graph operations.
|
|
30266
30288
|
|
|
@@ -77,6 +77,24 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
77
77
|
"safe_install_extension",
|
|
78
78
|
"safe_remove_extension",
|
|
79
79
|
}),
|
|
80
|
+
# Deletes the classifier already rated HIGH (CRITICAL for the raw project
|
|
81
|
+
# delete) on tools that carried no @_destructive_op, so safe mode never saw
|
|
82
|
+
# them. Plus the 21.0 AI deblur, rated MEDIUM: it creates media, and is
|
|
83
|
+
# registered so it is audited and its dry run is honest.
|
|
84
|
+
"folder": frozenset({"remove_motion_blur"}),
|
|
85
|
+
"fusion_comp": frozenset({"delete_keyframe", "delete_tool"}),
|
|
86
|
+
"gallery_stills": frozenset({"delete_stills"}),
|
|
87
|
+
"media_pool_item": frozenset({"remove_motion_blur"}),
|
|
88
|
+
"media_pool_item_markers": frozenset({
|
|
89
|
+
"delete_at_frame",
|
|
90
|
+
"delete_by_color",
|
|
91
|
+
"delete_by_custom_data",
|
|
92
|
+
}),
|
|
93
|
+
"project_manager": frozenset({"delete", "safe_project_delete"}),
|
|
94
|
+
"project_settings": frozenset({"delete_color_group"}),
|
|
95
|
+
"render": frozenset({"delete_all_jobs", "delete_job", "delete_preset"}),
|
|
96
|
+
"render_presets": frozenset({"delete_burnin"}),
|
|
97
|
+
"resolve_control": frozenset({"delete_user_preferences_preset"}),
|
|
80
98
|
"media_pool": frozenset({
|
|
81
99
|
"delete_clips",
|
|
82
100
|
"delete_folders",
|
|
@@ -155,6 +173,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
155
173
|
"create_subtitles",
|
|
156
174
|
}),
|
|
157
175
|
"timeline_item": frozenset({
|
|
176
|
+
"delete_keyframe",
|
|
158
177
|
"set_output_blanking",
|
|
159
178
|
"set_use_timeline_for_output_blanking",
|
|
160
179
|
"set_clip_enabled",
|
|
@@ -250,7 +269,9 @@ NO_ARCHIVE_ON_KEYS: Dict[Tuple[str, str], frozenset] = {
|
|
|
250
269
|
|
|
251
270
|
# ── Non-timeline write tools ────────────────────────────────────────────────
|
|
252
271
|
#
|
|
253
|
-
# Tools whose registered actions
|
|
272
|
+
# Tools whose registered actions do not mutate the working timeline: they write
|
|
273
|
+
# plugin folders, or act on projects, the render queue, presets, the gallery,
|
|
274
|
+
# pool items or app preferences. Every
|
|
254
275
|
# gate applies to them — safe mode, dry-run refusal, the audit log — but they
|
|
255
276
|
# skip version-on-mutate archiving, and skip resolving the versioning context
|
|
256
277
|
# at all: that goes through the project-root provider, which reaches Resolve,
|
|
@@ -258,7 +279,12 @@ NO_ARCHIVE_ON_KEYS: Dict[Tuple[str, str], frozenset] = {
|
|
|
258
279
|
# Resolve. `media_pool` has its own branch for the same reason; these differ in
|
|
259
280
|
# that there is no project state to log.
|
|
260
281
|
|
|
261
|
-
NON_TIMELINE_WRITE_TOOLS: frozenset = frozenset({
|
|
282
|
+
NON_TIMELINE_WRITE_TOOLS: frozenset = frozenset({
|
|
283
|
+
"dctl", "fuse_plugin", "script_plugin",
|
|
284
|
+
"folder", "gallery_stills", "media_pool_item", "media_pool_item_markers",
|
|
285
|
+
"project_manager", "project_settings", "render", "render_presets",
|
|
286
|
+
"resolve_control",
|
|
287
|
+
})
|
|
262
288
|
|
|
263
289
|
|
|
264
290
|
# ── Strict-mode allowlist ───────────────────────────────────────────────────
|
|
@@ -327,6 +353,7 @@ NATIVE_DRY_RUN_ACTIONS: frozenset = frozenset({
|
|
|
327
353
|
("timeline_ai", "create_subtitles"),
|
|
328
354
|
("script_plugin", "safe_install_extension"),
|
|
329
355
|
("script_plugin", "safe_remove_extension"),
|
|
356
|
+
("project_manager", "safe_project_delete"),
|
|
330
357
|
})
|
|
331
358
|
|
|
332
359
|
|
|
@@ -125,8 +125,10 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
125
125
|
name = "risk_classification"
|
|
126
126
|
|
|
127
127
|
_CRITICAL_ACTIONS: Set[Tuple[str, str]] = {
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
# Raw project delete. This rule used to name `delete_project`, which
|
|
129
|
+
# no tool dispatches, so it matched nothing and deleting a project
|
|
130
|
+
# passed every gate.
|
|
131
|
+
("project_manager", "delete"),
|
|
130
132
|
("media_pool", "delete_timelines"),
|
|
131
133
|
("media_pool", "delete_clips"),
|
|
132
134
|
}
|
|
@@ -140,15 +142,11 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
140
142
|
("fuse_plugin", "remove"),
|
|
141
143
|
("script_plugin", "remove"),
|
|
142
144
|
("script_plugin", "safe_remove_extension"),
|
|
145
|
+
# Guarded delete: disposable `_mcp_` projects only, and the open one
|
|
146
|
+
# only with close_current=True — but still permanent.
|
|
147
|
+
("project_manager", "safe_project_delete"),
|
|
143
148
|
("timeline", "delete_clips"),
|
|
144
|
-
("timeline", "delete_clip_by_id"),
|
|
145
|
-
("timeline", "delete_markers"),
|
|
146
|
-
("timeline", "ripple_delete"),
|
|
147
|
-
("timeline", "cut_clip"),
|
|
148
149
|
("edit_engine", "execute_selects"),
|
|
149
|
-
("edit_engine", "auto_cut_silence"),
|
|
150
|
-
("edit_engine", "ripple_trim"),
|
|
151
|
-
("project_manager", "save_project_as"),
|
|
152
150
|
("media_pool", "delete_folders"),
|
|
153
151
|
("timeline", "delete_track"),
|
|
154
152
|
("timeline", "lift_range"),
|
|
@@ -264,6 +262,11 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
264
262
|
("fuse_plugin", "install"),
|
|
265
263
|
("script_plugin", "install"),
|
|
266
264
|
("script_plugin", "safe_install_extension"),
|
|
265
|
+
# 21.0 AI deblur renders NEW media and never touches the source (it is
|
|
266
|
+
# confirm-token gated for that reason). The `remove_` prefix rule rated
|
|
267
|
+
# it HIGH on its name alone, which would make safe mode block a create.
|
|
268
|
+
("folder", "remove_motion_blur"),
|
|
269
|
+
("media_pool_item", "remove_motion_blur"),
|
|
267
270
|
# Additive edits that place content into an existing timeline. Nothing
|
|
268
271
|
# is deleted (`overwrite_range`, which does delete, is HIGH), but the
|
|
269
272
|
# timeline is no longer what it was.
|
|
@@ -378,7 +381,16 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
378
381
|
radius = BlastRadius.PROJECT if "project" in tool_name else BlastRadius.TIMELINE
|
|
379
382
|
conf_required = True
|
|
380
383
|
reasons.append(f"Action '{action}' is permanently destructive across {radius.value}")
|
|
381
|
-
elif pair in cls._HIGH_RISK_ACTIONS or
|
|
384
|
+
elif pair in cls._HIGH_RISK_ACTIONS or (
|
|
385
|
+
# The name-prefix rule is a fallback for UNLISTED actions: an
|
|
386
|
+
# explicit lower rating wins. It used to fire first and so could
|
|
387
|
+
# not be overridden — `remove_motion_blur` creates media, yet read
|
|
388
|
+
# HIGH on its name.
|
|
389
|
+
pair not in cls._LOW_RISK_ACTIONS
|
|
390
|
+
and pair not in cls._MEDIUM_RISK_ACTIONS
|
|
391
|
+
and pair not in cls._GRAPH_LUT_ACTIONS
|
|
392
|
+
and (action.startswith("delete_") or action.startswith("remove_"))
|
|
393
|
+
):
|
|
382
394
|
level = RiskLevel.HIGH
|
|
383
395
|
destructive = True
|
|
384
396
|
if params.get("ripple", False):
|