ue-mcp 1.3.5 → 1.3.6
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/README.md +2 -0
- package/dist/context-cli.js +1 -1
- package/dist/context-cli.js.map +1 -1
- package/dist/deployer.d.ts +35 -0
- package/dist/deployer.js +88 -13
- package/dist/deployer.js.map +1 -1
- package/dist/editor-control.d.ts +2 -1
- package/dist/editor-control.js +1 -0
- package/dist/editor-control.js.map +1 -1
- package/dist/flow/guard-schema.d.ts +204 -0
- package/dist/flow/guard-schema.js +60 -0
- package/dist/flow/guard-schema.js.map +1 -0
- package/dist/flow/guard.d.ts +20 -0
- package/dist/flow/guard.js +32 -0
- package/dist/flow/guard.js.map +1 -1
- package/dist/flow/guards.d.ts +56 -0
- package/dist/flow/guards.js +175 -0
- package/dist/flow/guards.js.map +1 -0
- package/dist/flow/loader.js +13 -0
- package/dist/flow/loader.js.map +1 -1
- package/dist/flow/schema.d.ts +99 -0
- package/dist/flow/schema.js +2 -0
- package/dist/flow/schema.js.map +1 -1
- package/dist/flow/write-methods.d.ts +16 -0
- package/dist/flow/write-methods.js +18 -3
- package/dist/flow/write-methods.js.map +1 -1
- package/dist/index.js +47 -13
- package/dist/index.js.map +1 -1
- package/dist/plugin/loader.d.ts +17 -0
- package/dist/plugin/loader.js +12 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/plugin/manifest.d.ts +99 -0
- package/dist/plugin/manifest.js +2 -0
- package/dist/plugin/manifest.js.map +1 -1
- package/dist/session.js +1 -1
- package/dist/session.js.map +1 -1
- package/dist/tool-counts.json +2 -2
- package/dist/tools/level.js +2 -0
- package/dist/tools/level.js.map +1 -1
- package/package.json +7 -4
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +4 -2
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +8 -5
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +1 -1
- package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +6 -5
- package/plugin/ue_mcp_bridge/UE_MCP_Bridge.uplugin +1 -1
- package/dist/flow/task-guards.d.ts +0 -13
- package/dist/flow/task-guards.js +0 -96
- package/dist/flow/task-guards.js.map +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* What a guard is, declared once and used by both things that can declare one:
|
|
4
|
+
* a plugin manifest and a project's `ue-mcp.yml`.
|
|
5
|
+
*
|
|
6
|
+
* A guard used to be a task whose NAME encoded everything: `guard.sandbox.
|
|
7
|
+
* beforeWrite` meant a guard called sandbox, running before the call, applying
|
|
8
|
+
* only to writes. That encoding could not express anything else, so ordering
|
|
9
|
+
* was unreachable, a guard wanting both hooks had to be two tasks that did not
|
|
10
|
+
* know they were related, and a misspelling produced an ordinary task nobody
|
|
11
|
+
* ever called rather than an error.
|
|
12
|
+
*
|
|
13
|
+
* The four "phases" were also never four things. They are two hooks and two
|
|
14
|
+
* scopes, so they are two fields.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Which calls a guard sees.
|
|
18
|
+
*
|
|
19
|
+
* all every call, including reads. For audit and rate limiting.
|
|
20
|
+
* mutations every call that changes editor state, whether or not it names
|
|
21
|
+
* an asset. This is what "block everything that mutates" means:
|
|
22
|
+
* spawning an actor and starting a play session are in, reads are
|
|
23
|
+
* out.
|
|
24
|
+
* writes calls that modify content already on disk. Narrower than
|
|
25
|
+
* mutations on purpose: it is the source-control question, and a
|
|
26
|
+
* call that creates something new modifies nothing yet.
|
|
27
|
+
*/
|
|
28
|
+
export declare const GuardScopeSchema: z.ZodDefault<z.ZodEnum<["all", "mutations", "writes"]>>;
|
|
29
|
+
/** One hook: the task to run, and the options it is built with. */
|
|
30
|
+
export declare const GuardHookSchema: z.ZodObject<{
|
|
31
|
+
class_path: z.ZodString;
|
|
32
|
+
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
33
|
+
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
options: Record<string, unknown>;
|
|
35
|
+
class_path: string;
|
|
36
|
+
}, {
|
|
37
|
+
class_path: string;
|
|
38
|
+
options?: Record<string, unknown> | undefined;
|
|
39
|
+
}>;
|
|
40
|
+
export declare const GuardDeclarationSchema: z.ZodEffects<z.ZodObject<{
|
|
41
|
+
description: z.ZodOptional<z.ZodString>;
|
|
42
|
+
/**
|
|
43
|
+
* `writes` costs nothing on a read: the pipeline only resolves which files
|
|
44
|
+
* a call touches when a guard asks for it.
|
|
45
|
+
*/
|
|
46
|
+
scope: z.ZodDefault<z.ZodEnum<["all", "mutations", "writes"]>>;
|
|
47
|
+
/** Lower runs first before the call, and last after it. */
|
|
48
|
+
order: z.ZodDefault<z.ZodNumber>;
|
|
49
|
+
/** Runs before the call. Denying here means the call never happens. */
|
|
50
|
+
before: z.ZodOptional<z.ZodObject<{
|
|
51
|
+
class_path: z.ZodString;
|
|
52
|
+
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
53
|
+
}, "strip", z.ZodTypeAny, {
|
|
54
|
+
options: Record<string, unknown>;
|
|
55
|
+
class_path: string;
|
|
56
|
+
}, {
|
|
57
|
+
class_path: string;
|
|
58
|
+
options?: Record<string, unknown> | undefined;
|
|
59
|
+
}>>;
|
|
60
|
+
/** Runs after a successful call, and may replace its result. */
|
|
61
|
+
after: z.ZodOptional<z.ZodObject<{
|
|
62
|
+
class_path: z.ZodString;
|
|
63
|
+
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
64
|
+
}, "strip", z.ZodTypeAny, {
|
|
65
|
+
options: Record<string, unknown>;
|
|
66
|
+
class_path: string;
|
|
67
|
+
}, {
|
|
68
|
+
class_path: string;
|
|
69
|
+
options?: Record<string, unknown> | undefined;
|
|
70
|
+
}>>;
|
|
71
|
+
}, "strip", z.ZodTypeAny, {
|
|
72
|
+
order: number;
|
|
73
|
+
scope: "all" | "mutations" | "writes";
|
|
74
|
+
description?: string | undefined;
|
|
75
|
+
before?: {
|
|
76
|
+
options: Record<string, unknown>;
|
|
77
|
+
class_path: string;
|
|
78
|
+
} | undefined;
|
|
79
|
+
after?: {
|
|
80
|
+
options: Record<string, unknown>;
|
|
81
|
+
class_path: string;
|
|
82
|
+
} | undefined;
|
|
83
|
+
}, {
|
|
84
|
+
description?: string | undefined;
|
|
85
|
+
order?: number | undefined;
|
|
86
|
+
scope?: "all" | "mutations" | "writes" | undefined;
|
|
87
|
+
before?: {
|
|
88
|
+
class_path: string;
|
|
89
|
+
options?: Record<string, unknown> | undefined;
|
|
90
|
+
} | undefined;
|
|
91
|
+
after?: {
|
|
92
|
+
class_path: string;
|
|
93
|
+
options?: Record<string, unknown> | undefined;
|
|
94
|
+
} | undefined;
|
|
95
|
+
}>, {
|
|
96
|
+
order: number;
|
|
97
|
+
scope: "all" | "mutations" | "writes";
|
|
98
|
+
description?: string | undefined;
|
|
99
|
+
before?: {
|
|
100
|
+
options: Record<string, unknown>;
|
|
101
|
+
class_path: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
after?: {
|
|
104
|
+
options: Record<string, unknown>;
|
|
105
|
+
class_path: string;
|
|
106
|
+
} | undefined;
|
|
107
|
+
}, {
|
|
108
|
+
description?: string | undefined;
|
|
109
|
+
order?: number | undefined;
|
|
110
|
+
scope?: "all" | "mutations" | "writes" | undefined;
|
|
111
|
+
before?: {
|
|
112
|
+
class_path: string;
|
|
113
|
+
options?: Record<string, unknown> | undefined;
|
|
114
|
+
} | undefined;
|
|
115
|
+
after?: {
|
|
116
|
+
class_path: string;
|
|
117
|
+
options?: Record<string, unknown> | undefined;
|
|
118
|
+
} | undefined;
|
|
119
|
+
}>;
|
|
120
|
+
/** Guards keyed by name. The name is an identifier, not a phase encoding. */
|
|
121
|
+
export declare const GuardsSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
|
|
122
|
+
description: z.ZodOptional<z.ZodString>;
|
|
123
|
+
/**
|
|
124
|
+
* `writes` costs nothing on a read: the pipeline only resolves which files
|
|
125
|
+
* a call touches when a guard asks for it.
|
|
126
|
+
*/
|
|
127
|
+
scope: z.ZodDefault<z.ZodEnum<["all", "mutations", "writes"]>>;
|
|
128
|
+
/** Lower runs first before the call, and last after it. */
|
|
129
|
+
order: z.ZodDefault<z.ZodNumber>;
|
|
130
|
+
/** Runs before the call. Denying here means the call never happens. */
|
|
131
|
+
before: z.ZodOptional<z.ZodObject<{
|
|
132
|
+
class_path: z.ZodString;
|
|
133
|
+
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
134
|
+
}, "strip", z.ZodTypeAny, {
|
|
135
|
+
options: Record<string, unknown>;
|
|
136
|
+
class_path: string;
|
|
137
|
+
}, {
|
|
138
|
+
class_path: string;
|
|
139
|
+
options?: Record<string, unknown> | undefined;
|
|
140
|
+
}>>;
|
|
141
|
+
/** Runs after a successful call, and may replace its result. */
|
|
142
|
+
after: z.ZodOptional<z.ZodObject<{
|
|
143
|
+
class_path: z.ZodString;
|
|
144
|
+
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
145
|
+
}, "strip", z.ZodTypeAny, {
|
|
146
|
+
options: Record<string, unknown>;
|
|
147
|
+
class_path: string;
|
|
148
|
+
}, {
|
|
149
|
+
class_path: string;
|
|
150
|
+
options?: Record<string, unknown> | undefined;
|
|
151
|
+
}>>;
|
|
152
|
+
}, "strip", z.ZodTypeAny, {
|
|
153
|
+
order: number;
|
|
154
|
+
scope: "all" | "mutations" | "writes";
|
|
155
|
+
description?: string | undefined;
|
|
156
|
+
before?: {
|
|
157
|
+
options: Record<string, unknown>;
|
|
158
|
+
class_path: string;
|
|
159
|
+
} | undefined;
|
|
160
|
+
after?: {
|
|
161
|
+
options: Record<string, unknown>;
|
|
162
|
+
class_path: string;
|
|
163
|
+
} | undefined;
|
|
164
|
+
}, {
|
|
165
|
+
description?: string | undefined;
|
|
166
|
+
order?: number | undefined;
|
|
167
|
+
scope?: "all" | "mutations" | "writes" | undefined;
|
|
168
|
+
before?: {
|
|
169
|
+
class_path: string;
|
|
170
|
+
options?: Record<string, unknown> | undefined;
|
|
171
|
+
} | undefined;
|
|
172
|
+
after?: {
|
|
173
|
+
class_path: string;
|
|
174
|
+
options?: Record<string, unknown> | undefined;
|
|
175
|
+
} | undefined;
|
|
176
|
+
}>, {
|
|
177
|
+
order: number;
|
|
178
|
+
scope: "all" | "mutations" | "writes";
|
|
179
|
+
description?: string | undefined;
|
|
180
|
+
before?: {
|
|
181
|
+
options: Record<string, unknown>;
|
|
182
|
+
class_path: string;
|
|
183
|
+
} | undefined;
|
|
184
|
+
after?: {
|
|
185
|
+
options: Record<string, unknown>;
|
|
186
|
+
class_path: string;
|
|
187
|
+
} | undefined;
|
|
188
|
+
}, {
|
|
189
|
+
description?: string | undefined;
|
|
190
|
+
order?: number | undefined;
|
|
191
|
+
scope?: "all" | "mutations" | "writes" | undefined;
|
|
192
|
+
before?: {
|
|
193
|
+
class_path: string;
|
|
194
|
+
options?: Record<string, unknown> | undefined;
|
|
195
|
+
} | undefined;
|
|
196
|
+
after?: {
|
|
197
|
+
class_path: string;
|
|
198
|
+
options?: Record<string, unknown> | undefined;
|
|
199
|
+
} | undefined;
|
|
200
|
+
}>>>;
|
|
201
|
+
export type GuardScope = z.infer<typeof GuardScopeSchema>;
|
|
202
|
+
export type GuardHook = z.infer<typeof GuardHookSchema>;
|
|
203
|
+
export type GuardDeclaration = z.infer<typeof GuardDeclarationSchema>;
|
|
204
|
+
export type GuardDeclarations = z.infer<typeof GuardsSchema>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* What a guard is, declared once and used by both things that can declare one:
|
|
4
|
+
* a plugin manifest and a project's `ue-mcp.yml`.
|
|
5
|
+
*
|
|
6
|
+
* A guard used to be a task whose NAME encoded everything: `guard.sandbox.
|
|
7
|
+
* beforeWrite` meant a guard called sandbox, running before the call, applying
|
|
8
|
+
* only to writes. That encoding could not express anything else, so ordering
|
|
9
|
+
* was unreachable, a guard wanting both hooks had to be two tasks that did not
|
|
10
|
+
* know they were related, and a misspelling produced an ordinary task nobody
|
|
11
|
+
* ever called rather than an error.
|
|
12
|
+
*
|
|
13
|
+
* The four "phases" were also never four things. They are two hooks and two
|
|
14
|
+
* scopes, so they are two fields.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Which calls a guard sees.
|
|
18
|
+
*
|
|
19
|
+
* all every call, including reads. For audit and rate limiting.
|
|
20
|
+
* mutations every call that changes editor state, whether or not it names
|
|
21
|
+
* an asset. This is what "block everything that mutates" means:
|
|
22
|
+
* spawning an actor and starting a play session are in, reads are
|
|
23
|
+
* out.
|
|
24
|
+
* writes calls that modify content already on disk. Narrower than
|
|
25
|
+
* mutations on purpose: it is the source-control question, and a
|
|
26
|
+
* call that creates something new modifies nothing yet.
|
|
27
|
+
*/
|
|
28
|
+
export const GuardScopeSchema = z.enum(["all", "mutations", "writes"]).default("all");
|
|
29
|
+
/** One hook: the task to run, and the options it is built with. */
|
|
30
|
+
export const GuardHookSchema = z.object({
|
|
31
|
+
class_path: z.string().min(1, "a guard hook needs the class_path of the task that implements it"),
|
|
32
|
+
options: z.record(z.unknown()).default({}),
|
|
33
|
+
});
|
|
34
|
+
export const GuardDeclarationSchema = z
|
|
35
|
+
.object({
|
|
36
|
+
description: z.string().optional(),
|
|
37
|
+
/**
|
|
38
|
+
* `writes` costs nothing on a read: the pipeline only resolves which files
|
|
39
|
+
* a call touches when a guard asks for it.
|
|
40
|
+
*/
|
|
41
|
+
scope: GuardScopeSchema,
|
|
42
|
+
/** Lower runs first before the call, and last after it. */
|
|
43
|
+
order: z.number().int().default(0),
|
|
44
|
+
/** Runs before the call. Denying here means the call never happens. */
|
|
45
|
+
before: GuardHookSchema.optional(),
|
|
46
|
+
/** Runs after a successful call, and may replace its result. */
|
|
47
|
+
after: GuardHookSchema.optional(),
|
|
48
|
+
})
|
|
49
|
+
.refine((g) => g.before !== undefined || g.after !== undefined, {
|
|
50
|
+
message: "a guard needs a before hook, an after hook, or both. One with neither would be "
|
|
51
|
+
+ "registered, reported, and never run, which is indistinguishable from having no guard.",
|
|
52
|
+
});
|
|
53
|
+
/** Guards keyed by name. The name is an identifier, not a phase encoding. */
|
|
54
|
+
export const GuardsSchema = z
|
|
55
|
+
.record(z.string().regex(/^[a-z][a-z0-9_-]*$/i, {
|
|
56
|
+
message: "a guard name must start with a letter and hold only letters, digits, underscores "
|
|
57
|
+
+ "and hyphens",
|
|
58
|
+
}), GuardDeclarationSchema)
|
|
59
|
+
.default({});
|
|
60
|
+
//# sourceMappingURL=guard-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-schema.js","sourceRoot":"","sources":["../../src/flow/guard-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEtF,mEAAmE;AACnE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kEAAkE,CAAC;IACjG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAC3C,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC;KACpC,MAAM,CAAC;IACN,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC;;;OAGG;IACH,KAAK,EAAE,gBAAgB;IACvB,2DAA2D;IAC3D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAClC,uEAAuE;IACvE,MAAM,EAAE,eAAe,CAAC,QAAQ,EAAE;IAClC,gEAAgE;IAChE,KAAK,EAAE,eAAe,CAAC,QAAQ,EAAE;CAClC,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,EAAE;IAC9D,OAAO,EACL,iFAAiF;UAC/E,uFAAuF;CAC5F,CAAC,CAAC;AAEL,6EAA6E;AAC7E,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC;KAC1B,MAAM,CACL,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,qBAAqB,EAAE;IACtC,OAAO,EACL,mFAAmF;UACjF,aAAa;CAClB,CAAC,EACF,sBAAsB,CACvB;KACA,OAAO,CAAC,EAAE,CAAC,CAAC"}
|
package/dist/flow/guard.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { GuardRegistry as FlowkitGuardRegistry, type Guard, type GuardContext } from "@db-lyon/flowkit/guard";
|
|
17
17
|
import type { IBridge } from "../bridge.js";
|
|
18
|
+
import type { ProjectContext } from "../project.js";
|
|
18
19
|
import type { EditorSession } from "../session.js";
|
|
19
20
|
import { type WriteClassification } from "./write-methods.js";
|
|
20
21
|
/** Resolve a UE content path to an absolute on-disk file, or null if it does not exist. */
|
|
@@ -48,5 +49,24 @@ export declare class GuardRegistry extends FlowkitGuardRegistry<CallContext, unk
|
|
|
48
49
|
* "write" means.
|
|
49
50
|
*/
|
|
50
51
|
export declare function writeScope(ctx: CallContext): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Every call that changes editor state.
|
|
54
|
+
*
|
|
55
|
+
* Wider than `writeScope`, and deliberately so. That one asks which existing
|
|
56
|
+
* files a call modifies, which is the question a source-control guard has. A
|
|
57
|
+
* guard told to stand in front of everything that mutates has a different
|
|
58
|
+
* question: spawning an actor, starting a play session or pressing a dialog
|
|
59
|
+
* button all change something and name no asset, so they are invisible to the
|
|
60
|
+
* narrower scope while being exactly what "block every mutation" means.
|
|
61
|
+
*/
|
|
62
|
+
export declare function mutationScope(ctx: CallContext): boolean;
|
|
51
63
|
/** Build the per-call context, wiring the lazy write-enrichment helpers. */
|
|
52
64
|
export declare function makeCallContext(method: string, params: Record<string, unknown>, timeoutMs: number | undefined, rawBridge: IBridge, resolveExistingFile: ResolveExistingFile, session?: EditorSession): CallContext;
|
|
65
|
+
/**
|
|
66
|
+
* Turn a content path into the file on disk it names, or null.
|
|
67
|
+
*
|
|
68
|
+
* This is what lets a guard scoped to writes see which existing files a call
|
|
69
|
+
* would modify. It is deliberately null-returning rather than throwing: a path
|
|
70
|
+
* that resolves to nothing is a call that creates something, not an error.
|
|
71
|
+
*/
|
|
72
|
+
export declare function makeResolveExistingFile(project: ProjectContext): ResolveExistingFile;
|
package/dist/flow/guard.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* is about to modify.
|
|
15
15
|
*/
|
|
16
16
|
import { GuardRegistry as FlowkitGuardRegistry, guardContextBase, lazy, } from "@db-lyon/flowkit/guard";
|
|
17
|
+
import * as fs from "node:fs";
|
|
17
18
|
import { classifyWrite } from "./write-methods.js";
|
|
18
19
|
/** The bridge's guard set. Built-in guards register directly; plugin guards are discovered. */
|
|
19
20
|
export class GuardRegistry extends FlowkitGuardRegistry {
|
|
@@ -27,6 +28,19 @@ export class GuardRegistry extends FlowkitGuardRegistry {
|
|
|
27
28
|
export function writeScope(ctx) {
|
|
28
29
|
return ctx.writeFiles().length > 0;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Every call that changes editor state.
|
|
33
|
+
*
|
|
34
|
+
* Wider than `writeScope`, and deliberately so. That one asks which existing
|
|
35
|
+
* files a call modifies, which is the question a source-control guard has. A
|
|
36
|
+
* guard told to stand in front of everything that mutates has a different
|
|
37
|
+
* question: spawning an actor, starting a play session or pressing a dialog
|
|
38
|
+
* button all change something and name no asset, so they are invisible to the
|
|
39
|
+
* narrower scope while being exactly what "block every mutation" means.
|
|
40
|
+
*/
|
|
41
|
+
export function mutationScope(ctx) {
|
|
42
|
+
return ctx.write().mutates;
|
|
43
|
+
}
|
|
30
44
|
/** Build the per-call context, wiring the lazy write-enrichment helpers. */
|
|
31
45
|
export function makeCallContext(method, params, timeoutMs, rawBridge, resolveExistingFile, session) {
|
|
32
46
|
const ctx = {
|
|
@@ -46,4 +60,22 @@ export function makeCallContext(method, params, timeoutMs, rawBridge, resolveExi
|
|
|
46
60
|
});
|
|
47
61
|
return Object.assign(ctx, { write, writeFiles });
|
|
48
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Turn a content path into the file on disk it names, or null.
|
|
65
|
+
*
|
|
66
|
+
* This is what lets a guard scoped to writes see which existing files a call
|
|
67
|
+
* would modify. It is deliberately null-returning rather than throwing: a path
|
|
68
|
+
* that resolves to nothing is a call that creates something, not an error.
|
|
69
|
+
*/
|
|
70
|
+
export function makeResolveExistingFile(project) {
|
|
71
|
+
return (contentPath) => {
|
|
72
|
+
try {
|
|
73
|
+
const abs = project.resolveContentPath(contentPath);
|
|
74
|
+
return fs.existsSync(abs) ? abs : null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
49
81
|
//# sourceMappingURL=guard.js.map
|
package/dist/flow/guard.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/flow/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,aAAa,IAAI,oBAAoB,EACrC,gBAAgB,EAChB,IAAI,GAGL,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/flow/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,aAAa,IAAI,oBAAoB,EACrC,gBAAgB,EAChB,IAAI,GAGL,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAI9B,OAAO,EAAE,aAAa,EAA4B,MAAM,oBAAoB,CAAC;AA0B7E,+FAA+F;AAC/F,MAAM,OAAO,aAAc,SAAQ,oBAA0C;CAAG;AAEhF;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,GAAgB;IACzC,OAAO,GAAG,CAAC,UAAU,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,GAAgB;IAC5C,OAAO,GAAG,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC;AAC7B,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAC7B,MAAc,EACd,MAA+B,EAC/B,SAA6B,EAC7B,SAAkB,EAClB,mBAAwC,EACxC,OAAuB;IAEvB,MAAM,GAAG,GAAG;QACV,GAAG,gBAAgB,EAAE;QACrB,MAAM;QACN,MAAM;QACN,SAAS;QACT,MAAM,EAAE,SAAS;QACjB,OAAO;KACO,CAAC;IAEjB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,CAAC,MAAM;YACb,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC;YAChF,CAAC,CAAC,EAAE,CAAC;IACT,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAuB;IAC7D,OAAO,CAAC,WAAmB,EAAiB,EAAE;QAC5C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,OAAO,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;YACpD,OAAO,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building guards from what somebody declared.
|
|
3
|
+
*
|
|
4
|
+
* A guard is a first-class thing here, not a task that happens to be named a
|
|
5
|
+
* certain way. Two sources declare them in the same shape, a plugin manifest
|
|
6
|
+
* and a project's `ue-mcp.yml`, and both arrive here as `GuardDeclaration`
|
|
7
|
+
* records that are turned into pipeline guards directly.
|
|
8
|
+
*
|
|
9
|
+
* A guard is IMPLEMENTED BY a task, named by `class_path`, but it is not one.
|
|
10
|
+
* The task registry holds tasks; this holds guards; a name collision between
|
|
11
|
+
* the two is now impossible rather than load-bearing.
|
|
12
|
+
*/
|
|
13
|
+
import type { TaskRegistry } from "@db-lyon/flowkit";
|
|
14
|
+
import type { IBridge } from "../bridge.js";
|
|
15
|
+
import type { ToolContext } from "../types.js";
|
|
16
|
+
import { type BridgeGuard } from "./guard.js";
|
|
17
|
+
import type { GuardDeclarations } from "./guard-schema.js";
|
|
18
|
+
/** Where a declaration came from, so an error can say which file to open. */
|
|
19
|
+
export interface GuardSource {
|
|
20
|
+
/** `ue-mcp.yml`, or a plugin's name. */
|
|
21
|
+
readonly label: string;
|
|
22
|
+
}
|
|
23
|
+
export interface BuildGuardsDeps {
|
|
24
|
+
registry: TaskRegistry;
|
|
25
|
+
ctx: ToolContext;
|
|
26
|
+
/**
|
|
27
|
+
* The bridge a guard's own calls go out on when no session is attached.
|
|
28
|
+
*
|
|
29
|
+
* Raw on purpose: a guard that calls the editor must not re-enter the flow
|
|
30
|
+
* pipeline that is running it. That is a statement about the registry and
|
|
31
|
+
* not about dialogs, so the per-session path below still puts the dialog
|
|
32
|
+
* gate back, with no pipeline attached.
|
|
33
|
+
*/
|
|
34
|
+
rawBridge: IBridge;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Turn declarations into pipeline guards.
|
|
38
|
+
*
|
|
39
|
+
* Every guard is one object with the hooks it declared, so a guard wanting
|
|
40
|
+
* both sides is one guard that knows it is one, and ordering is a number
|
|
41
|
+
* rather than something the name could not say.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildGuards(declarations: GuardDeclarations, deps: BuildGuardsDeps, source: GuardSource): Promise<BridgeGuard[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Refuse to start when a task is still named like a guard.
|
|
46
|
+
*
|
|
47
|
+
* Before guards were declared, a task with this name WAS a guard. Under the
|
|
48
|
+
* declaration model nothing discovers it, so it becomes an ordinary task
|
|
49
|
+
* nobody calls: a source-control write guard would keep appearing in the
|
|
50
|
+
* config and stop gating anything, silently.
|
|
51
|
+
*
|
|
52
|
+
* That is the exact failure the declaration model exists to prevent, so the
|
|
53
|
+
* migration must not reintroduce it. A leftover name is fatal and says what to
|
|
54
|
+
* write instead.
|
|
55
|
+
*/
|
|
56
|
+
export declare function assertNoLegacyGuardTasks(taskNames: string[], source: GuardSource): void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { mutationScope, writeScope } from "./guard.js";
|
|
2
|
+
import { withoutDialogActuation } from "../dialog-guard.js";
|
|
3
|
+
import { DialogGatedBridge } from "./guarded-bridge.js";
|
|
4
|
+
import { McpError, ErrorCode } from "../errors.js";
|
|
5
|
+
import { debug } from "../log.js";
|
|
6
|
+
/**
|
|
7
|
+
* The context a hook task runs in.
|
|
8
|
+
*
|
|
9
|
+
* Bound to the editor whose call is being guarded rather than to whichever
|
|
10
|
+
* bridge was built first, so a guard can neither recurse through the pipeline
|
|
11
|
+
* nor act on another project's editor. `withoutDialogActuation` sits on top
|
|
12
|
+
* because arming a dialog policy is served during a modal, so a guard could
|
|
13
|
+
* otherwise answer the dialog already on screen.
|
|
14
|
+
*/
|
|
15
|
+
function hookContext(cc, deps) {
|
|
16
|
+
if (!cc.session)
|
|
17
|
+
return { ...deps.ctx, bridge: deps.rawBridge };
|
|
18
|
+
return {
|
|
19
|
+
...deps.ctx,
|
|
20
|
+
bridge: withoutDialogActuation(cc.session, new DialogGatedBridge(cc.bridge, cc.session)),
|
|
21
|
+
project: cc.session.project,
|
|
22
|
+
session: cc.session,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** What a hook is told about the call it is guarding. */
|
|
26
|
+
function hookOptions(cc, declared, result) {
|
|
27
|
+
return {
|
|
28
|
+
// The declaration first, so the call being guarded always wins a
|
|
29
|
+
// collision: a guard reads its configuration and its subject from one
|
|
30
|
+
// object, and the subject is never masked by a stale default.
|
|
31
|
+
...declared,
|
|
32
|
+
method: cc.method,
|
|
33
|
+
params: cc.params,
|
|
34
|
+
paths: cc.writeFiles(),
|
|
35
|
+
...(result !== undefined ? { result } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Run one hook and give back what it answered. */
|
|
39
|
+
async function runHook(hook, cc, deps, result) {
|
|
40
|
+
const task = await deps.registry.create(hook.class_path, hookContext(cc, deps), hookOptions(cc, hook.options, result));
|
|
41
|
+
return (await task.execute());
|
|
42
|
+
}
|
|
43
|
+
/** The message a denial carries, from wherever the hook put it. */
|
|
44
|
+
function refusalReason(r) {
|
|
45
|
+
return r.error?.message ?? r.message ?? "no reason given";
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve every declared hook up front.
|
|
49
|
+
*
|
|
50
|
+
* A `class_path` that cannot be resolved is fatal at build time, not at the
|
|
51
|
+
* moment a guarded call arrives. A guard that cannot be constructed would
|
|
52
|
+
* leave the calls it names ungated while the configuration says otherwise,
|
|
53
|
+
* which is the one failure this whole mechanism exists to prevent.
|
|
54
|
+
*/
|
|
55
|
+
async function assertResolvable(name, which, hook, deps, source) {
|
|
56
|
+
try {
|
|
57
|
+
await deps.registry.resolve(hook.class_path);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
61
|
+
throw new Error(`${source.label} declares guard '${name}' with a ${which} hook whose class_path `
|
|
62
|
+
+ `'${hook.class_path}' could not be resolved: ${why}. A guard that cannot be built would `
|
|
63
|
+
+ "leave the calls it covers ungated while the configuration says they are guarded, so the "
|
|
64
|
+
+ "server stops here rather than starting without it.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Turn declarations into pipeline guards.
|
|
69
|
+
*
|
|
70
|
+
* Every guard is one object with the hooks it declared, so a guard wanting
|
|
71
|
+
* both sides is one guard that knows it is one, and ordering is a number
|
|
72
|
+
* rather than something the name could not say.
|
|
73
|
+
*/
|
|
74
|
+
export async function buildGuards(declarations, deps, source) {
|
|
75
|
+
const guards = [];
|
|
76
|
+
for (const [name, decl] of Object.entries(declarations)) {
|
|
77
|
+
if (decl.before)
|
|
78
|
+
await assertResolvable(name, "before", decl.before, deps, source);
|
|
79
|
+
if (decl.after)
|
|
80
|
+
await assertResolvable(name, "after", decl.after, deps, source);
|
|
81
|
+
const guard = {
|
|
82
|
+
name,
|
|
83
|
+
order: decl.order,
|
|
84
|
+
// Both narrower scopes are lazy: a guard that does not ask pays nothing,
|
|
85
|
+
// and a pipeline of guards that all ask pays once.
|
|
86
|
+
...(decl.scope === "writes" ? { appliesTo: (cc) => writeScope(cc) } : {}),
|
|
87
|
+
...(decl.scope === "mutations" ? { appliesTo: (cc) => mutationScope(cc) } : {}),
|
|
88
|
+
...(decl.before
|
|
89
|
+
? {
|
|
90
|
+
before: async (cc) => {
|
|
91
|
+
let answered;
|
|
92
|
+
try {
|
|
93
|
+
answered = await runHook(decl.before, cc, deps);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
// A hook that threw denies the call. It is the same outcome as
|
|
97
|
+
// returning failure, and a guard that errors is not a guard
|
|
98
|
+
// that approved.
|
|
99
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
100
|
+
throw new McpError(ErrorCode.WRITE_BLOCKED, `guard '${name}' errored on ${cc.method}: ${why}`);
|
|
101
|
+
}
|
|
102
|
+
if (answered?.success === false) {
|
|
103
|
+
const files = cc.writeFiles();
|
|
104
|
+
const on = files.length > 0 ? ` on ${files.join(", ")}` : "";
|
|
105
|
+
throw new McpError(ErrorCode.WRITE_BLOCKED, `blocked (${cc.method})${on}: ${refusalReason(answered)}`);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
: {}),
|
|
110
|
+
...(decl.after
|
|
111
|
+
? {
|
|
112
|
+
after: async (cc, result) => {
|
|
113
|
+
let answered;
|
|
114
|
+
try {
|
|
115
|
+
answered = await runHook(decl.after, cc, deps, result);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// The call already happened. Failing it now would report a
|
|
119
|
+
// mutation as not having occurred, which is worse than an
|
|
120
|
+
// audit hook that did not run.
|
|
121
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
122
|
+
debug("guard", `after hook of '${name}' errored on ${cc.method}: ${why}`);
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (answered?.success === false) {
|
|
126
|
+
debug("guard", `after hook of '${name}' reported failure on ${cc.method}: ${refusalReason(answered)}`);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
// Returning data replaces the result; returning nothing leaves it.
|
|
130
|
+
return answered?.data;
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
guards.push(guard);
|
|
136
|
+
}
|
|
137
|
+
return guards;
|
|
138
|
+
}
|
|
139
|
+
/** The dead naming convention: a task called `guard.<name>.<phase>`. */
|
|
140
|
+
const LEGACY_GUARD_TASK = /^guard\.[^.]+\.(before|after)[A-Za-z0-9]*$/;
|
|
141
|
+
/**
|
|
142
|
+
* Refuse to start when a task is still named like a guard.
|
|
143
|
+
*
|
|
144
|
+
* Before guards were declared, a task with this name WAS a guard. Under the
|
|
145
|
+
* declaration model nothing discovers it, so it becomes an ordinary task
|
|
146
|
+
* nobody calls: a source-control write guard would keep appearing in the
|
|
147
|
+
* config and stop gating anything, silently.
|
|
148
|
+
*
|
|
149
|
+
* That is the exact failure the declaration model exists to prevent, so the
|
|
150
|
+
* migration must not reintroduce it. A leftover name is fatal and says what to
|
|
151
|
+
* write instead.
|
|
152
|
+
*/
|
|
153
|
+
export function assertNoLegacyGuardTasks(taskNames, source) {
|
|
154
|
+
const legacy = taskNames.filter((n) => LEGACY_GUARD_TASK.test(n));
|
|
155
|
+
if (legacy.length === 0)
|
|
156
|
+
return;
|
|
157
|
+
const examples = legacy.slice(0, 3).map((name) => {
|
|
158
|
+
const [, guardName, phase] = /^guard\.([^.]+)\.(.+)$/.exec(name);
|
|
159
|
+
const scope = /Write$/.test(phase) ? "writes" : "all";
|
|
160
|
+
const hook = phase.startsWith("after") ? "after" : "before";
|
|
161
|
+
return (` ${name}\n`
|
|
162
|
+
+ " becomes:\n"
|
|
163
|
+
+ " guards:\n"
|
|
164
|
+
+ ` ${guardName}:\n`
|
|
165
|
+
+ ` scope: ${scope}\n`
|
|
166
|
+
+ ` ${hook}:\n`
|
|
167
|
+
+ " class_path: <the class_path that task had>");
|
|
168
|
+
});
|
|
169
|
+
throw new Error(`${source.label} declares ${legacy.length} task(s) named like a guard, which no longer makes `
|
|
170
|
+
+ "them one. A guard is declared in a `guards:` section now, so these are ordinary tasks that "
|
|
171
|
+
+ "nothing calls: whatever they were gating is ungated while the config still says it is "
|
|
172
|
+
+ `guarded.\n\n${examples.join("\n\n")}\n\n`
|
|
173
|
+
+ "See the guards documentation for the full shape, including order and the after hook.");
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/flow/guards.ts"],"names":[],"mappings":"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"}
|
package/dist/flow/loader.js
CHANGED
|
@@ -319,6 +319,19 @@ function defaultFlows() {
|
|
|
319
319
|
// asset domain is a real one (/Game/VFX/Fire); override name/packagePath via
|
|
320
320
|
// runtime params. The last step is the verify gate - the run surfaces whether
|
|
321
321
|
// the system actually emits, catching the empty-shell failure.
|
|
322
|
+
//
|
|
323
|
+
// WHAT A GOOD FIRE ACTUALLY IS, from reading a production reference system:
|
|
324
|
+
// layered emitters, not one. A convincing fire is roughly four:
|
|
325
|
+
// - two flame emitters on sprite renderers, doubled for depth
|
|
326
|
+
// - a light emitter, so the fire lights its surroundings
|
|
327
|
+
// - a heat-distortion emitter
|
|
328
|
+
// Colour and size are curve-driven over particle age rather than constant,
|
|
329
|
+
// which is what stops it reading as a flat billboard. Every module involved is
|
|
330
|
+
// a stock engine script under /Niagara/Modules/.
|
|
331
|
+
//
|
|
332
|
+
// This flow builds ONE emitter, which is the honest 0-to-1 starting point and
|
|
333
|
+
// not the finished look. Extending it toward the shape above is the obvious
|
|
334
|
+
// next move, and the reason that shape is written down here.
|
|
322
335
|
const FIRE_PKG = "/Game/VFX/Fire";
|
|
323
336
|
const fireSteps = {
|
|
324
337
|
1: { task: "niagara.create_emitter", options: { name: "E_Flame", packagePath: FIRE_PKG } },
|