Skip to content

Commit 28dce33

Browse files
semdkibanamachine
andauthored
[One Workflow] Step registry completion context enhancement (elastic#260054)
## Summary issue: elastic/security-team#15982. (Resolves requirement #2) Enriches `SelectionContext` with step `values` so that `PropertySelectionHandler` implementations (`search`, `resolve`, `getDetails`) can access sibling property values from the current step definition. There are no existing steps using `context.values` yet — this is a prerequisite for upcoming steps from the Cases team that need to read sibling properties (e.g. `owner`) to scope their search/resolve logic. ### What changed **`SelectionContext.values`** — A new `values` field (`{ config, input }`) is populated from the step's YAML properties at the time `search`/`resolve`/`getDetails` are called. Handlers can now read sibling values like `context.values.input.owner` instead of having no visibility into the rest of the step. **YAML value extraction fix** — `getValueFromValueNode` now handles non-scalar YAML nodes (arrays/sequences) via `.toJSON()`, fixing a bug where array properties like `owner: [securitySolution]` appeared as `undefined` in `context.values`. **Example** — An example has been implemented in the `examples.externalStep` from the _workflows_examples_ plugin (test with `node scripts/kibana --dev --run-examples`) ### Demo https://github.com/user-attachments/assets/b0cf18ae-5906-4561-bf5e-31b228b08a30 ### Files changed | Area | Files | Change | |------|-------|--------| | Shared types | `kbn-workflows/types/v1.ts`, `latest.ts` | Added `StepSelectionValues` interface, `values` field on `SelectionContext`, generic type parameters on 4 interfaces | | Step registry | `workflows_extensions/.../step_registry/types.ts` | Thread `Config`/`Input` schema types into `StepPropertyHandler` for automatic inference | | Value builder | `build_workflow_lookup.ts` | Added `buildStepSelectionValues()` helper + fixed `getValueFromValueNode` for non-scalar nodes | | Context wiring | `collect_all_custom_property_items.ts`, `get_custom_property_suggestions.ts` | Pass `values` when constructing `SelectionContext` | | Docs | `STEPS.md` | Updated `SelectionContext` type definition and documented `context.values` | | Tests | `build_workflow_lookup.test.ts`, `validate_custom_properties.test.ts`, `get_custom_property_suggestions.test.ts` | New tests for value building (including arrays), updated assertions for new context shape | --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 72d6553 commit 28dce33

14 files changed

Lines changed: 264 additions & 34 deletions

File tree

examples/workflows_extensions_example/common/step_types/external_step.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const ConfigSchema = z.object({
4141
proxy: z.object({
4242
id: z.string(),
4343
url: z.string().optional(),
44+
ssl: z.boolean().optional().describe('When true, only HTTPS proxies are shown'),
4445
}),
4546
});
4647

examples/workflows_extensions_example/public/plugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export class WorkflowsExtensionsExamplePlugin
6969
'my-third-proxy': { name: 'Development Proxy', url: 'https://example.com/third' },
7070
'my-fourth-proxy': { name: 'Testing Proxy', url: 'https://example.com/fourth' },
7171
'my-fifth-proxy': { name: 'Backup Proxy', url: 'https://example.com/fifth' },
72+
'my-sixth-proxy': { name: 'HTTP Proxy', url: 'http://example.com/sixth' },
7273
}),
7374
});
7475
registerTriggerDefinitions(plugins.workflowsExtensions);

examples/workflows_extensions_example/public/step_types/external_step.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,40 +22,55 @@ export const getExternalStepDefinition = (deps: { externalService: IExampleExter
2222
config: {
2323
'proxy.id': {
2424
selection: {
25-
search: async (input, _context) => {
25+
search: async (input, context) => {
2626
const proxies = await deps.externalService.getProxies();
2727
const inputTrimmed = input.trim();
28+
const sslOnly = context.values.config.proxy?.ssl === true;
2829
return proxies
29-
.filter(
30-
(proxy) =>
30+
.filter((proxy) => {
31+
if (sslOnly && !proxy.url.startsWith('https://')) return false;
32+
return (
3133
inputTrimmed.length === 0 ||
3234
proxy.id.includes(inputTrimmed) ||
3335
proxy.name.toLowerCase().includes(inputTrimmed.toLowerCase())
34-
)
36+
);
37+
})
3538
.map((proxy) => ({
3639
value: proxy.id,
3740
label: proxy.name,
3841
description: 'URL: ' + proxy.url,
3942
}));
4043
},
41-
resolve: async (value, _context) => {
44+
resolve: async (value, context) => {
4245
const proxy = await deps.externalService.getProxy(value);
4346
if (!proxy) {
4447
return null;
4548
}
49+
const sslOnly = context.values.config.proxy?.ssl === true;
50+
if (sslOnly && !proxy.url.startsWith('https://')) {
51+
return null;
52+
}
4653
return {
4754
value: proxy.id,
4855
label: proxy.name,
4956
description: 'URL: ' + proxy.url,
5057
};
5158
},
52-
getDetails: async (value, _context, option) => {
59+
getDetails: async (value, context, option) => {
5360
if (option) {
5461
return {
5562
message: `Proxy "${option.label}" is connected`,
5663
links: [{ text: 'Manage proxies', path: 'https://example.com/proxies' }],
5764
};
5865
}
66+
const sslOnly = context.values.config.proxy?.ssl === true;
67+
const proxy = await deps.externalService.getProxy(value);
68+
if (proxy && sslOnly && !proxy.url.startsWith('https://')) {
69+
return {
70+
message: `Proxy "${proxy.name}" uses HTTP but proxy.ssl is enabled. Select an HTTPS proxy or set proxy.ssl to false.`,
71+
links: [{ text: 'Manage proxies', path: 'https://example.com/proxies' }],
72+
};
73+
}
5974
return {
6075
message: `Proxy "${value}" not found. Please select an existing proxy or create a new one.`,
6176
links: [

src/platform/packages/shared/kbn-workflows/types/latest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export type {
6464
SelectionOption,
6565
SelectionDetails,
6666
SelectionContext,
67+
StepSelectionValues,
6768
RequestOptions,
6869
GetAvailableConnectorsResponse,
6970
} from './v1';

src/platform/packages/shared/kbn-workflows/types/v1.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -503,12 +503,16 @@ export interface InternalConnectorContract extends BaseConnectorContract {
503503
};
504504
}
505505

506-
export interface StepPropertyHandler<T = unknown> {
506+
export interface StepPropertyHandler<
507+
T = unknown,
508+
TConfig extends Record<string, unknown> = Record<string, unknown>,
509+
TInput extends Record<string, unknown> = Record<string, unknown>
510+
> {
507511
/**
508512
* Entity selection configuration for the property.
509513
* Provides a unified interface for search, resolution, and decoration of entity references.
510514
*/
511-
selection?: PropertySelectionHandler<Exclude<T, undefined>>;
515+
selection?: PropertySelectionHandler<Exclude<T, undefined>, TConfig, TInput>;
512516
/**
513517
* Connector ID selection configuration for the property.
514518
* Used to resolve connector IDs for custom steps.
@@ -518,19 +522,29 @@ export interface StepPropertyHandler<T = unknown> {
518522
connectorIdSelection?: ConnectorIdSelectionHandler;
519523
}
520524

521-
export interface PropertySelectionHandler<T = unknown> {
525+
export interface PropertySelectionHandler<
526+
T = unknown,
527+
TConfig extends Record<string, unknown> = Record<string, unknown>,
528+
TInput extends Record<string, unknown> = Record<string, unknown>
529+
> {
522530
/**
523531
* Search for options matching the input query.
524532
* Used by autocomplete dropdowns when the user types.
525533
*/
526-
search: (input: string, context: SelectionContext) => Promise<SelectionOption<T>[]>;
534+
search: (
535+
input: string,
536+
context: SelectionContext<TConfig, TInput>
537+
) => Promise<SelectionOption<T>[]>;
527538

528539
/**
529540
* Resolve an entity by its value.
530541
* Used when loading existing values or when a value is pasted.
531542
* Returns null if the entity is not found.
532543
*/
533-
resolve: (value: T, context: SelectionContext) => Promise<SelectionOption<T> | null>;
544+
resolve: (
545+
value: T,
546+
context: SelectionContext<TConfig, TInput>
547+
) => Promise<SelectionOption<T> | null>;
534548

535549
/**
536550
* Get detailed information for the current value.
@@ -539,7 +553,7 @@ export interface PropertySelectionHandler<T = unknown> {
539553
*/
540554
getDetails: (
541555
input: string,
542-
context: SelectionContext,
556+
context: SelectionContext<TConfig, TInput>,
543557
option: SelectionOption<T> | null
544558
) => Promise<SelectionDetails>;
545559
}
@@ -567,13 +581,35 @@ export interface SelectionDetails {
567581
}>;
568582
}
569583

570-
export interface SelectionContext {
584+
/**
585+
* Structured values of the current step, split by scope.
586+
*
587+
* Built from scalar leaf properties in the YAML step definition.
588+
* Intermediate map nodes that have no scalar value are **not** represented;
589+
* a missing key means "not yet defined in the YAML", not "empty object".
590+
*/
591+
export interface StepSelectionValues<
592+
TConfig extends Record<string, unknown> = Record<string, unknown>,
593+
TInput extends Record<string, unknown> = Record<string, unknown>
594+
> {
595+
/** Root-level step properties (everything outside the `with` block). */
596+
config: TConfig;
597+
/** Properties nested under the `with` block. */
598+
input: TInput;
599+
}
600+
601+
export interface SelectionContext<
602+
TConfig extends Record<string, unknown> = Record<string, unknown>,
603+
TInput extends Record<string, unknown> = Record<string, unknown>
604+
> {
571605
/** The step type ID (e.g., "onechat.runAgent") */
572606
stepType: string;
573607
/** The property path ("config" or "input") */
574608
scope: 'config' | 'input';
575609
/** The property key (e.g., "agent_id") */
576610
propertyKey: string;
611+
/** Sibling values of the current step, keyed by scope. */
612+
values: StepSelectionValues<TConfig, TInput>;
577613
}
578614

579615
export interface ConnectorIdSelectionHandler {

src/platform/plugins/shared/workflows_extensions/dev_docs/STEPS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,16 +427,29 @@ interface SelectionDetails {
427427
}>;
428428
}
429429

430+
interface StepSelectionValues {
431+
/** Root-level step properties (everything outside the `with` block). */
432+
config: Record<string, unknown>;
433+
/** Properties nested under the `with` block. */
434+
input: Record<string, unknown>;
435+
}
436+
430437
interface SelectionContext {
431438
/** The step type ID (e.g., "ai.runAgent", "one-chat.invoke") */
432439
stepType: string;
433440
/** The property scope ("config" or "input") */
434441
scope: 'config' | 'input';
435442
/** The property key (e.g., "agent-id") */
436443
propertyKey: string;
444+
/** Sibling values of the current step, keyed by scope. */
445+
values: StepSelectionValues;
437446
}
438447
```
439448

449+
`context.values` gives handlers access to the current step's other property values.
450+
For example, if the YAML step has `with: { owner: securitySolution }`, the handler
451+
can read `context.values.input.owner`. Missing properties are `undefined`.
452+
440453
#### Example Implementation
441454

442455
For a complete working example, see the `external_step` implementation in `examples/workflows_extensions_example`:

src/platform/plugins/shared/workflows_extensions/public/step_registry/types.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,32 @@ export interface EditorHandlers<
5959
Output extends z.ZodType = z.ZodType,
6060
Config extends z.ZodObject = z.ZodObject
6161
> {
62-
config?: EditorHandlersConfig<Config>;
63-
input?: EditorHandlersInput<Input>;
62+
config?: EditorHandlersConfig<Config, Input>;
63+
input?: EditorHandlersInput<Input, Config>;
6464
dynamicSchema?: DynamicSchema<Input, Output, Config>;
6565
}
6666

67-
export type EditorHandlersConfig<Config extends z.ZodObject = z.ZodObject> = {
68-
[K in DotKeysOf<z.infer<Config>>]?: StepPropertyHandler<DotObject<z.infer<Config>>[K]>;
67+
export type EditorHandlersConfig<
68+
Config extends z.ZodObject = z.ZodObject,
69+
Input extends z.ZodType = z.ZodType
70+
> = {
71+
[K in DotKeysOf<z.infer<Config>>]?: StepPropertyHandler<
72+
DotObject<z.infer<Config>>[K],
73+
z.infer<Config>,
74+
Input extends z.ZodObject ? z.infer<Input> : Record<string, unknown>
75+
>;
6976
};
7077

71-
export type EditorHandlersInput<Input extends z.ZodType = z.ZodType> = Input extends z.ZodObject
78+
export type EditorHandlersInput<
79+
Input extends z.ZodType = z.ZodType,
80+
Config extends z.ZodObject = z.ZodObject
81+
> = Input extends z.ZodObject
7282
? {
73-
[K in DotKeysOf<z.infer<Input>>]?: StepPropertyHandler<DotObject<z.infer<Input>>[K]>;
83+
[K in DotKeysOf<z.infer<Input>>]?: StepPropertyHandler<
84+
DotObject<z.infer<Input>>[K],
85+
z.infer<Config>,
86+
z.infer<Input>
87+
>;
7488
}
7589
: {};
7690

src/platform/plugins/shared/workflows_management/public/entities/workflows/store/workflow_detail/utils/build_workflow_lookup.test.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
*/
99

1010
import { LineCounter, parseDocument } from 'yaml';
11-
import { buildWorkflowLookup, getValueFromValueNode, inspectStep } from './build_workflow_lookup';
11+
import {
12+
buildStepSelectionValues,
13+
buildWorkflowLookup,
14+
getValueFromValueNode,
15+
inspectStep,
16+
} from './build_workflow_lookup';
1217

1318
describe('inspectStep', () => {
1419
describe('simple step parsing', () => {
@@ -589,3 +594,68 @@ steps:
589594
expect(result.steps.step1.stepType).toBe('console');
590595
});
591596
});
597+
598+
describe('buildStepSelectionValues', () => {
599+
function getStep(yaml: string): ReturnType<typeof inspectStep>[string] {
600+
const lineCounter = new LineCounter();
601+
const yamlDocument = parseDocument(yaml, { lineCounter, keepSourceTokens: true });
602+
const stepsNode = (yamlDocument.contents as any).get('steps');
603+
const steps = inspectStep(stepsNode, lineCounter);
604+
return Object.values(steps)[0];
605+
}
606+
607+
it('should split config and input from step properties', () => {
608+
const step = getStep(`
609+
steps:
610+
- name: s1
611+
type: my.step
612+
connector-id: abc
613+
with:
614+
owner: securitySolution
615+
message: hello
616+
`);
617+
const values = buildStepSelectionValues(step);
618+
expect(values.config).toEqual({ 'connector-id': 'abc' });
619+
expect(values.input).toEqual({ owner: 'securitySolution', message: 'hello' });
620+
});
621+
622+
it('should handle nested with paths', () => {
623+
const step = getStep(`
624+
steps:
625+
- name: s1
626+
type: my.step
627+
with:
628+
inputs:
629+
field1: value1
630+
field2: value2
631+
`);
632+
const values = buildStepSelectionValues(step);
633+
expect(values.config).toEqual({});
634+
expect(values.input).toEqual({ inputs: { field1: 'value1', field2: 'value2' } });
635+
});
636+
637+
it('should handle array values under with', () => {
638+
const step = getStep(`
639+
steps:
640+
- name: s1
641+
type: my.step
642+
with:
643+
field1:
644+
- value1
645+
- value2
646+
`);
647+
const values = buildStepSelectionValues(step);
648+
expect(values.input).toEqual({ field1: ['value1', 'value2'] });
649+
});
650+
651+
it('should return empty objects when step has no custom properties', () => {
652+
const step = getStep(`
653+
steps:
654+
- name: s1
655+
type: my.step
656+
`);
657+
const values = buildStepSelectionValues(step);
658+
expect(values.config).toEqual({});
659+
expect(values.input).toEqual({});
660+
});
661+
});

0 commit comments

Comments
 (0)