@@ -535,6 +567,16 @@ export default function PropertyPanel() {
);
}
+function ContractMetadataRow({ label, value }: { label: string; value?: string }) {
+ if (!value) return null;
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
function SensitiveFieldInput({
value,
onChange,
diff --git a/src/generated/load-schemas.test.ts b/src/generated/load-schemas.test.ts
index dbd680d..f5b2803 100644
--- a/src/generated/load-schemas.test.ts
+++ b/src/generated/load-schemas.test.ts
@@ -1,5 +1,10 @@
import { describe, it, expect } from 'vitest';
-import { getEngineModuleTypes, getEngineCoercionRules, getEngineStepTypes } from './load-schemas';
+import {
+ getEngineModuleTypes,
+ getEngineCoercionRules,
+ getEngineStepTypes,
+ normalizeEditorContractBundle,
+} from './load-schemas';
describe('getEngineModuleTypes', () => {
it('loads http.server from engine schemas', () => {
@@ -51,3 +56,78 @@ describe('getEngineStepTypes', () => {
expect(Array.isArray(anyStep.outputs)).toBe(true);
});
});
+
+describe('normalizeEditorContractBundle', () => {
+ it('normalizes all editor bundle schema sections', () => {
+ const bundle = normalizeEditorContractBundle({
+ version: 'editor-bundle/v1',
+ workflowVersion: '0.0.0-test',
+ moduleSchemas: {
+ 'plugin.greeter': {
+ type: 'plugin.greeter',
+ label: 'Greeter',
+ category: 'integration',
+ configFields: [],
+ defaultConfig: {},
+ },
+ },
+ stepSchemas: {
+ 'step.sayHello': {
+ type: 'step.sayHello',
+ plugin: 'greeter',
+ description: 'Say hello',
+ configFields: [],
+ outputs: [{ key: 'reply', type: 'demo.GreetResponse' }],
+ },
+ },
+ coercionRules: {
+ 'demo.GreetResponse': ['any'],
+ },
+ contracts: {
+ 'greeter:module:plugin.greeter': {
+ id: 'greeter:module:plugin.greeter',
+ ownerType: 'module',
+ ownerKey: 'plugin.greeter',
+ mode: 'strict',
+ requestMessage: 'demo.GreetRequest',
+ responseMessage: 'demo.GreetResponse',
+ configMessage: 'demo.GreeterConfig',
+ source: 'plugin-contracts-json',
+ },
+ },
+ messages: {
+ 'demo.GreetRequest': { id: 'demo.GreetRequest', name: 'GreetRequest', fullName: 'demo.GreetRequest', fields: [] },
+ },
+ schemas: {
+ app: { type: 'object' },
+ infra: { type: 'object' },
+ wfctl: { type: 'object' },
+ },
+ });
+
+ expect(bundle.moduleSchemas['plugin.greeter'].label).toBe('Greeter');
+ expect(bundle.stepSchemas['step.sayHello'].outputs?.[0]?.type).toBe('demo.GreetResponse');
+ expect(bundle.coercionRules['demo.GreetResponse']).toEqual(['any']);
+ expect(bundle.contracts['greeter:module:plugin.greeter'].mode).toBe('strict');
+ expect(bundle.messages['demo.GreetRequest'].fullName).toBe('demo.GreetRequest');
+ expect(bundle.schemas.wfctl).toEqual({ type: 'object' });
+ });
+
+ it('does not throw when optional bundle sections are missing', () => {
+ const bundle = normalizeEditorContractBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {},
+ coercionRules: {},
+ schemas: {
+ app: { type: 'object' },
+ },
+ });
+
+ expect(bundle.stepSchemas).toEqual({});
+ expect(bundle.contracts).toEqual({});
+ expect(bundle.messages).toEqual({});
+ expect(bundle.schemas.app).toEqual({ type: 'object' });
+ expect(bundle.schemas.infra).toBeUndefined();
+ expect(bundle.schemas.wfctl).toBeUndefined();
+ });
+});
diff --git a/src/generated/load-schemas.ts b/src/generated/load-schemas.ts
index f24eaf2..3adc07d 100644
--- a/src/generated/load-schemas.ts
+++ b/src/generated/load-schemas.ts
@@ -1,5 +1,13 @@
import engineData from './engine-schemas.json';
import type { ModuleTypeInfo, IOPort, ConfigFieldDef } from '../types/workflow';
+import type {
+ EditorContractBundle,
+ EditorContractDescriptor,
+ EditorMessageDescriptor,
+ EditorYamlSchemas,
+ EngineBundleModuleSchema,
+ EngineBundleStepSchema,
+} from '../types/editor';
interface EngineModuleSchema {
type: string;
@@ -29,6 +37,17 @@ interface EngineSchemas {
coercionRules: Record
;
}
+export interface NormalizedEditorContractBundle {
+ version: string;
+ workflowVersion?: string;
+ moduleSchemas: Record;
+ stepSchemas: Record;
+ coercionRules: Record;
+ contracts: Record;
+ messages: Record;
+ schemas: EditorYamlSchemas;
+}
+
const data = engineData as EngineSchemas;
function toIOPorts(defs?: { name: string; type: string }[]): IOPort[] {
@@ -81,3 +100,16 @@ export function getEngineStepTypes(): Record {
}
return result;
}
+
+export function normalizeEditorContractBundle(bundle: EditorContractBundle): NormalizedEditorContractBundle {
+ return {
+ version: bundle.version,
+ workflowVersion: bundle.workflowVersion,
+ moduleSchemas: bundle.moduleSchemas ?? {},
+ stepSchemas: bundle.stepSchemas ?? {},
+ coercionRules: bundle.coercionRules ?? {},
+ contracts: bundle.contracts ?? {},
+ messages: bundle.messages ?? {},
+ schemas: bundle.schemas ?? { app: {} },
+ };
+}
diff --git a/src/stores/moduleSchemaStore.contracts.test.ts b/src/stores/moduleSchemaStore.contracts.test.ts
new file mode 100644
index 0000000..244e4e2
--- /dev/null
+++ b/src/stores/moduleSchemaStore.contracts.test.ts
@@ -0,0 +1,237 @@
+import { describe, it, expect, beforeEach } from 'vitest';
+import { useModuleSchemaStore } from './moduleSchemaStore.ts';
+
+function resetModuleSchemaStore() {
+ useModuleSchemaStore.getState().resetSchemaState();
+}
+
+describe('moduleSchemaStore contract bundle loading', () => {
+ beforeEach(() => {
+ resetModuleSchemaStore();
+ });
+
+ it('loads bundle modules, steps, coercion rules, contracts, messages, and YAML schemas', () => {
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {
+ 'plugin.greeter': {
+ type: 'plugin.greeter',
+ label: 'Greeter',
+ category: 'integration',
+ configFields: [],
+ defaultConfig: { greeting: 'hello' },
+ },
+ },
+ stepSchemas: {
+ 'step.sayHello': {
+ type: 'step.sayHello',
+ plugin: 'greeter',
+ description: 'Say hello',
+ configFields: [],
+ outputs: [{ key: 'reply', type: 'demo.GreetResponse' }],
+ },
+ },
+ coercionRules: {
+ 'demo.GreetResponse': ['any'],
+ },
+ contracts: {
+ 'builtin:http.server': {
+ id: 'builtin:http.server',
+ ownerType: 'module',
+ ownerKey: 'http.server',
+ mode: 'strict',
+ requestMessage: 'workflow.http.ServerRequest',
+ responseMessage: 'workflow.http.ServerResponse',
+ source: 'builtin',
+ },
+ 'greeter:module:plugin.greeter': {
+ id: 'greeter:module:plugin.greeter',
+ plugin: 'greeter',
+ ownerType: 'module',
+ ownerKey: 'plugin.greeter',
+ mode: 'proto_with_legacy',
+ requestMessage: 'demo.GreetRequest',
+ responseMessage: 'demo.GreetResponse',
+ configMessage: 'demo.GreeterConfig',
+ source: 'plugin-contracts-json',
+ descriptorSetRef: 'buf.build/demo/greeter',
+ },
+ },
+ messages: {
+ 'demo.GreetResponse': { id: 'demo.GreetResponse', name: 'GreetResponse', fullName: 'demo.GreetResponse', fields: [] },
+ },
+ schemas: {
+ app: { type: 'object' },
+ wfctl: { type: 'object' },
+ },
+ });
+
+ const state = useModuleSchemaStore.getState();
+ expect(state.moduleTypeMap['plugin.greeter']?.defaultConfig).toEqual({ greeting: 'hello' });
+ expect(state.stepTypeMap['step.sayHello']?.outputs[0]?.type).toBe('demo.GreetResponse');
+ expect(state.coercionRules['demo.GreetResponse']).toEqual(['any']);
+ expect(state.getContractByOwner('module', 'plugin.greeter')?.descriptorSetRef).toBe('buf.build/demo/greeter');
+ expect(state.getMessage('demo.GreetResponse')?.name).toBe('GreetResponse');
+ expect(state.getYamlSchema('wfctl')).toEqual({ type: 'object' });
+ });
+
+ it('preserves bundle module ioSignature when no input/output arrays are present', () => {
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {
+ 'plugin.stream': {
+ type: 'plugin.stream',
+ label: 'Stream',
+ category: 'integration',
+ configFields: [],
+ defaultConfig: {},
+ ioSignature: {
+ inputs: [{ name: 'in', type: 'demo.StreamRequest' }],
+ outputs: [{ name: 'out', type: 'demo.StreamResponse' }],
+ },
+ },
+ },
+ coercionRules: {},
+ contracts: {},
+ messages: {},
+ schemas: { app: {} },
+ });
+
+ expect(useModuleSchemaStore.getState().moduleTypeMap['plugin.stream']?.ioSignature).toEqual({
+ inputs: [{ name: 'in', type: 'demo.StreamRequest' }],
+ outputs: [{ name: 'out', type: 'demo.StreamResponse' }],
+ });
+ });
+
+ it('preserves built-in contract overlays when plugin contracts own a different key', () => {
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {},
+ coercionRules: {},
+ contracts: {
+ 'builtin:http.server': {
+ id: 'builtin:http.server',
+ ownerType: 'module',
+ ownerKey: 'http.server',
+ mode: 'strict',
+ responseMessage: 'workflow.http.Response',
+ source: 'builtin',
+ },
+ },
+ messages: {},
+ schemas: { app: {} },
+ });
+
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {},
+ coercionRules: {},
+ contracts: {
+ 'plugin:greeter': {
+ id: 'plugin:greeter',
+ plugin: 'greeter',
+ ownerType: 'module',
+ ownerKey: 'plugin.greeter',
+ mode: 'strict',
+ responseMessage: 'demo.GreetResponse',
+ source: 'plugin-contracts-json',
+ },
+ },
+ messages: {},
+ schemas: { app: {} },
+ });
+
+ expect(useModuleSchemaStore.getState().getContractByOwner('module', 'http.server')?.responseMessage).toBe('workflow.http.Response');
+ expect(useModuleSchemaStore.getState().getContractByOwner('module', 'plugin.greeter')?.responseMessage).toBe('demo.GreetResponse');
+ });
+
+ it('lets a later contract replace an existing contract for the same owner key', () => {
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {},
+ coercionRules: {},
+ contracts: {
+ 'builtin:http.server': {
+ id: 'builtin:http.server',
+ ownerType: 'module',
+ ownerKey: 'http.server',
+ mode: 'legacy',
+ responseMessage: 'workflow.http.LegacyResponse',
+ source: 'builtin',
+ },
+ },
+ messages: {},
+ schemas: { app: {} },
+ });
+
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {},
+ coercionRules: {},
+ contracts: {
+ 'plugin:http.server': {
+ id: 'plugin:http.server',
+ plugin: 'http-plugin',
+ ownerType: 'module',
+ ownerKey: 'http.server',
+ mode: 'strict',
+ responseMessage: 'plugin.http.Response',
+ source: 'plugin-contracts-json',
+ },
+ },
+ messages: {},
+ schemas: { app: {} },
+ });
+
+ const contract = useModuleSchemaStore.getState().getContractByOwner('module', 'http.server');
+ expect(contract?.id).toBe('plugin:http.server');
+ expect(contract?.responseMessage).toBe('plugin.http.Response');
+ });
+
+ it('does not erase bundle contracts or active bundle schemas when legacy schema loaders run later', () => {
+ useModuleSchemaStore.getState().loadEditorBundle({
+ version: 'editor-bundle/v1',
+ moduleSchemas: {
+ 'plugin.greeter': {
+ type: 'plugin.greeter',
+ label: 'Bundle Greeter',
+ category: 'integration',
+ configFields: [],
+ defaultConfig: { source: 'bundle' },
+ },
+ },
+ coercionRules: {},
+ contracts: {
+ 'plugin:greeter': {
+ id: 'plugin:greeter',
+ ownerType: 'module',
+ ownerKey: 'plugin.greeter',
+ mode: 'strict',
+ responseMessage: 'demo.GreetResponse',
+ source: 'plugin-contracts-json',
+ },
+ },
+ messages: {
+ 'demo.GreetResponse': { id: 'demo.GreetResponse', name: 'GreetResponse', fullName: 'demo.GreetResponse', fields: [] },
+ },
+ schemas: { app: {} },
+ });
+
+ useModuleSchemaStore.getState().loadSchemas({
+ 'plugin.greeter': {
+ type: 'plugin.greeter',
+ label: 'Legacy Greeter',
+ category: 'integration',
+ configFields: [],
+ defaultConfig: { source: 'legacy' },
+ },
+ });
+ useModuleSchemaStore.getState().loadPluginSchemas([]);
+
+ const state = useModuleSchemaStore.getState();
+ expect(state.getContractByOwner('module', 'plugin.greeter')?.responseMessage).toBe('demo.GreetResponse');
+ expect(state.getMessage('demo.GreetResponse')?.name).toBe('GreetResponse');
+ expect(state.moduleTypeMap['plugin.greeter']?.label).toBe('Bundle Greeter');
+ expect(state.serverSchemas['plugin.greeter']?.label).toBe('Bundle Greeter');
+ });
+});
diff --git a/src/stores/moduleSchemaStore.ts b/src/stores/moduleSchemaStore.ts
index 057e3d4..f4c77d1 100644
--- a/src/stores/moduleSchemaStore.ts
+++ b/src/stores/moduleSchemaStore.ts
@@ -1,7 +1,21 @@
import { create } from 'zustand';
import type { ModuleTypeInfo, ConfigFieldDef, ModuleCategory, IOSignature } from '../types/workflow.ts';
-import type { PluginSchemaData, ServerModuleSchema as EditorServerModuleSchema } from '../types/editor.ts';
-import { getEngineModuleTypes } from '../generated/load-schemas';
+import type {
+ EditorContractBundle,
+ EditorContractDescriptor,
+ EditorMessageDescriptor,
+ EditorYamlSchemas,
+ EngineBundleModuleSchema,
+ PluginSchemaData,
+ ServerModuleSchema as EditorServerModuleSchema,
+} from '../types/editor.ts';
+import {
+ getEngineCoercionRules,
+ getEngineModuleTypes,
+ getEngineStepTypes,
+ normalizeEditorContractBundle,
+ type StepTypeInfo,
+} from '../generated/load-schemas';
// Shape of a server-side I/O port definition
interface ServerIODef {
@@ -18,6 +32,7 @@ interface ServerModuleSchema {
description?: string;
inputs?: ServerIODef[];
outputs?: ServerIODef[];
+ ioSignature?: IOSignature;
configFields: ServerConfigField[];
defaultConfig?: Record;
maxIncoming?: number | null;
@@ -58,6 +73,20 @@ interface ModuleSchemaState {
moduleTypes: ModuleTypeInfo[];
/** Module type map keyed by type string */
moduleTypeMap: Record;
+ /** Step type map keyed by step type string */
+ stepTypeMap: Record;
+ /** Coercion rules keyed by source type */
+ coercionRules: Record;
+ /** Strict contract descriptors keyed by descriptor id */
+ contracts: Record;
+ /** Contract descriptor id by "ownerType:ownerKey" */
+ contractOwnerIndex: Record;
+ /** Message descriptors keyed by descriptor id/full name */
+ messages: Record;
+ /** YAML schemas from the editor bundle */
+ yamlSchemas: EditorYamlSchemas;
+ /** Whether a host bundle has been loaded */
+ bundleLoaded: boolean;
/** Available services from the engine */
services: ServiceInfo[];
/** Whether services have been loaded */
@@ -70,6 +99,16 @@ interface ModuleSchemaState {
loadSchemas: (schemas: Record) => void;
/** Append plugin schemas to the existing module type map */
loadPluginSchemas: (plugins: PluginSchemaData[]) => void;
+ /** Load the canonical editor contract bundle provided by the host */
+ loadEditorBundle: (bundle: EditorContractBundle) => void;
+ /** Lookup a contract descriptor by owner type and owner key */
+ getContractByOwner: (ownerType: EditorContractDescriptor['ownerType'], ownerKey: string) => EditorContractDescriptor | undefined;
+ /** Lookup a message descriptor by id/full name */
+ getMessage: (messageId: string) => EditorMessageDescriptor | undefined;
+ /** Lookup a YAML schema by bundle schema key */
+ getYamlSchema: (schemaName: keyof EditorYamlSchemas | string) => Record | undefined;
+ /** Reset transient schema state to generated defaults */
+ resetSchemaState: () => void;
}
/** Map server field types to UI field types */
@@ -159,7 +198,7 @@ function mergeSchemas(
seen.add(staticType.type);
const server = serverSchemas[staticType.type];
if (server) {
- const serverIO = convertIOSignature(server.inputs, server.outputs);
+ const serverIO = server.ioSignature ?? convertIOSignature(server.inputs, server.outputs);
merged.push({
...staticType,
label: server.label || staticType.label,
@@ -184,7 +223,7 @@ function mergeSchemas(
category: normalizeCategory(server.category),
configFields: convertFields(server.configFields),
defaultConfig: server.defaultConfig ?? {},
- ioSignature: convertIOSignature(server.inputs, server.outputs),
+ ioSignature: server.ioSignature ?? convertIOSignature(server.inputs, server.outputs),
maxIncoming: server.maxIncoming,
maxOutgoing: server.maxOutgoing,
});
@@ -213,8 +252,63 @@ function editorSchemaToModuleTypeInfo(
};
}
+function bundleSchemaToServerSchema(type: string, schema: EngineBundleModuleSchema): ServerModuleSchema {
+ return {
+ type: schema.type ?? type,
+ label: schema.label ?? type,
+ category: schema.category ?? 'integration',
+ description: schema.description,
+ inputs: schema.inputs,
+ outputs: schema.outputs,
+ ioSignature: schema.ioSignature,
+ configFields: schema.configFields ?? [],
+ defaultConfig: schema.defaultConfig,
+ maxIncoming: schema.maxIncoming,
+ maxOutgoing: schema.maxOutgoing,
+ };
+}
+
+function bundleStepToStepTypeInfo(type: string, schema: NonNullable[string]): StepTypeInfo {
+ return {
+ type: schema.type ?? type,
+ plugin: schema.plugin,
+ description: schema.description ?? '',
+ configFields: schema.configFields ?? [],
+ outputs: schema.outputs ?? [],
+ };
+}
+
+function contractOwnerKey(ownerType: EditorContractDescriptor['ownerType'], ownerKey: string): string {
+ return `${ownerType}:${ownerKey}`;
+}
+
+function mergeContractsByOwner(
+ currentContracts: Record,
+ currentIndex: Record,
+ incoming: Record,
+): { contracts: Record; contractOwnerIndex: Record } {
+ const contracts = { ...currentContracts };
+ const contractOwnerIndex = { ...currentIndex };
+
+ for (const [id, descriptor] of Object.entries(incoming)) {
+ const ownerIndexKey = contractOwnerKey(descriptor.ownerType, descriptor.ownerKey);
+ const previousId = contractOwnerIndex[ownerIndexKey];
+ if (previousId && previousId !== id) {
+ delete contracts[previousId];
+ }
+ contracts[id] = descriptor;
+ contractOwnerIndex[ownerIndexKey] = id;
+ }
+
+ return { contracts, contractOwnerIndex };
+}
+
const initialModuleTypeMap = getEngineModuleTypes();
const initialModuleTypes = Object.values(initialModuleTypeMap);
+const initialStepTypeMap = getEngineStepTypes();
+const initialCoercionRules = getEngineCoercionRules();
+
+const initialYamlSchemas: EditorYamlSchemas = { app: {} };
const useModuleSchemaStore = create((set, get) => ({
loaded: false,
@@ -222,6 +316,13 @@ const useModuleSchemaStore = create((set, get) => ({
serverSchemas: {},
moduleTypes: initialModuleTypes,
moduleTypeMap: initialModuleTypeMap,
+ stepTypeMap: initialStepTypeMap,
+ coercionRules: initialCoercionRules,
+ contracts: {},
+ contractOwnerIndex: {},
+ messages: {},
+ yamlSchemas: initialYamlSchemas,
+ bundleLoaded: false,
services: [],
servicesLoaded: false,
@@ -241,6 +342,10 @@ const useModuleSchemaStore = create((set, get) => ({
return;
}
const schemas: Record = await res.json();
+ if (get().bundleLoaded) {
+ set({ loaded: true, loading: false });
+ return;
+ }
const merged = mergeSchemas(initialModuleTypes, schemas);
const mergedMap = Object.fromEntries(merged.map((t) => [t.type, t]));
set({
@@ -282,6 +387,13 @@ const useModuleSchemaStore = create((set, get) => ({
},
loadSchemas: (schemas) => {
+ if (get().bundleLoaded) {
+ set({
+ loaded: true,
+ loading: false,
+ });
+ return;
+ }
const merged = mergeSchemas(initialModuleTypes, schemas);
const mergedMap = Object.fromEntries(merged.map((t) => [t.type, t]));
set({
@@ -294,6 +406,8 @@ const useModuleSchemaStore = create((set, get) => ({
},
loadPluginSchemas: (plugins) => {
+ if (get().bundleLoaded) return;
+
const { moduleTypes, moduleTypeMap } = get();
const newTypes = [...moduleTypes];
const newMap = { ...moduleTypeMap };
@@ -313,6 +427,75 @@ const useModuleSchemaStore = create((set, get) => ({
set({ moduleTypes: newTypes, moduleTypeMap: newMap });
},
+
+ loadEditorBundle: (bundle) => {
+ const normalized = normalizeEditorContractBundle(bundle);
+ const bundleServerSchemas = Object.fromEntries(
+ Object.entries(normalized.moduleSchemas).map(([type, schema]) => [
+ type,
+ bundleSchemaToServerSchema(type, schema),
+ ]),
+ );
+ const merged = mergeSchemas(initialModuleTypes, bundleServerSchemas);
+ const mergedMap = Object.fromEntries(merged.map((t) => [t.type, t]));
+ const stepTypeMap = {
+ ...initialStepTypeMap,
+ ...Object.fromEntries(
+ Object.entries(normalized.stepSchemas).map(([type, schema]) => [
+ type,
+ bundleStepToStepTypeInfo(type, schema),
+ ]),
+ ),
+ };
+ const { contracts, contractOwnerIndex } = mergeContractsByOwner(
+ get().contracts,
+ get().contractOwnerIndex,
+ normalized.contracts,
+ );
+
+ set({
+ serverSchemas: bundleServerSchemas,
+ moduleTypes: merged,
+ moduleTypeMap: mergedMap,
+ stepTypeMap,
+ coercionRules: { ...initialCoercionRules, ...normalized.coercionRules },
+ contracts,
+ contractOwnerIndex,
+ messages: { ...get().messages, ...normalized.messages },
+ yamlSchemas: normalized.schemas,
+ loaded: true,
+ loading: false,
+ bundleLoaded: true,
+ });
+ },
+
+ getContractByOwner: (ownerType, ownerKey) => {
+ const id = get().contractOwnerIndex[contractOwnerKey(ownerType, ownerKey)];
+ return id ? get().contracts[id] : undefined;
+ },
+
+ getMessage: (messageId) => get().messages[messageId],
+
+ getYamlSchema: (schemaName) => get().yamlSchemas[schemaName],
+
+ resetSchemaState: () => {
+ set({
+ loaded: false,
+ loading: false,
+ serverSchemas: {},
+ moduleTypes: initialModuleTypes,
+ moduleTypeMap: initialModuleTypeMap,
+ stepTypeMap: initialStepTypeMap,
+ coercionRules: initialCoercionRules,
+ contracts: {},
+ contractOwnerIndex: {},
+ messages: {},
+ yamlSchemas: initialYamlSchemas,
+ bundleLoaded: false,
+ services: [],
+ servicesLoaded: false,
+ });
+ },
}));
export default useModuleSchemaStore;
diff --git a/src/types/editor.ts b/src/types/editor.ts
index 154ec35..9900ae3 100644
--- a/src/types/editor.ts
+++ b/src/types/editor.ts
@@ -75,6 +75,8 @@ export interface WorkflowEditorProps {
onSchemaRequest?: () => Promise;
/** Called when editor needs plugin schemas */
onPluginSchemaRequest?: () => Promise;
+ /** Called when editor needs the canonical Workflow editor contract bundle */
+ onEditorBundleRequest?: () => Promise;
/** Called when editor detects file: references in YAML and needs the host to resolve them.
* The host reads the file at the given path (relative to the open document) and returns its content.
* Returns null if file not found. */
@@ -130,6 +132,92 @@ export interface PluginSchemaData {
modules: Record;
}
+export type EditorContractMode = 'strict' | 'proto_with_legacy' | 'legacy';
+
+export type JsonSchemaObject = Record;
+
+export interface EditorYamlSchemas {
+ app: JsonSchemaObject;
+ infra?: JsonSchemaObject;
+ wfctl?: JsonSchemaObject;
+ [schemaName: string]: JsonSchemaObject | undefined;
+}
+
+export interface EditorContractDescriptor {
+ id: string;
+ plugin?: string;
+ ownerType: 'module' | 'step' | 'service';
+ ownerKey: string;
+ mode: EditorContractMode;
+ requestMessage?: string;
+ responseMessage?: string;
+ configMessage?: string;
+ descriptorSetRef?: string;
+ source: 'builtin' | 'plugin-manifest' | 'plugin-contracts-json' | 'live-plugin';
+ [field: string]: unknown;
+}
+
+export interface EditorMessageFieldDescriptor {
+ name: string;
+ type?: string;
+ label?: string;
+ number?: number;
+ repeated?: boolean;
+ required?: boolean;
+ description?: string;
+ defaultValue?: unknown;
+ [field: string]: unknown;
+}
+
+export interface EditorMessageDescriptor {
+ id: string;
+ name: string;
+ fullName?: string;
+ package?: string;
+ fields?: EditorMessageFieldDescriptor[];
+ [field: string]: unknown;
+}
+
+export interface EditorContractBundle {
+ version: string;
+ workflowVersion?: string;
+ moduleSchemas: Record;
+ stepSchemas?: Record;
+ coercionRules: Record;
+ contracts?: Record;
+ messages?: Record;
+ schemas: EditorYamlSchemas;
+ snippets?: unknown[];
+ descriptorSets?: Record;
+ dslReference?: unknown;
+ [field: string]: unknown;
+}
+
+export interface EngineBundleModuleSchema {
+ type?: string;
+ label?: string;
+ category?: string;
+ description?: string;
+ inputs?: { name: string; type: string; description?: string }[];
+ outputs?: { name: string; type: string; description?: string }[];
+ configFields?: import('./workflow').ConfigFieldDef[];
+ defaultConfig?: Record;
+ maxIncoming?: number | null;
+ maxOutgoing?: number | null;
+ ioSignature?: import('./workflow').IOSignature;
+ [field: string]: unknown;
+}
+
+export interface EngineBundleStepSchema {
+ type?: string;
+ plugin?: string;
+ description?: string;
+ configFields?: import('./workflow').ConfigFieldDef[];
+ outputs?: { key: string; type: string; description?: string }[];
+ readKeys?: string[];
+ [field: string]: unknown;
+}
+
/** Server-side module schema (matches moduleSchemaStore's existing format) */
export interface ServerModuleSchema {
label?: string;