dcc-mcp-runtime 0.2.0__tar.gz
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.
- dcc_mcp_runtime-0.2.0/.github/workflows/ci.yml +41 -0
- dcc_mcp_runtime-0.2.0/.github/workflows/release.yml +61 -0
- dcc_mcp_runtime-0.2.0/.gitignore +7 -0
- dcc_mcp_runtime-0.2.0/.release-please-manifest.json +3 -0
- dcc_mcp_runtime-0.2.0/CHANGELOG.md +19 -0
- dcc_mcp_runtime-0.2.0/LICENSE +21 -0
- dcc_mcp_runtime-0.2.0/PKG-INFO +52 -0
- dcc_mcp_runtime-0.2.0/README.md +38 -0
- dcc_mcp_runtime-0.2.0/adapters/adobe_runtime_entry.py +18 -0
- dcc_mcp_runtime-0.2.0/adapters/aftereffects-runtime-handshake.patch +12 -0
- dcc_mcp_runtime-0.2.0/adapters/capcut-runtime-handshake.patch +12 -0
- dcc_mcp_runtime-0.2.0/adapters/capcut_runtime_entry.py +14 -0
- dcc_mcp_runtime-0.2.0/adapters/obs-runtime-handshake.patch +12 -0
- dcc_mcp_runtime-0.2.0/adapters/obs_runtime_entry.py +16 -0
- dcc_mcp_runtime-0.2.0/docs/ADR-0001-shared-runtime.md +24 -0
- dcc_mcp_runtime-0.2.0/docs/migration/adobe.md +18 -0
- dcc_mcp_runtime-0.2.0/docs/migration/capcut.md +11 -0
- dcc_mcp_runtime-0.2.0/docs/migration/obs.md +11 -0
- dcc_mcp_runtime-0.2.0/pyoxidizer.bzl +39 -0
- dcc_mcp_runtime-0.2.0/pyproject.toml +35 -0
- dcc_mcp_runtime-0.2.0/release-please-config.json +17 -0
- dcc_mcp_runtime-0.2.0/runtime/manifest.json +12 -0
- dcc_mcp_runtime-0.2.0/runtime/manifests/adobe.json +11 -0
- dcc_mcp_runtime-0.2.0/runtime/manifests/capcut.json +11 -0
- dcc_mcp_runtime-0.2.0/runtime/manifests/obs.json +11 -0
- dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/__init__.py +17 -0
- dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/bootstrap.py +38 -0
- dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/cli.py +25 -0
- dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/handshake.py +55 -0
- dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/lifecycle.py +51 -0
- dcc_mcp_runtime-0.2.0/src/dcc_mcp_runtime/manifest.py +111 -0
- dcc_mcp_runtime-0.2.0/tests/test_bootstrap.py +14 -0
- dcc_mcp_runtime-0.2.0/tests/test_runtime.py +59 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
19
|
+
python-version: ["3.9", "3.11", "3.12"]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install package and test dependencies
|
|
26
|
+
run: python -m pip install -e ".[dev]"
|
|
27
|
+
- name: Test
|
|
28
|
+
run: python -m pytest -q
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: python -m ruff check src tests
|
|
31
|
+
- name: Format check
|
|
32
|
+
run: python -m ruff format --check src tests
|
|
33
|
+
- name: Validate manifests
|
|
34
|
+
shell: bash
|
|
35
|
+
run: |
|
|
36
|
+
python -m dcc_mcp_runtime.cli validate-manifest runtime/manifest.json
|
|
37
|
+
for f in runtime/manifests/*.json; do
|
|
38
|
+
python -m dcc_mcp_runtime.cli validate-manifest "$f" >/dev/null
|
|
39
|
+
done
|
|
40
|
+
- name: Build artifacts
|
|
41
|
+
run: python -m build
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
pull-requests: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release-please:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
outputs:
|
|
16
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
17
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
18
|
+
version: ${{ steps.release.outputs.version }}
|
|
19
|
+
steps:
|
|
20
|
+
- id: release
|
|
21
|
+
uses: googleapis/release-please-action@v4
|
|
22
|
+
with:
|
|
23
|
+
config-file: release-please-config.json
|
|
24
|
+
manifest-file: .release-please-manifest.json
|
|
25
|
+
|
|
26
|
+
build-release:
|
|
27
|
+
needs: release-please
|
|
28
|
+
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
permissions:
|
|
31
|
+
contents: write
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
with:
|
|
35
|
+
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
36
|
+
- uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
- name: Install package and release tooling
|
|
40
|
+
run: python -m pip install -e ".[dev]"
|
|
41
|
+
- name: Test release commit
|
|
42
|
+
run: python -m pytest -q
|
|
43
|
+
- name: Lint release commit
|
|
44
|
+
run: python -m ruff check src tests
|
|
45
|
+
- name: Check formatting
|
|
46
|
+
run: python -m ruff format --check src tests
|
|
47
|
+
- name: Validate manifests
|
|
48
|
+
run: |
|
|
49
|
+
python -m dcc_mcp_runtime.cli validate-manifest runtime/manifest.json
|
|
50
|
+
for f in runtime/manifests/*.json; do python -m dcc_mcp_runtime.cli validate-manifest "$f" >/dev/null; done
|
|
51
|
+
- name: Build release artifacts
|
|
52
|
+
run: python -m build
|
|
53
|
+
- name: Upload release artifacts
|
|
54
|
+
uses: softprops/action-gh-release@v2
|
|
55
|
+
with:
|
|
56
|
+
tag_name: ${{ needs.release-please.outputs.tag_name }}
|
|
57
|
+
files: |
|
|
58
|
+
dist/*
|
|
59
|
+
runtime/manifest.json
|
|
60
|
+
runtime/manifests/*.json
|
|
61
|
+
docs/migration/*.md
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0](https://github.com/dcc-mcp/dcc-mcp-runtime/compare/v0.1.0...v0.2.0) (2026-09-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* route OBS through shared runtime ([42dc3e7](https://github.com/dcc-mcp/dcc-mcp-runtime/commit/42dc3e7950309280edc215662d5793601f68de87))
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
- Initial shared external-adapter runtime contract.
|
|
13
|
+
- Added manifest validation, capability handshake, install-plan lifecycle, and
|
|
14
|
+
PyOxidizer packaging scaffold.
|
|
15
|
+
- Added migration entrypoints for Adobe, CapCut, and OBS.
|
|
16
|
+
|
|
17
|
+
This first release is a prerelease scaffold. Runtime and adapter signatures,
|
|
18
|
+
wheel hashes, and final PyOxidizer binaries must be populated before a stable
|
|
19
|
+
production release.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 loonghao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dcc-mcp-runtime
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Shared, signed runtime contract for external DCC-MCP adapters
|
|
5
|
+
Author-email: loonghao <hal.long@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: build<2,>=1.2; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff<1,>=0.8; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# dcc-mcp-runtime
|
|
16
|
+
|
|
17
|
+
Shared Python runtime for **external** DCC-MCP adapters (Adobe, CapCut and OBS).
|
|
18
|
+
The runtime is a versioned, side-by-side install containing CPython, `dcc-mcp-core`
|
|
19
|
+
and adapter wheels under `lib/site-packages`. Development still uses `uv`/wheels;
|
|
20
|
+
PyOxidizer is the reproducible distribution path.
|
|
21
|
+
|
|
22
|
+
Embedded DCC Python (Maya, Blender, Houdini, 3ds Max, etc.) is intentionally not
|
|
23
|
+
injected into this runtime. Those adapters keep their host interpreter and only
|
|
24
|
+
consume the same protocol/manifest contract.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
uv sync --extra dev
|
|
30
|
+
uv run pytest
|
|
31
|
+
uv run ruff check .
|
|
32
|
+
python -m build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`runtime/manifest.json` is the machine-readable contract. Validate an adapter
|
|
36
|
+
before loading it:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
python -m dcc_mcp_runtime.cli validate-manifest runtime/manifests/capcut.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Installation and upgrade are represented by a typed plan. The runtime never
|
|
43
|
+
executes arbitrary shell/Python: an operator must approve an exact plan and a
|
|
44
|
+
host-owned installer performs the allow-listed operation. A failed health/hash
|
|
45
|
+
verification leaves the previous runtime active for rollback.
|
|
46
|
+
|
|
47
|
+
Releases use [release-please](https://github.com/googleapis/release-please):
|
|
48
|
+
Conventional Commits on `main` create a release PR; merging it creates the tag
|
|
49
|
+
and release, then the same workflow builds and uploads the wheel, sdist, and
|
|
50
|
+
runtime/adapter manifests to that exact release tag.
|
|
51
|
+
|
|
52
|
+
See [migration guides](docs/migration/) and [ADR 0001](docs/ADR-0001-shared-runtime.md).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# dcc-mcp-runtime
|
|
2
|
+
|
|
3
|
+
Shared Python runtime for **external** DCC-MCP adapters (Adobe, CapCut and OBS).
|
|
4
|
+
The runtime is a versioned, side-by-side install containing CPython, `dcc-mcp-core`
|
|
5
|
+
and adapter wheels under `lib/site-packages`. Development still uses `uv`/wheels;
|
|
6
|
+
PyOxidizer is the reproducible distribution path.
|
|
7
|
+
|
|
8
|
+
Embedded DCC Python (Maya, Blender, Houdini, 3ds Max, etc.) is intentionally not
|
|
9
|
+
injected into this runtime. Those adapters keep their host interpreter and only
|
|
10
|
+
consume the same protocol/manifest contract.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```powershell
|
|
15
|
+
uv sync --extra dev
|
|
16
|
+
uv run pytest
|
|
17
|
+
uv run ruff check .
|
|
18
|
+
python -m build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`runtime/manifest.json` is the machine-readable contract. Validate an adapter
|
|
22
|
+
before loading it:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
python -m dcc_mcp_runtime.cli validate-manifest runtime/manifests/capcut.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Installation and upgrade are represented by a typed plan. The runtime never
|
|
29
|
+
executes arbitrary shell/Python: an operator must approve an exact plan and a
|
|
30
|
+
host-owned installer performs the allow-listed operation. A failed health/hash
|
|
31
|
+
verification leaves the previous runtime active for rollback.
|
|
32
|
+
|
|
33
|
+
Releases use [release-please](https://github.com/googleapis/release-please):
|
|
34
|
+
Conventional Commits on `main` create a release PR; merging it creates the tag
|
|
35
|
+
and release, then the same workflow builds and uploads the wheel, sdist, and
|
|
36
|
+
runtime/adapter manifests to that exact release tag.
|
|
37
|
+
|
|
38
|
+
See [migration guides](docs/migration/) and [ADR 0001](docs/ADR-0001-shared-runtime.md).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Standalone Adobe/After Effects bridge entry point.
|
|
2
|
+
|
|
3
|
+
The selected standalone DCC-MCP adapter is ``dcc-mcp-aftereffects``. The
|
|
4
|
+
``adobepy`` repository remains an SDK/broker and is not launched here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dcc_mcp_runtime.bootstrap import require_adapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
require_adapter("adobe")
|
|
12
|
+
from dcc_mcp_aftereffects.server import start_server
|
|
13
|
+
|
|
14
|
+
start_server()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
diff --git a/src/dcc_mcp_aftereffects/server.py b/src/dcc_mcp_aftereffects/server.py
|
|
2
|
+
--- a/src/dcc_mcp_aftereffects/server.py
|
|
3
|
+
+++ b/src/dcc_mcp_aftereffects/server.py
|
|
4
|
+
@@
|
|
5
|
+
from dcc_mcp_core.server_base import DccServerBase
|
|
6
|
+
+from dcc_mcp_runtime.bootstrap import require_adapter
|
|
7
|
+
@@
|
|
8
|
+
def start_server(port: Optional[int] = None) -> AfterEffectsMcpServer:
|
|
9
|
+
global _server
|
|
10
|
+
if _server is None or not _server.is_running:
|
|
11
|
+
+ require_adapter("adobe")
|
|
12
|
+
_server = AfterEffectsMcpServer(
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
diff --git a/src/dcc_mcp_capcut/server.py b/src/dcc_mcp_capcut/server.py
|
|
2
|
+
--- a/src/dcc_mcp_capcut/server.py
|
|
3
|
+
+++ b/src/dcc_mcp_capcut/server.py
|
|
4
|
+
@@
|
|
5
|
+
from dcc_mcp_core.server_base import DccServerBase
|
|
6
|
+
+from dcc_mcp_runtime.bootstrap import require_adapter
|
|
7
|
+
@@
|
|
8
|
+
def start_server(port: Optional[int] = None) -> CapCutMcpServer:
|
|
9
|
+
global _server
|
|
10
|
+
if _server is None or not _server.is_running:
|
|
11
|
+
+ require_adapter("capcut")
|
|
12
|
+
_server = CapCutMcpServer(port if port is not None else DEFAULT_PORT)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Drop-in entry point for dcc-mcp-capcut in the shared runtime."""
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_runtime.bootstrap import require_adapter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
require_adapter("capcut")
|
|
8
|
+
from dcc_mcp_capcut.server import start_server
|
|
9
|
+
|
|
10
|
+
start_server()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
main()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
diff --git a/src/dcc_mcp_obs/server.py b/src/dcc_mcp_obs/server.py
|
|
2
|
+
--- a/src/dcc_mcp_obs/server.py
|
|
3
|
+
+++ b/src/dcc_mcp_obs/server.py
|
|
4
|
+
@@
|
|
5
|
+
from dcc_mcp_core.server_base import DccServerBase
|
|
6
|
+
+from dcc_mcp_runtime.bootstrap import require_adapter
|
|
7
|
+
@@
|
|
8
|
+
def start_server(*, port: int | None = None, host_pid: int | None = None) -> ObsMcpServer:
|
|
9
|
+
global _server
|
|
10
|
+
if _server is None or not _server.is_running:
|
|
11
|
+
+ require_adapter("obs")
|
|
12
|
+
if _server is not None:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Drop-in entry point for dcc-mcp-obs in the shared runtime."""
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_runtime.bootstrap import require_adapter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
require_adapter("obs")
|
|
8
|
+
# The shared runtime owns Python and transport; OBS owns its native plugin
|
|
9
|
+
# and WebSocket bridge. Avoid the legacy self-contained wrapper.
|
|
10
|
+
from dcc_mcp_obs.server import main as obs_main
|
|
11
|
+
|
|
12
|
+
obs_main()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# ADR 0001: Shared runtime for external DCC adapters
|
|
2
|
+
|
|
3
|
+
## Decision
|
|
4
|
+
|
|
5
|
+
Adobe, CapCut and OBS use a side-by-side PyOxidizer runtime containing a
|
|
6
|
+
supported CPython build, `dcc-mcp-core`, and independently versioned adapter
|
|
7
|
+
wheels in `lib/site-packages`. Adapter startup performs a manifest/ABI/
|
|
8
|
+
capability handshake before registering with the gateway.
|
|
9
|
+
|
|
10
|
+
Maya, Blender, Houdini, 3ds Max and other embedded hosts retain their native
|
|
11
|
+
Python. They may consume the manifest protocol, but are never forced to load a
|
|
12
|
+
foreign interpreter or ABI.
|
|
13
|
+
|
|
14
|
+
## Integrity and lifecycle
|
|
15
|
+
|
|
16
|
+
Every runtime and wheel records SHA-256 and an operator-issued signature. The
|
|
17
|
+
installer receives an exact typed plan, requires explicit confirmation, verifies
|
|
18
|
+
the downloaded artifact before activation, and keeps the previous side-by-side
|
|
19
|
+
version until health and capability checks pass. Failed activation restores the
|
|
20
|
+
previous version. The runtime has no generic shell, registry, or raw Python
|
|
21
|
+
execution API.
|
|
22
|
+
|
|
23
|
+
PyOxidizer is a publishing mechanism; `uv sync`, wheel builds and normal Python
|
|
24
|
+
tests remain the development path.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Adobe migration
|
|
2
|
+
|
|
3
|
+
## Selected standalone adapter
|
|
4
|
+
|
|
5
|
+
The current standalone DCC-MCP bridge is `F:\github\dcc-mcp-aftereffects`: it
|
|
6
|
+
contains `DccServerBase`, a CEP bridge broker and MCP server lifecycle. The
|
|
7
|
+
`F:\github\dcc-mcp-adobepy` repository is an Adobe SDK/broker and remains a
|
|
8
|
+
library/transport dependency; it is not treated as the standalone adapter.
|
|
9
|
+
|
|
10
|
+
Keep the existing Photoshop/After Effects bridge and host plug-in in Adobe's
|
|
11
|
+
native process. Replace only the standalone service launch with the shared
|
|
12
|
+
runtime executable and pass the adapter wheel through the operator-owned
|
|
13
|
+
`adapter_wheels` allow-list. Do not import the runtime into CEP/UXP or inject
|
|
14
|
+
CPython into an Adobe process.
|
|
15
|
+
|
|
16
|
+
At startup load `runtime/manifests/adobe.json`, call `negotiate`, then register
|
|
17
|
+
the resulting `capabilities_fingerprint`. A failed ABI/core/signature check is
|
|
18
|
+
reported as not-ready and leaves the previous runtime active.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# CapCut migration
|
|
2
|
+
|
|
3
|
+
CapCut is an external bridge: package `dcc-mcp-capcut` as a wheel and install it
|
|
4
|
+
under `lib/site-packages` of the shared runtime. Keep CapCut Desktop UI control,
|
|
5
|
+
token bridge and installation lifecycle in the adapter. The runtime only owns
|
|
6
|
+
process startup, MCP/gateway registration and typed handshake.
|
|
7
|
+
|
|
8
|
+
Use the exact install plan from `dcc_mcp_runtime.lifecycle.plan_install`; route
|
|
9
|
+
execution through `ui_control__system_operation` with an operator grant. Never
|
|
10
|
+
add a PowerShell or arbitrary Python fallback. Preserve side-by-side rollback
|
|
11
|
+
when CapCut or the runtime fails readiness.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# OBS migration
|
|
2
|
+
|
|
3
|
+
OBS is an external native/WebSocket bridge. Build `dcc-mcp-obs` as a wheel and
|
|
4
|
+
place it in the runtime's `lib/site-packages`; start `adapters/obs_runtime_entry.py`
|
|
5
|
+
from the shared runtime. Keep OBS process discovery, WebSocket authentication
|
|
6
|
+
and scene operations in that adapter. Do not load the shared runtime into OBS's
|
|
7
|
+
own plug-in Python.
|
|
8
|
+
|
|
9
|
+
Use the existing OBS acceptance and readiness checks after handshake. Upgrade
|
|
10
|
+
only after wheel hash/signature verification and a successful loopback health
|
|
11
|
+
probe; retain the previous runtime for rollback.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""PyOxidizer distribution for the shared external-adapter runtime.
|
|
2
|
+
|
|
3
|
+
Build with `pyoxidizer build --var core_version=0.20.14`. Adapter wheels are
|
|
4
|
+
provided through the `DCC_MCP_RUNTIME_ADAPTER_WHEELS` build variable (a
|
|
5
|
+
semicolon-separated, operator-supplied allow-list). No network or arbitrary
|
|
6
|
+
commands are executed by this file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def make_exe():
|
|
10
|
+
dist = default_python_distribution()
|
|
11
|
+
policy = dist.make_python_packaging_policy()
|
|
12
|
+
policy.resources_location = "filesystem-relative:lib"
|
|
13
|
+
python_config = dist.make_python_interpreter_config()
|
|
14
|
+
python_config.oxidized_importer = False
|
|
15
|
+
python_config.filesystem_importer = True
|
|
16
|
+
python_config.module_search_paths = ["$ORIGIN/lib/site-packages"]
|
|
17
|
+
python_config.run_module = "dcc_mcp_runtime.cli"
|
|
18
|
+
python_config.parse_argv = True
|
|
19
|
+
exe = dist.to_python_executable(
|
|
20
|
+
name="dcc-mcp-runtime",
|
|
21
|
+
packaging_policy=policy,
|
|
22
|
+
config=python_config,
|
|
23
|
+
)
|
|
24
|
+
wheels = VARS.get("adapter_wheels", "").split(";")
|
|
25
|
+
wheels = [wheel for wheel in wheels if wheel]
|
|
26
|
+
resources = exe.pip_install([".", "dcc-mcp-core=={}".format(VARS["core_version"])])
|
|
27
|
+
if wheels:
|
|
28
|
+
resources += exe.pip_install(wheels)
|
|
29
|
+
exe.add_python_resources(resources)
|
|
30
|
+
return exe
|
|
31
|
+
|
|
32
|
+
def make_install(exe):
|
|
33
|
+
files = FileManifest()
|
|
34
|
+
files.add_python_resource(".", exe)
|
|
35
|
+
return files
|
|
36
|
+
|
|
37
|
+
register_target("exe", make_exe)
|
|
38
|
+
register_target("install", make_install, depends=["exe"], default=True)
|
|
39
|
+
resolve_targets()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dcc-mcp-runtime"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Shared, signed runtime contract for external DCC-MCP adapters"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "loonghao", email = "hal.long@outlook.com" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = ["build>=1.2,<2", "pytest>=8,<9", "ruff>=0.8,<1"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
dcc-mcp-runtime = "dcc_mcp_runtime.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/dcc_mcp_runtime"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
pythonpath = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
target-version = "py39"
|
|
30
|
+
line-length = 100
|
|
31
|
+
src = ["src", "tests"]
|
|
32
|
+
exclude = ["build", "dist", ".venv", ".pytest_cache", ".ruff_cache"]
|
|
33
|
+
|
|
34
|
+
[tool.ruff.lint]
|
|
35
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packages": {
|
|
3
|
+
".": {
|
|
4
|
+
"release-type": "python",
|
|
5
|
+
"package-name": "dcc-mcp-runtime",
|
|
6
|
+
"changelog-path": "CHANGELOG.md",
|
|
7
|
+
"include-component-in-tag": false,
|
|
8
|
+
"extra-files": [
|
|
9
|
+
{
|
|
10
|
+
"type": "toml",
|
|
11
|
+
"path": "pyproject.toml",
|
|
12
|
+
"jsonpath": "$.project.version"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"runtime_id": "dcc-mcp-external",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"python_version": "3.11",
|
|
6
|
+
"python_abi": "cp311-win_amd64",
|
|
7
|
+
"platform": "windows-x86_64",
|
|
8
|
+
"dcc_mcp_core": ">=0.19.13,<1.0.0",
|
|
9
|
+
"runtime_sha256": null,
|
|
10
|
+
"signature": null,
|
|
11
|
+
"adapters": []
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"adapter_id": "adobe",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"dcc_mcp_core": ">=0.19.13,<1.0.0",
|
|
6
|
+
"python_abi": "cp311-win_amd64",
|
|
7
|
+
"capabilities": ["documents", "layers", "timeline", "media", "export"],
|
|
8
|
+
"wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
9
|
+
"signature": null,
|
|
10
|
+
"host_mode": "external"
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"adapter_id": "capcut",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"dcc_mcp_core": ">=0.19.13,<1.0.0",
|
|
6
|
+
"python_abi": "cp311-win_amd64",
|
|
7
|
+
"capabilities": ["project", "media", "timeline", "captions", "audio", "effects", "export"],
|
|
8
|
+
"wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
9
|
+
"signature": null,
|
|
10
|
+
"host_mode": "external"
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1",
|
|
3
|
+
"adapter_id": "obs",
|
|
4
|
+
"version": "1.4.0",
|
|
5
|
+
"dcc_mcp_core": ">=0.19.13,<1.0.0",
|
|
6
|
+
"python_abi": "cp311-win_amd64",
|
|
7
|
+
"capabilities": ["scenes", "sources", "streaming", "recording", "replay-buffer"],
|
|
8
|
+
"wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
|
|
9
|
+
"signature": null,
|
|
10
|
+
"host_mode": "external"
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Shared runtime contracts for external DCC-MCP adapters."""
|
|
2
|
+
|
|
3
|
+
from .handshake import CapabilityHandshake, HandshakeResult, negotiate
|
|
4
|
+
from .lifecycle import InstallPlan, InstallResult, plan_install
|
|
5
|
+
from .manifest import AdapterManifest, RuntimeManifest, load_manifest
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AdapterManifest",
|
|
9
|
+
"CapabilityHandshake",
|
|
10
|
+
"HandshakeResult",
|
|
11
|
+
"InstallPlan",
|
|
12
|
+
"InstallResult",
|
|
13
|
+
"RuntimeManifest",
|
|
14
|
+
"load_manifest",
|
|
15
|
+
"negotiate",
|
|
16
|
+
"plan_install",
|
|
17
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Compatibility bootstrap used by external adapter entry points.
|
|
2
|
+
|
|
3
|
+
It performs only manifest loading and negotiation. Process spawning, host UI
|
|
4
|
+
control and installer execution remain owned by each adapter/core.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .handshake import HandshakeResult, negotiate
|
|
13
|
+
from .manifest import AdapterManifest, RuntimeManifest, load_manifest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def runtime_root() -> Path:
|
|
17
|
+
"""Resolve the side-by-side runtime root without consulting the shell."""
|
|
18
|
+
configured = os.environ.get("DCC_MCP_RUNTIME_ROOT")
|
|
19
|
+
if configured:
|
|
20
|
+
return Path(configured).resolve()
|
|
21
|
+
# Installed package: <root>/lib/site-packages/dcc_mcp_runtime.
|
|
22
|
+
return Path(__file__).resolve().parents[3]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def negotiate_adapter(adapter_id: str) -> HandshakeResult:
|
|
26
|
+
root = runtime_root()
|
|
27
|
+
runtime = load_manifest(root / "runtime" / "manifest.json")
|
|
28
|
+
adapter = load_manifest(root / "runtime" / "manifests" / f"{adapter_id}.json")
|
|
29
|
+
if not isinstance(runtime, RuntimeManifest) or not isinstance(adapter, AdapterManifest):
|
|
30
|
+
raise ValueError("runtime or adapter manifest has an invalid shape")
|
|
31
|
+
return negotiate(runtime, adapter)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def require_adapter(adapter_id: str) -> HandshakeResult:
|
|
35
|
+
result = negotiate_adapter(adapter_id)
|
|
36
|
+
if not result.accepted:
|
|
37
|
+
raise RuntimeError(f"DCC_MCP_RUNTIME_HANDSHAKE_FAILED:{result.reason}")
|
|
38
|
+
return result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Small validation CLI used by installers and CI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from .manifest import load_manifest
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(prog="dcc-mcp-runtime")
|
|
13
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
14
|
+
validate = sub.add_parser("validate-manifest")
|
|
15
|
+
validate.add_argument("path")
|
|
16
|
+
args = parser.parse_args()
|
|
17
|
+
if args.command == "validate-manifest":
|
|
18
|
+
manifest = load_manifest(args.path)
|
|
19
|
+
print(json.dumps(manifest.to_dict(), ensure_ascii=False, indent=2))
|
|
20
|
+
return 0
|
|
21
|
+
return 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__": # pragma: no cover - exercised by the CLI smoke path
|
|
25
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Runtime/adapter capability negotiation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .manifest import AdapterManifest, RuntimeManifest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class CapabilityHandshake:
|
|
12
|
+
runtime_id: str
|
|
13
|
+
runtime_version: str
|
|
14
|
+
python_abi: str
|
|
15
|
+
dcc_mcp_core: str
|
|
16
|
+
adapter_id: str
|
|
17
|
+
adapter_version: str
|
|
18
|
+
capabilities: tuple[str, ...]
|
|
19
|
+
capabilities_fingerprint: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class HandshakeResult:
|
|
24
|
+
accepted: bool
|
|
25
|
+
reason: str
|
|
26
|
+
handshake: CapabilityHandshake | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def negotiate(runtime: RuntimeManifest, adapter: AdapterManifest) -> HandshakeResult:
|
|
30
|
+
if adapter.host_mode == "embedded":
|
|
31
|
+
return HandshakeResult(False, "embedded_adapter_requires_host_runtime")
|
|
32
|
+
if runtime.python_abi != adapter.python_abi:
|
|
33
|
+
return HandshakeResult(False, "python_abi_mismatch")
|
|
34
|
+
if runtime.dcc_mcp_core != adapter.dcc_mcp_core:
|
|
35
|
+
return HandshakeResult(False, "dcc_mcp_core_mismatch")
|
|
36
|
+
capabilities = tuple(sorted(set(adapter.capabilities)))
|
|
37
|
+
# The runtime does not sign/authorise capabilities; it carries a stable
|
|
38
|
+
# handshake value for the gateway to compare with its catalog.
|
|
39
|
+
import hashlib
|
|
40
|
+
|
|
41
|
+
fingerprint = hashlib.sha256("\n".join(capabilities).encode()).hexdigest()
|
|
42
|
+
return HandshakeResult(
|
|
43
|
+
True,
|
|
44
|
+
"accepted",
|
|
45
|
+
CapabilityHandshake(
|
|
46
|
+
runtime.runtime_id,
|
|
47
|
+
runtime.version,
|
|
48
|
+
runtime.python_abi,
|
|
49
|
+
runtime.dcc_mcp_core,
|
|
50
|
+
adapter.adapter_id,
|
|
51
|
+
adapter.version,
|
|
52
|
+
capabilities,
|
|
53
|
+
fingerprint,
|
|
54
|
+
),
|
|
55
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Consent-gated, verifiable and rollback-safe install planning."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class InstallPlan:
|
|
10
|
+
operation_id: str
|
|
11
|
+
runtime_id: str
|
|
12
|
+
target_version: str
|
|
13
|
+
package_path: str
|
|
14
|
+
expected_sha256: str
|
|
15
|
+
previous_version: str | None
|
|
16
|
+
requires_confirmation: bool = True
|
|
17
|
+
rollback_version: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class InstallResult:
|
|
22
|
+
operation_id: str
|
|
23
|
+
status: str
|
|
24
|
+
active_version: str | None
|
|
25
|
+
reason: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def plan_install(
|
|
29
|
+
*,
|
|
30
|
+
operation_id: str,
|
|
31
|
+
runtime_id: str,
|
|
32
|
+
target_version: str,
|
|
33
|
+
package_path: str,
|
|
34
|
+
expected_sha256: str,
|
|
35
|
+
previous_version: str | None = None,
|
|
36
|
+
) -> InstallPlan:
|
|
37
|
+
"""Return an exact plan; execution belongs to a host-owned allow-list."""
|
|
38
|
+
if not operation_id or not runtime_id or not target_version or not package_path:
|
|
39
|
+
raise ValueError("operation_id, runtime_id, target_version and package_path are required")
|
|
40
|
+
if len(expected_sha256) != 64:
|
|
41
|
+
raise ValueError("expected_sha256 must be a 64-character SHA-256 hex digest")
|
|
42
|
+
return InstallPlan(
|
|
43
|
+
operation_id,
|
|
44
|
+
runtime_id,
|
|
45
|
+
target_version,
|
|
46
|
+
package_path,
|
|
47
|
+
expected_sha256,
|
|
48
|
+
previous_version,
|
|
49
|
+
True,
|
|
50
|
+
previous_version,
|
|
51
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Strict, serialisable runtime and adapter manifests.
|
|
2
|
+
|
|
3
|
+
The manifest is deliberately data-only so it can be verified before importing
|
|
4
|
+
any adapter code. Hash/signature fields are opaque strings; signature checking
|
|
5
|
+
is delegated to the operator-owned trust service.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
SCHEMA_VERSION = "1"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AdapterManifest:
|
|
20
|
+
adapter_id: str
|
|
21
|
+
version: str
|
|
22
|
+
dcc_mcp_core: str
|
|
23
|
+
python_abi: str
|
|
24
|
+
capabilities: tuple[str, ...]
|
|
25
|
+
wheel_sha256: str
|
|
26
|
+
signature: str | None = None
|
|
27
|
+
host_mode: str = "external"
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
if not self.adapter_id or not self.version or not self.python_abi:
|
|
31
|
+
raise ValueError("adapter_id, version and python_abi are required")
|
|
32
|
+
if self.host_mode not in {"external", "embedded"}:
|
|
33
|
+
raise ValueError("host_mode must be external or embedded")
|
|
34
|
+
if len(self.wheel_sha256) != 64:
|
|
35
|
+
raise ValueError("wheel_sha256 must be a 64-character SHA-256 hex digest")
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_dict(cls, data: dict[str, Any]) -> AdapterManifest:
|
|
39
|
+
return cls(
|
|
40
|
+
adapter_id=str(data["adapter_id"]),
|
|
41
|
+
version=str(data["version"]),
|
|
42
|
+
dcc_mcp_core=str(data["dcc_mcp_core"]),
|
|
43
|
+
python_abi=str(data["python_abi"]),
|
|
44
|
+
capabilities=tuple(str(x) for x in data.get("capabilities", [])),
|
|
45
|
+
wheel_sha256=str(data["wheel_sha256"]),
|
|
46
|
+
signature=data.get("signature"),
|
|
47
|
+
host_mode=str(data.get("host_mode", "external")),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"adapter_id": self.adapter_id,
|
|
53
|
+
"version": self.version,
|
|
54
|
+
"dcc_mcp_core": self.dcc_mcp_core,
|
|
55
|
+
"python_abi": self.python_abi,
|
|
56
|
+
"capabilities": list(self.capabilities),
|
|
57
|
+
"wheel_sha256": self.wheel_sha256,
|
|
58
|
+
"signature": self.signature,
|
|
59
|
+
"host_mode": self.host_mode,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class RuntimeManifest:
|
|
65
|
+
runtime_id: str
|
|
66
|
+
version: str
|
|
67
|
+
python_version: str
|
|
68
|
+
python_abi: str
|
|
69
|
+
platform: str
|
|
70
|
+
dcc_mcp_core: str
|
|
71
|
+
adapters: tuple[AdapterManifest, ...] = field(default_factory=tuple)
|
|
72
|
+
runtime_sha256: str | None = None
|
|
73
|
+
signature: str | None = None
|
|
74
|
+
schema_version: str = SCHEMA_VERSION
|
|
75
|
+
|
|
76
|
+
def __post_init__(self) -> None:
|
|
77
|
+
if self.schema_version != SCHEMA_VERSION:
|
|
78
|
+
raise ValueError(f"unsupported manifest schema: {self.schema_version}")
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict[str, Any]:
|
|
81
|
+
return {
|
|
82
|
+
"schema_version": self.schema_version,
|
|
83
|
+
"runtime_id": self.runtime_id,
|
|
84
|
+
"version": self.version,
|
|
85
|
+
"python_version": self.python_version,
|
|
86
|
+
"python_abi": self.python_abi,
|
|
87
|
+
"platform": self.platform,
|
|
88
|
+
"dcc_mcp_core": self.dcc_mcp_core,
|
|
89
|
+
"runtime_sha256": self.runtime_sha256,
|
|
90
|
+
"signature": self.signature,
|
|
91
|
+
"adapters": [a.to_dict() for a in self.adapters],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def load_manifest(path: str | Path) -> RuntimeManifest | AdapterManifest:
|
|
96
|
+
"""Load and validate either manifest shape without importing adapters."""
|
|
97
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
98
|
+
if "runtime_id" in data:
|
|
99
|
+
return RuntimeManifest(
|
|
100
|
+
runtime_id=str(data["runtime_id"]),
|
|
101
|
+
version=str(data["version"]),
|
|
102
|
+
python_version=str(data["python_version"]),
|
|
103
|
+
python_abi=str(data["python_abi"]),
|
|
104
|
+
platform=str(data["platform"]),
|
|
105
|
+
dcc_mcp_core=str(data["dcc_mcp_core"]),
|
|
106
|
+
adapters=tuple(AdapterManifest.from_dict(x) for x in data.get("adapters", [])),
|
|
107
|
+
runtime_sha256=data.get("runtime_sha256"),
|
|
108
|
+
signature=data.get("signature"),
|
|
109
|
+
schema_version=str(data.get("schema_version", SCHEMA_VERSION)),
|
|
110
|
+
)
|
|
111
|
+
return AdapterManifest.from_dict(data)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from dcc_mcp_runtime.bootstrap import negotiate_adapter, runtime_root
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_runtime_root_can_be_explicit(monkeypatch):
|
|
7
|
+
monkeypatch.setenv("DCC_MCP_RUNTIME_ROOT", str(Path.cwd()))
|
|
8
|
+
assert runtime_root() == Path.cwd().resolve()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_capcut_and_obs_compatibility_entries_handshake(monkeypatch):
|
|
12
|
+
monkeypatch.setenv("DCC_MCP_RUNTIME_ROOT", str(Path.cwd()))
|
|
13
|
+
assert negotiate_adapter("capcut").accepted
|
|
14
|
+
assert negotiate_adapter("obs").accepted
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from dcc_mcp_runtime.handshake import negotiate
|
|
2
|
+
from dcc_mcp_runtime.lifecycle import plan_install
|
|
3
|
+
from dcc_mcp_runtime.manifest import AdapterManifest, RuntimeManifest, load_manifest
|
|
4
|
+
|
|
5
|
+
HASH = "a" * 64
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def runtime() -> RuntimeManifest:
|
|
9
|
+
return RuntimeManifest(
|
|
10
|
+
runtime_id="test-runtime",
|
|
11
|
+
version="1.0.0",
|
|
12
|
+
python_version="3.11",
|
|
13
|
+
python_abi="cp311-win_amd64",
|
|
14
|
+
platform="windows-x86_64",
|
|
15
|
+
dcc_mcp_core=">=0.19.13,<1.0.0",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def adapter(**overrides) -> AdapterManifest:
|
|
20
|
+
values = dict(
|
|
21
|
+
adapter_id="capcut",
|
|
22
|
+
version="0.1.0",
|
|
23
|
+
dcc_mcp_core=">=0.19.13,<1.0.0",
|
|
24
|
+
python_abi="cp311-win_amd64",
|
|
25
|
+
capabilities=("timeline", "media"),
|
|
26
|
+
wheel_sha256=HASH,
|
|
27
|
+
)
|
|
28
|
+
values.update(overrides)
|
|
29
|
+
return AdapterManifest(**values)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_external_handshake_includes_stable_fingerprint():
|
|
33
|
+
result = negotiate(runtime(), adapter())
|
|
34
|
+
assert result.accepted is True
|
|
35
|
+
assert result.handshake is not None
|
|
36
|
+
assert len(result.handshake.capabilities_fingerprint) == 64
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_embedded_adapter_is_not_injected():
|
|
40
|
+
result = negotiate(runtime(), adapter(host_mode="embedded"))
|
|
41
|
+
assert result == result.__class__(False, "embedded_adapter_requires_host_runtime")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_install_plan_requires_confirmation_and_rollback():
|
|
45
|
+
plan = plan_install(
|
|
46
|
+
operation_id="op-1",
|
|
47
|
+
runtime_id="dcc-mcp-external",
|
|
48
|
+
target_version="0.2.0",
|
|
49
|
+
package_path="runtime-0.2.0.zip",
|
|
50
|
+
expected_sha256=HASH,
|
|
51
|
+
previous_version="0.1.0",
|
|
52
|
+
)
|
|
53
|
+
assert plan.requires_confirmation
|
|
54
|
+
assert plan.rollback_version == "0.1.0"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_runtime_manifest_is_loadable():
|
|
58
|
+
manifest = load_manifest("runtime/manifest.json")
|
|
59
|
+
assert manifest.runtime_id == "dcc-mcp-external"
|