ue-mcp 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flow/guard.d.ts +26 -37
- package/dist/flow/guard.js +40 -25
- package/dist/flow/guard.js.map +1 -1
- package/dist/flow/guarded-bridge.d.ts +0 -10
- package/dist/flow/guarded-bridge.js +18 -23
- package/dist/flow/guarded-bridge.js.map +1 -1
- package/dist/flow/schema.d.ts +276 -16
- package/dist/flow/task-guards.d.ts +1 -1
- package/dist/flow/task-guards.js +44 -68
- package/dist/flow/task-guards.js.map +1 -1
- package/dist/tool-counts.json +2 -2
- package/package.json +7 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +10 -32
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -21
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +3 -18
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +1 -19
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +30 -0
package/dist/flow/task-guards.js
CHANGED
|
@@ -10,20 +10,29 @@
|
|
|
10
10
|
* - `afterWrite` run after a write-classified call
|
|
11
11
|
*
|
|
12
12
|
* No new plugin-activation concept is needed: the loader already registers every
|
|
13
|
-
* `manifest.tasks` entry by name, so a `guard.*.*` task is picked up here.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* just guards.
|
|
13
|
+
* `manifest.tasks` entry by name, so a `guard.*.*` task is picked up here. The
|
|
14
|
+
* core knows nothing about what a guard does - source control, access policy,
|
|
15
|
+
* audit, and rate limiting are all just guards.
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* The naming convention and the discovery walk are flowkit's; this module binds
|
|
18
|
+
* them to the bridge: the `write` scope comes from `write-methods.ts`, the guard
|
|
19
|
+
* task is invoked with `{ method, params, paths }` (plus `result` for `after`
|
|
20
|
+
* guards), and a denial surfaces as an `McpError` the tool layer already knows
|
|
21
|
+
* how to render.
|
|
22
22
|
*/
|
|
23
23
|
import * as fs from "node:fs";
|
|
24
|
+
import { discoverTaskGuards as discoverFlowkitGuards } from "@db-lyon/flowkit/guard";
|
|
25
|
+
import { writeScope } from "./guard.js";
|
|
24
26
|
import { McpError, ErrorCode } from "../errors.js";
|
|
25
|
-
import { debug } from "../log.js";
|
|
26
|
-
|
|
27
|
+
import { debug, info } from "../log.js";
|
|
28
|
+
/** Adapts flowkit's logger to this server's `guard`-component logging. */
|
|
29
|
+
const GUARD_LOGGER = {
|
|
30
|
+
debug: (...args) => debug("guard", args.map(String).join(" ")),
|
|
31
|
+
info: (...args) => info("guard", args.map(String).join(" ")),
|
|
32
|
+
warn: (...args) => info("guard", args.map(String).join(" ")),
|
|
33
|
+
error: (...args) => info("guard", args.map(String).join(" ")),
|
|
34
|
+
child: () => GUARD_LOGGER,
|
|
35
|
+
};
|
|
27
36
|
/** Resolve a UE content path to an absolute file, or null if it does not exist. */
|
|
28
37
|
export function makeResolveExistingFile(project) {
|
|
29
38
|
return (contentPath) => {
|
|
@@ -42,63 +51,30 @@ export function makeResolveExistingFile(project) {
|
|
|
42
51
|
* guard that itself calls the bridge cannot recurse through the pipeline.
|
|
43
52
|
*/
|
|
44
53
|
export function discoverTaskGuards(registry, ctx, rawBridge) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return await task.run();
|
|
71
|
-
}
|
|
72
|
-
catch (e) {
|
|
73
|
-
throw new McpError(ErrorCode.WRITE_BLOCKED, `guard '${name}' errored on ${cc.method}: ${e.message}`);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
const guard = {
|
|
77
|
-
name: `${name}.${phase}`,
|
|
78
|
-
appliesTo: writeScoped ? (cc) => cc.writeFiles().length > 0 : undefined,
|
|
79
|
-
};
|
|
80
|
-
if (isBefore) {
|
|
81
|
-
guard.before = async (cc) => {
|
|
82
|
-
const r = await runTask(cc);
|
|
83
|
-
if (!r.success) {
|
|
84
|
-
const reason = r.error?.message ?? `denied by guard '${name}'`;
|
|
85
|
-
const scope = cc.writeFiles().length ? ` on ${cc.writeFiles().join(", ")}` : "";
|
|
86
|
-
throw new McpError(ErrorCode.WRITE_BLOCKED, `blocked (${cc.method})${scope}: ${reason}`);
|
|
87
|
-
}
|
|
88
|
-
debug("guard", `guard '${name}' allowed ${cc.method}`);
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
// `after` guards observe the result for side effects (audit); a failing
|
|
93
|
-
// after-guard is logged but does not fail the already-completed call.
|
|
94
|
-
guard.after = async (cc, result) => {
|
|
95
|
-
const r = await runTask(cc, result);
|
|
96
|
-
if (!r.success)
|
|
97
|
-
debug("guard", `after-guard '${name}' reported failure on ${cc.method}: ${r.error?.message}`);
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
guards.push(guard);
|
|
101
|
-
}
|
|
102
|
-
return guards;
|
|
54
|
+
return discoverFlowkitGuards(registry, {
|
|
55
|
+
scopes: { write: writeScope },
|
|
56
|
+
// Bind the guard to the editor whose call it is guarding, not to whichever
|
|
57
|
+
// bridge happened to be built first. cc.bridge is the RAW bridge of the
|
|
58
|
+
// session serving this call, so a guard can neither recurse through the
|
|
59
|
+
// pipeline nor act on another project's editor.
|
|
60
|
+
contextFor: (cc) => cc.session
|
|
61
|
+
? { ...ctx, bridge: cc.bridge, project: cc.session.project, session: cc.session }
|
|
62
|
+
: { ...ctx, bridge: rawBridge },
|
|
63
|
+
optionsFor: (cc, result) => ({
|
|
64
|
+
method: cc.method,
|
|
65
|
+
params: cc.params,
|
|
66
|
+
paths: cc.writeFiles(),
|
|
67
|
+
...(result !== undefined ? { result } : {}),
|
|
68
|
+
}),
|
|
69
|
+
onDeny: (info) => {
|
|
70
|
+
const files = info.ctx.writeFiles();
|
|
71
|
+
const scope = files.length ? ` on ${files.join(", ")}` : "";
|
|
72
|
+
return new McpError(ErrorCode.WRITE_BLOCKED, `blocked (${info.ctx.method})${scope}: ${info.reason}`);
|
|
73
|
+
},
|
|
74
|
+
onError: (info) => new McpError(ErrorCode.WRITE_BLOCKED, `guard '${info.guard}' errored on ${info.ctx.method}: ${info.reason}`),
|
|
75
|
+
// A failing after-guard is logged but does not fail the already-completed call.
|
|
76
|
+
onAfterFailure: (info) => debug("guard", `after-guard '${info.guard}' reported failure on ${info.ctx.method}: ${info.reason}`),
|
|
77
|
+
logger: GUARD_LOGGER,
|
|
78
|
+
});
|
|
103
79
|
}
|
|
104
80
|
//# sourceMappingURL=task-guards.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-guards.js","sourceRoot":"","sources":["../../src/flow/task-guards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"task-guards.js","sourceRoot":"","sources":["../../src/flow/task-guards.ts"],"names":[],"mappings":"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"}
|
package/dist/tool-counts.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ue-mcp",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Unreal Engine MCP server - 24 tools, 783+ actions for AI-driven editor control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
"audit:docs": "node scripts/audit-docs.mjs && node scripts/audit-params.mjs",
|
|
62
62
|
"audit:handlers": "node scripts/audit-handlers.mjs",
|
|
63
63
|
"audit:em-dash": "node scripts/check-em-dashes.mjs",
|
|
64
|
-
"audit": "
|
|
64
|
+
"audit:unity": "node scripts/audit-unity-collisions.mjs",
|
|
65
|
+
"audit": "npm run audit:docs && npm run audit:handlers && npm run audit:em-dash && npm run audit:unity",
|
|
65
66
|
"test:watch": "vitest",
|
|
66
67
|
"test:level": "vitest run tests/smoke/level.test.ts",
|
|
67
68
|
"test:asset": "vitest run tests/smoke/asset.test.ts",
|
|
@@ -84,10 +85,11 @@
|
|
|
84
85
|
"generate": "tsx scripts/generate-default-config.ts",
|
|
85
86
|
"generate:metadata": "tsx scripts/generate-tool-metadata.ts",
|
|
86
87
|
"prepublishOnly": "npx tsc && tsx scripts/generate-default-config.ts && tsx scripts/generate-tool-metadata.ts",
|
|
87
|
-
"gen:epic-docs": "npx tsc && node scripts/gen-epic-docs.mjs"
|
|
88
|
+
"gen:epic-docs": "npx tsc && node scripts/gen-epic-docs.mjs",
|
|
89
|
+
"prepare": "husky"
|
|
88
90
|
},
|
|
89
91
|
"dependencies": {
|
|
90
|
-
"@db-lyon/flowkit": "
|
|
92
|
+
"@db-lyon/flowkit": "^0.14.0",
|
|
91
93
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
92
94
|
"js-yaml": "^4.1.0",
|
|
93
95
|
"ws": "^8.18.0",
|
|
@@ -97,6 +99,7 @@
|
|
|
97
99
|
"@types/js-yaml": "^4.0.9",
|
|
98
100
|
"@types/node": "^22.0.0",
|
|
99
101
|
"@types/ws": "^8.5.0",
|
|
102
|
+
"husky": "^9.1.7",
|
|
100
103
|
"tsx": "^4.19.0",
|
|
101
104
|
"typescript": "^5.7.0",
|
|
102
105
|
"vitest": "^3.2.4"
|
|
@@ -75,32 +75,10 @@
|
|
|
75
75
|
#include "AI/Navigation/NavCollisionBase.h"
|
|
76
76
|
|
|
77
77
|
// ─── Protected mount guardrail ──────────────────────────────────────────
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// UEditorAssetLibrary::DeleteAsset will happily destroy files under
|
|
81
|
-
// <engineRoot>/Engine/Content/ if not stopped - verified the hard way.
|
|
82
|
-
// Apply this check to every handler that deletes, moves, or renames an
|
|
83
|
-
// asset. Plugin content roots (mounted under /<PluginName>/) are NOT
|
|
84
|
-
// protected here; per-project plugin content is expected to be writable.
|
|
78
|
+
// The rule itself is MCPIsProtectedAssetPath in HandlerUtils.h, shared by
|
|
79
|
+
// every asset translation unit so the write paths cannot drift apart.
|
|
85
80
|
namespace
|
|
86
81
|
{
|
|
87
|
-
bool IsProtectedAssetPath(const FString& Path)
|
|
88
|
-
{
|
|
89
|
-
FString P = Path;
|
|
90
|
-
P.TrimStartAndEndInline();
|
|
91
|
-
if (P.IsEmpty()) return false;
|
|
92
|
-
// Tolerate leading whitespace and the surface form (no leading slash).
|
|
93
|
-
if (!P.StartsWith(TEXT("/"))) P = TEXT("/") + P;
|
|
94
|
-
const FString L = P.ToLower();
|
|
95
|
-
if (L.StartsWith(TEXT("/engine/"))) return true;
|
|
96
|
-
if (L.StartsWith(TEXT("/script/"))) return true;
|
|
97
|
-
if (L.StartsWith(TEXT("/memory/"))) return true;
|
|
98
|
-
if (L.StartsWith(TEXT("/temp/"))) return true;
|
|
99
|
-
// Verse runtime objects surface as /Script/CoreUObject.* etc.
|
|
100
|
-
if (L.Contains(TEXT("/script/"))) return true;
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
82
|
TSharedPtr<FJsonValue> MakeProtectedPathError(const FString& Path)
|
|
105
83
|
{
|
|
106
84
|
return MCPError(FString::Printf(
|
|
@@ -1488,8 +1466,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::RenameAsset(const TSharedPtr<FJsonObject>
|
|
|
1488
1466
|
return MCPError(TEXT("Missing 'sourcePath'+'destinationPath' or 'assetPath'+'newName'"));
|
|
1489
1467
|
}
|
|
1490
1468
|
|
|
1491
|
-
if (
|
|
1492
|
-
if (
|
|
1469
|
+
if (MCPIsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
|
|
1470
|
+
if (MCPIsProtectedAssetPath(DestPath)) return MakeProtectedPathError(DestPath);
|
|
1493
1471
|
|
|
1494
1472
|
// Idempotency: if already at destination, no-op.
|
|
1495
1473
|
if (SourcePath == DestPath)
|
|
@@ -1697,7 +1675,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAsset(const TSharedPtr<FJsonObject>
|
|
|
1697
1675
|
FString AssetPath;
|
|
1698
1676
|
if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
|
|
1699
1677
|
|
|
1700
|
-
if (
|
|
1678
|
+
if (MCPIsProtectedAssetPath(AssetPath)) return MakeProtectedPathError(AssetPath);
|
|
1701
1679
|
|
|
1702
1680
|
const bool bForce = OptionalBool(Params, TEXT("force"), false);
|
|
1703
1681
|
|
|
@@ -1763,7 +1741,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAssetBatch(const TSharedPtr<FJsonOb
|
|
|
1763
1741
|
TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
1764
1742
|
Entry->SetStringField(TEXT("path"), Path);
|
|
1765
1743
|
|
|
1766
|
-
if (
|
|
1744
|
+
if (MCPIsProtectedAssetPath(Path))
|
|
1767
1745
|
{
|
|
1768
1746
|
Entry->SetStringField(TEXT("status"), TEXT("protected"));
|
|
1769
1747
|
Entry->SetStringField(TEXT("reason"), TEXT("Engine/Script/Memory/Temp mounts are read-only via the bridge"));
|
|
@@ -1892,7 +1870,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRename(const TSharedPtr<FJsonObject>&
|
|
|
1892
1870
|
continue;
|
|
1893
1871
|
}
|
|
1894
1872
|
|
|
1895
|
-
if (
|
|
1873
|
+
if (MCPIsProtectedAssetPath(SourcePath) || MCPIsProtectedAssetPath(NewPackagePath))
|
|
1896
1874
|
{
|
|
1897
1875
|
Record->SetStringField(TEXT("status"), TEXT("protected"));
|
|
1898
1876
|
Record->SetStringField(TEXT("reason"), TEXT("Engine/Script/Memory/Temp mounts are read-only via the bridge"));
|
|
@@ -2555,8 +2533,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::MoveFolder(const TSharedPtr<FJsonObject>&
|
|
|
2555
2533
|
SourcePath.RemoveFromEnd(TEXT("/"));
|
|
2556
2534
|
DestinationPath.RemoveFromEnd(TEXT("/"));
|
|
2557
2535
|
|
|
2558
|
-
if (
|
|
2559
|
-
if (
|
|
2536
|
+
if (MCPIsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
|
|
2537
|
+
if (MCPIsProtectedAssetPath(DestinationPath)) return MakeProtectedPathError(DestinationPath);
|
|
2560
2538
|
|
|
2561
2539
|
// Scan source path to discover all assets
|
|
2562
2540
|
IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
|
|
@@ -2738,7 +2716,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteFolder(const TSharedPtr<FJsonObject
|
|
|
2738
2716
|
continue;
|
|
2739
2717
|
}
|
|
2740
2718
|
|
|
2741
|
-
if (
|
|
2719
|
+
if (MCPIsProtectedAssetPath(Norm))
|
|
2742
2720
|
{
|
|
2743
2721
|
Entry->SetStringField(TEXT("status"), TEXT("failed"));
|
|
2744
2722
|
Entry->SetStringField(TEXT("reason"), TEXT("protected_path"));
|
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp
CHANGED
|
@@ -64,26 +64,6 @@ namespace
|
|
|
64
64
|
bool PassedPreflight() const { return Error.IsEmpty(); }
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
// Mirrors IsProtectedAssetPath in AssetHandlers.cpp, which is file-local to
|
|
68
|
-
// that translation unit. Keep the two rule sets identical: a write path that
|
|
69
|
-
// enforces a weaker rule than its neighbours is how engine content gets
|
|
70
|
-
// mutated through the bridge.
|
|
71
|
-
bool IsProtectedBulkAssetPath(const FString& Path)
|
|
72
|
-
{
|
|
73
|
-
FString Normalized = Path;
|
|
74
|
-
Normalized.TrimStartAndEndInline();
|
|
75
|
-
if (Normalized.IsEmpty()) return false;
|
|
76
|
-
if (!Normalized.StartsWith(TEXT("/"))) Normalized = TEXT("/") + Normalized;
|
|
77
|
-
const FString Lower = Normalized.ToLower();
|
|
78
|
-
if (Lower.StartsWith(TEXT("/engine/"))) return true;
|
|
79
|
-
if (Lower.StartsWith(TEXT("/script/"))) return true;
|
|
80
|
-
if (Lower.StartsWith(TEXT("/memory/"))) return true;
|
|
81
|
-
if (Lower.StartsWith(TEXT("/temp/"))) return true;
|
|
82
|
-
// Verse runtime objects surface as /Script/CoreUObject.* etc.
|
|
83
|
-
if (Lower.Contains(TEXT("/script/"))) return true;
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
67
|
TSharedPtr<FJsonObject> MakePropertyReadback(
|
|
88
68
|
const FPreparedPropertyWrite& Prepared,
|
|
89
69
|
const TSharedPtr<FJsonValue>& ActualValue,
|
|
@@ -198,7 +178,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkSetAssetProperties(const TSharedPtr<F
|
|
|
198
178
|
}
|
|
199
179
|
PreparedAsset.AssetPath = AssetPath;
|
|
200
180
|
|
|
201
|
-
if (
|
|
181
|
+
if (MCPIsProtectedAssetPath(AssetPath))
|
|
202
182
|
{
|
|
203
183
|
RejectItem(BulkStatusProtected, FString::Printf(
|
|
204
184
|
TEXT("Refusing to mutate protected mount: %s. Engine, /Script/, /Memory/, /Temp/ are read-only via the bridge."),
|
package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp
CHANGED
|
@@ -50,21 +50,6 @@ struct FPreparedUpsertItem
|
|
|
50
50
|
TArray<FPreparedProperty> Properties;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
bool IsProtectedAssetPath(const FString& Path)
|
|
54
|
-
{
|
|
55
|
-
FString Normalized = Path;
|
|
56
|
-
Normalized.TrimStartAndEndInline();
|
|
57
|
-
if (!Normalized.StartsWith(TEXT("/")))
|
|
58
|
-
{
|
|
59
|
-
Normalized = TEXT("/") + Normalized;
|
|
60
|
-
}
|
|
61
|
-
const FString Lower = Normalized.ToLower();
|
|
62
|
-
return Lower.StartsWith(TEXT("/engine/"))
|
|
63
|
-
|| Lower.StartsWith(TEXT("/script/"))
|
|
64
|
-
|| Lower.StartsWith(TEXT("/memory/"))
|
|
65
|
-
|| Lower.StartsWith(TEXT("/temp/"));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
53
|
UClass* ResolveDataAssetClass(const FString& ClassName)
|
|
69
54
|
{
|
|
70
55
|
UClass* DataClass = nullptr;
|
|
@@ -144,7 +129,7 @@ bool ParseAssetIdentity(
|
|
|
144
129
|
}
|
|
145
130
|
|
|
146
131
|
const FString LongPackageName = OutItem.PackagePath + TEXT("/") + OutItem.Name;
|
|
147
|
-
if (
|
|
132
|
+
if (MCPIsProtectedAssetPath(LongPackageName))
|
|
148
133
|
{
|
|
149
134
|
OutError = FString::Printf(TEXT("Refusing to mutate protected package '%s'"), *LongPackageName);
|
|
150
135
|
return false;
|
|
@@ -357,7 +342,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRestoreDataAssets(const TSharedPtr<FJ
|
|
|
357
342
|
FString AssetPath;
|
|
358
343
|
const TSharedPtr<FJsonObject>* Properties = nullptr;
|
|
359
344
|
if (!(*Item)->TryGetStringField(TEXT("assetPath"), AssetPath)
|
|
360
|
-
||
|
|
345
|
+
|| MCPIsProtectedAssetPath(AssetPath)
|
|
361
346
|
|| !(*Item)->TryGetObjectField(TEXT("properties"), Properties)
|
|
362
347
|
|| !Properties
|
|
363
348
|
|| !Properties->IsValid())
|
|
@@ -414,7 +399,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRestoreDataAssets(const TSharedPtr<FJ
|
|
|
414
399
|
FString AssetPath;
|
|
415
400
|
if (!(*CreatedAssetPaths)[PathIndex].IsValid()
|
|
416
401
|
|| !(*CreatedAssetPaths)[PathIndex]->TryGetString(AssetPath)
|
|
417
|
-
||
|
|
402
|
+
|| MCPIsProtectedAssetPath(AssetPath))
|
|
418
403
|
{
|
|
419
404
|
Errors.Add(MakeShared<FJsonValueString>(FString::Printf(TEXT("createdAssetPaths[%d] is invalid"), PathIndex)));
|
|
420
405
|
continue;
|
|
@@ -120,24 +120,6 @@ namespace
|
|
|
120
120
|
const TCHAR* const MeshMatStatusFailed = TEXT("failed");
|
|
121
121
|
const TCHAR* const MeshMatStatusSkipped = TEXT("skipped");
|
|
122
122
|
|
|
123
|
-
// Mirrors IsProtectedAssetPath in AssetHandlers.cpp, which is file-local to
|
|
124
|
-
// that translation unit. Keep the two rule sets identical: a write path that
|
|
125
|
-
// enforces a weaker rule than its neighbours is how engine content gets
|
|
126
|
-
// mutated through the bridge.
|
|
127
|
-
bool IsProtectedMeshMaterialPath(const FString& Path)
|
|
128
|
-
{
|
|
129
|
-
FString Normalized = Path;
|
|
130
|
-
Normalized.TrimStartAndEndInline();
|
|
131
|
-
if (Normalized.IsEmpty()) return false;
|
|
132
|
-
if (!Normalized.StartsWith(TEXT("/"))) Normalized = TEXT("/") + Normalized;
|
|
133
|
-
const FString Lower = Normalized.ToLower();
|
|
134
|
-
if (Lower.StartsWith(TEXT("/engine/"))) return true;
|
|
135
|
-
if (Lower.StartsWith(TEXT("/memory/"))) return true;
|
|
136
|
-
if (Lower.StartsWith(TEXT("/temp/"))) return true;
|
|
137
|
-
if (Lower.Contains(TEXT("/script/"))) return true;
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
123
|
struct FPreparedMeshMaterialAssignment
|
|
142
124
|
{
|
|
143
125
|
int32 ItemIndex = 0;
|
|
@@ -297,7 +279,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetMeshMaterialsBatch(const TSharedPtr<FJ
|
|
|
297
279
|
}
|
|
298
280
|
Item.AssetPath = AssetPath;
|
|
299
281
|
|
|
300
|
-
if (
|
|
282
|
+
if (MCPIsProtectedAssetPath(AssetPath))
|
|
301
283
|
{
|
|
302
284
|
Reject(MeshMatStatusProtected, FString::Printf(
|
|
303
285
|
TEXT("Refusing to mutate protected mount: %s. Engine, /Script/, /Memory/, /Temp/ are read-only via the bridge."),
|
|
@@ -135,6 +135,36 @@ inline TSharedPtr<FJsonValue> MCPCheckAssetExists(
|
|
|
135
135
|
return TSharedPtr<FJsonValue>();
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/** Protected mount guardrail. Engine-shipped content (/Engine/, /Script/,
|
|
139
|
+
* /Memory/, /Temp/) and Verse runtime classes must never be mutated through
|
|
140
|
+
* the bridge: UEditorAssetLibrary::DeleteAsset will happily destroy files
|
|
141
|
+
* under <engineRoot>/Engine/Content/ if not stopped. Every handler that
|
|
142
|
+
* deletes, moves, renames or writes an asset calls this. Plugin content roots
|
|
143
|
+
* (mounted under /<PluginName>/) are NOT protected; per-project plugin content
|
|
144
|
+
* is expected to be writable.
|
|
145
|
+
*
|
|
146
|
+
* This lives here rather than as a file-local copy per translation unit
|
|
147
|
+
* because the asset handlers are split across several files that share one
|
|
148
|
+
* unity blob: duplicate definitions collide at compile time, and independent
|
|
149
|
+
* copies drift, which is how a write path ends up enforcing a weaker rule
|
|
150
|
+
* than its neighbours. */
|
|
151
|
+
inline bool MCPIsProtectedAssetPath(const FString& Path)
|
|
152
|
+
{
|
|
153
|
+
FString Normalized = Path;
|
|
154
|
+
Normalized.TrimStartAndEndInline();
|
|
155
|
+
if (Normalized.IsEmpty()) return false;
|
|
156
|
+
// Tolerate the surface form, which may arrive without a leading slash.
|
|
157
|
+
if (!Normalized.StartsWith(TEXT("/"))) Normalized = TEXT("/") + Normalized;
|
|
158
|
+
const FString Lower = Normalized.ToLower();
|
|
159
|
+
if (Lower.StartsWith(TEXT("/engine/"))) return true;
|
|
160
|
+
if (Lower.StartsWith(TEXT("/memory/"))) return true;
|
|
161
|
+
if (Lower.StartsWith(TEXT("/temp/"))) return true;
|
|
162
|
+
// Verse runtime objects surface as /Script/CoreUObject.* etc, so /Script/
|
|
163
|
+
// is rejected wherever it appears, not just as a prefix.
|
|
164
|
+
if (Lower.Contains(TEXT("/script/"))) return true;
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
138
168
|
/** Emit the standard delete_asset rollback record on a create result. */
|
|
139
169
|
inline void MCPSetDeleteAssetRollback(TSharedPtr<FJsonObject> Result, const FString& AssetPath)
|
|
140
170
|
{
|