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.
Files changed (48) hide show
  1. package/README.md +2 -0
  2. package/dist/context-cli.js +1 -1
  3. package/dist/context-cli.js.map +1 -1
  4. package/dist/deployer.d.ts +35 -0
  5. package/dist/deployer.js +88 -13
  6. package/dist/deployer.js.map +1 -1
  7. package/dist/editor-control.d.ts +2 -1
  8. package/dist/editor-control.js +1 -0
  9. package/dist/editor-control.js.map +1 -1
  10. package/dist/flow/guard-schema.d.ts +204 -0
  11. package/dist/flow/guard-schema.js +60 -0
  12. package/dist/flow/guard-schema.js.map +1 -0
  13. package/dist/flow/guard.d.ts +20 -0
  14. package/dist/flow/guard.js +32 -0
  15. package/dist/flow/guard.js.map +1 -1
  16. package/dist/flow/guards.d.ts +56 -0
  17. package/dist/flow/guards.js +175 -0
  18. package/dist/flow/guards.js.map +1 -0
  19. package/dist/flow/loader.js +13 -0
  20. package/dist/flow/loader.js.map +1 -1
  21. package/dist/flow/schema.d.ts +99 -0
  22. package/dist/flow/schema.js +2 -0
  23. package/dist/flow/schema.js.map +1 -1
  24. package/dist/flow/write-methods.d.ts +16 -0
  25. package/dist/flow/write-methods.js +18 -3
  26. package/dist/flow/write-methods.js.map +1 -1
  27. package/dist/index.js +47 -13
  28. package/dist/index.js.map +1 -1
  29. package/dist/plugin/loader.d.ts +17 -0
  30. package/dist/plugin/loader.js +12 -0
  31. package/dist/plugin/loader.js.map +1 -1
  32. package/dist/plugin/manifest.d.ts +99 -0
  33. package/dist/plugin/manifest.js +2 -0
  34. package/dist/plugin/manifest.js.map +1 -1
  35. package/dist/session.js +1 -1
  36. package/dist/session.js.map +1 -1
  37. package/dist/tool-counts.json +2 -2
  38. package/dist/tools/level.js +2 -0
  39. package/dist/tools/level.js.map +1 -1
  40. package/package.json +7 -4
  41. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +4 -2
  42. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +8 -5
  43. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +1 -1
  44. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +6 -5
  45. package/plugin/ue_mcp_bridge/UE_MCP_Bridge.uplugin +1 -1
  46. package/dist/flow/task-guards.d.ts +0 -13
  47. package/dist/flow/task-guards.js +0 -96
  48. 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"}
@@ -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;
@@ -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
@@ -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;AAGhC,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,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"}
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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"}
@@ -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 } },