@codemation/core-nodes 0.7.1 → 0.8.0

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 (53) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/LICENSE +1 -37
  3. package/dist/index.cjs +957 -70
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +527 -61
  6. package/dist/index.d.ts +527 -61
  7. package/dist/index.js +936 -69
  8. package/dist/index.js.map +1 -1
  9. package/dist/metadata.json +162 -0
  10. package/package.json +4 -3
  11. package/src/authoring/defineRestNode.types.ts +17 -2
  12. package/src/chatModels/CodemationChatModelConfig.ts +47 -0
  13. package/src/chatModels/CodemationChatModelFactory.ts +103 -0
  14. package/src/chatModels/ManagedModelFetcher.ts +23 -0
  15. package/src/http/HttpRequestExecutor.ts +10 -2
  16. package/src/http/SSRFBlockedError.ts +16 -0
  17. package/src/http/SsrfGuard.ts +141 -0
  18. package/src/http/httpRequest.types.ts +6 -0
  19. package/src/index.ts +4 -0
  20. package/src/nodes/AIAgentConfig.ts +66 -0
  21. package/src/nodes/AIAgentNode.ts +205 -27
  22. package/src/nodes/BM25Index.ts +90 -0
  23. package/src/nodes/CallbackNodeFactory.ts +7 -0
  24. package/src/nodes/CronTriggerFactory.ts +9 -1
  25. package/src/nodes/DeferredMetaToolStrategy.ts +200 -0
  26. package/src/nodes/DeferredMetaToolStrategyFactory.ts +18 -0
  27. package/src/nodes/HttpRequestNodeFactory.ts +10 -3
  28. package/src/nodes/ManualTriggerFactory.ts +16 -1
  29. package/src/nodes/ToolLoadingStrategy.ts +28 -0
  30. package/src/nodes/WebhookTriggerFactory.ts +16 -2
  31. package/src/nodes/aggregate.ts +13 -2
  32. package/src/nodes/aiAgent.ts +9 -0
  33. package/src/nodes/assertion.ts +14 -1
  34. package/src/nodes/collections/collectionDeleteNode.types.ts +6 -0
  35. package/src/nodes/collections/collectionFindOneNode.types.ts +6 -0
  36. package/src/nodes/collections/collectionGetNode.types.ts +6 -0
  37. package/src/nodes/collections/collectionInsertNode.types.ts +6 -0
  38. package/src/nodes/collections/collectionListNode.types.ts +6 -0
  39. package/src/nodes/collections/collectionUpdateNode.types.ts +6 -0
  40. package/src/nodes/filter.ts +14 -2
  41. package/src/nodes/httpRequest.ts +72 -8
  42. package/src/nodes/if.ts +14 -2
  43. package/src/nodes/mapData.ts +13 -2
  44. package/src/nodes/merge.ts +9 -2
  45. package/src/nodes/noOp.ts +0 -1
  46. package/src/nodes/split.ts +13 -2
  47. package/src/nodes/subWorkflow.ts +15 -2
  48. package/src/nodes/switch.ts +18 -2
  49. package/src/nodes/testTrigger.ts +13 -0
  50. package/src/nodes/wait.ts +7 -1
  51. package/src/workflowAuthoring/WorkflowChatModelFactory.types.ts +4 -0
  52. package/src/workflows/AIAgentConnectionWorkflowExpander.ts +6 -3
  53. package/tsconfig.json +3 -1
@@ -0,0 +1,200 @@
1
+ import type { ToolSet } from "ai";
2
+ import { jsonSchema } from "ai";
3
+ import { z } from "zod";
4
+ import type { BM25Index } from "./BM25Index";
5
+ import type {
6
+ FindToolsResult,
7
+ ToolLoadingStrategy,
8
+ ToolLoadingStrategyInitInput,
9
+ ToolLoadingStrategyTurnContext,
10
+ } from "./ToolLoadingStrategy";
11
+
12
+ const PINNED_TOOLS_SOFT_LIMIT = 8;
13
+ const PINNED_TOOLS_HARD_LIMIT = 16;
14
+
15
+ const FIND_TOOLS_NAME = "find_tools";
16
+ const FIND_TOOLS_DEFAULT_LIMIT = 5;
17
+
18
+ interface McpToolEntry {
19
+ readonly serverId: string;
20
+ readonly toolName: string;
21
+ readonly description: string;
22
+ readonly toolDef: ToolSet[string];
23
+ }
24
+
25
+ /**
26
+ * Default tool-loading strategy: BM25-indexed MCP tool deferral via a `find_tools` meta-tool.
27
+ *
28
+ * - Node-backed tools and pinned MCP tools are always included in every turn.
29
+ * - `find_tools(query, limit?)` is added to the tool set when MCP tools are indexed.
30
+ * - Tools surfaced by `find_tools` are included in subsequent turns.
31
+ *
32
+ * Not DI-managed; instantiated per agent execution by DeferredMetaToolStrategyFactory.
33
+ */
34
+ export class DeferredMetaToolStrategy implements ToolLoadingStrategy {
35
+ private nodeBackedTools: ToolSet = {};
36
+ private pinnedTools: ToolSet = {};
37
+ private mcpEntries: McpToolEntry[] = [];
38
+ private toolsByServerId = new Map<string, Map<string, ToolSet[string]>>();
39
+ private foundToolIds = new Set<string>();
40
+
41
+ constructor(
42
+ private readonly bm25: BM25Index,
43
+ private readonly warnFn: (message: string) => void,
44
+ ) {}
45
+
46
+ async initialize(input: ToolLoadingStrategyInitInput): Promise<void> {
47
+ this.nodeBackedTools = { ...input.nodeBackedTools };
48
+
49
+ const pinnedIds = input.pinnedMcpTools ?? [];
50
+ if (pinnedIds.length > PINNED_TOOLS_HARD_LIMIT) {
51
+ throw new Error(
52
+ `Agent config error: pinnedMcpTools count (${pinnedIds.length}) exceeds hard limit of ${PINNED_TOOLS_HARD_LIMIT}.`,
53
+ );
54
+ }
55
+ if (pinnedIds.length > PINNED_TOOLS_SOFT_LIMIT) {
56
+ this.warnFn(
57
+ `Agent config: pinnedMcpTools count (${pinnedIds.length}) is above soft limit (${PINNED_TOOLS_SOFT_LIMIT}); consider deferring some via find_tools.`,
58
+ );
59
+ }
60
+
61
+ const indexTexts: string[] = [];
62
+ for (const [serverId, toolSet] of input.mcpToolsByServer.entries()) {
63
+ const serverMap = new Map<string, ToolSet[string]>();
64
+ this.toolsByServerId.set(serverId, serverMap);
65
+
66
+ for (const [toolName, toolDef] of Object.entries(toolSet)) {
67
+ serverMap.set(toolName, toolDef);
68
+ const entry: McpToolEntry = {
69
+ serverId,
70
+ toolName,
71
+ description: toolDef.description ?? "",
72
+ toolDef,
73
+ };
74
+ this.mcpEntries.push(entry);
75
+ indexTexts.push(`${toolName} ${entry.description}`);
76
+ }
77
+ }
78
+
79
+ if (indexTexts.length > 0) {
80
+ this.bm25.add(indexTexts);
81
+ }
82
+
83
+ this.pinnedTools = {};
84
+ for (const pinnedId of pinnedIds) {
85
+ const colonIdx = pinnedId.indexOf(":");
86
+ if (colonIdx === -1) continue;
87
+ const serverId = pinnedId.slice(0, colonIdx);
88
+ const toolName = pinnedId.slice(colonIdx + 1);
89
+ const serverMap = this.toolsByServerId.get(serverId);
90
+ const toolDef = serverMap?.get(toolName);
91
+ if (toolDef) {
92
+ this.pinnedTools[toolName] = toolDef;
93
+ }
94
+ }
95
+ }
96
+
97
+ getToolsForTurn(context: ToolLoadingStrategyTurnContext): ToolSet {
98
+ const result: ToolSet = { ...this.nodeBackedTools, ...this.pinnedTools };
99
+
100
+ const priorIds = context.previousFoundToolIds ?? [];
101
+ for (const foundId of priorIds) {
102
+ const colonIdx = foundId.indexOf(":");
103
+ if (colonIdx === -1) continue;
104
+ const serverId = foundId.slice(0, colonIdx);
105
+ const toolName = foundId.slice(colonIdx + 1);
106
+ const toolDef = this.toolsByServerId.get(serverId)?.get(toolName);
107
+ if (toolDef && !(toolName in result)) {
108
+ result[toolName] = toolDef;
109
+ }
110
+ }
111
+
112
+ if (this.mcpEntries.length > 0) {
113
+ result[FIND_TOOLS_NAME] = this.buildFindToolsDefinition();
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ ownsToolName(toolName: string): boolean {
120
+ if (toolName === FIND_TOOLS_NAME) return true;
121
+ // Any tool that came from an MCP server is strategy-owned so the coordinator
122
+ // does not attempt to dispatch it as a node-backed tool.
123
+ for (const serverMap of this.toolsByServerId.values()) {
124
+ if (serverMap.has(toolName)) return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ async executeMetaTool(toolName: string, input: unknown): Promise<unknown> {
130
+ if (toolName === FIND_TOOLS_NAME) {
131
+ const parsed = z.object({ query: z.string(), limit: z.number().int().min(1).max(10).optional() }).parse(input);
132
+ const limit = parsed.limit ?? FIND_TOOLS_DEFAULT_LIMIT;
133
+ const hits = this.bm25.search(parsed.query, limit);
134
+ const results: FindToolsResult[] = hits.map((idx) => {
135
+ const entry = this.mcpEntries[idx];
136
+ return {
137
+ serverId: entry.serverId,
138
+ toolName: entry.toolName,
139
+ description: entry.description,
140
+ inputSchema: (entry.toolDef as unknown as { inputSchema?: unknown }).inputSchema,
141
+ };
142
+ });
143
+ return results;
144
+ }
145
+
146
+ // Route to the MCP tool's execute callback (injected by AgentMcpIntegrationImpl with
147
+ // telemetry + 403 detection wrapping).
148
+ for (const serverMap of this.toolsByServerId.values()) {
149
+ const toolDef = serverMap.get(toolName);
150
+ if (toolDef) {
151
+ const executeFn = (toolDef as unknown as { execute?: (input: unknown) => Promise<unknown> }).execute;
152
+ if (executeFn) {
153
+ return await executeFn(input);
154
+ }
155
+ throw new Error(`DeferredMetaToolStrategy: MCP tool "${toolName}" has no execute callback`);
156
+ }
157
+ }
158
+
159
+ throw new Error(`DeferredMetaToolStrategy: unknown meta-tool or MCP tool "${toolName}"`);
160
+ }
161
+
162
+ recordFoundTools(results: ReadonlyArray<FindToolsResult>): void {
163
+ for (const r of results) {
164
+ this.foundToolIds.add(`${r.serverId}:${r.toolName}`);
165
+ }
166
+ }
167
+
168
+ getFoundToolIds(): ReadonlyArray<string> {
169
+ return [...this.foundToolIds];
170
+ }
171
+
172
+ private buildFindToolsDefinition(): ToolSet[string] {
173
+ const inputSchemaRecord = {
174
+ type: "object" as const,
175
+ properties: {
176
+ query: {
177
+ type: "string" as const,
178
+ description: "Natural language description of what you want to do.",
179
+ },
180
+ limit: {
181
+ type: "integer" as const,
182
+ minimum: 1,
183
+ maximum: 10,
184
+ description: `Maximum number of tools to return (default ${FIND_TOOLS_DEFAULT_LIMIT}).`,
185
+ },
186
+ },
187
+ required: ["query"],
188
+ additionalProperties: false,
189
+ };
190
+
191
+ return {
192
+ description:
193
+ "Search for tools available from connected MCP servers. " +
194
+ "After this call, the tools listed in the result will be callable on your very next turn. " +
195
+ "Use this when you need a capability not visible in your current tool list. " +
196
+ "Do not attempt to call a tool name you have not seen yet — use find_tools to discover it first.",
197
+ inputSchema: jsonSchema(inputSchemaRecord),
198
+ } as unknown as ToolSet[string];
199
+ }
200
+ }
@@ -0,0 +1,18 @@
1
+ import { injectable } from "@codemation/core";
2
+ import { BM25Index } from "./BM25Index";
3
+ import { DeferredMetaToolStrategy } from "./DeferredMetaToolStrategy";
4
+ import type { ToolLoadingStrategy, ToolLoadingStrategyInitInput } from "./ToolLoadingStrategy";
5
+
6
+ /**
7
+ * Factory for creating and initializing a DeferredMetaToolStrategy per agent execution.
8
+ * Injected into AIAgentNode; each agent call creates its own initialized strategy instance.
9
+ * BM25Index is constructed here (this file is a composition root via the Factory suffix).
10
+ */
11
+ @injectable()
12
+ export class DeferredMetaToolStrategyFactory {
13
+ async create(input: ToolLoadingStrategyInitInput): Promise<ToolLoadingStrategy> {
14
+ const strategy = new DeferredMetaToolStrategy(new BM25Index(), (msg) => console.warn(msg));
15
+ await strategy.initialize(input);
16
+ return strategy;
17
+ }
18
+ }
@@ -5,6 +5,7 @@ import type { CredentialSession, HttpRequestSpec } from "../http/httpRequest.typ
5
5
  import { HttpRequestExecutor } from "../http/HttpRequestExecutor";
6
6
  import { HttpBodyBuilder } from "../http/HttpBodyBuilder";
7
7
  import { HttpUrlBuilder } from "../http/HttpUrlBuilder";
8
+ import { SsrfGuard } from "../http/SsrfGuard";
8
9
  import type { HttpRequestDownloadMode } from "./httpRequest";
9
10
  import { HttpRequest } from "./httpRequest";
10
11
 
@@ -41,7 +42,12 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
41
42
  // Build the request (headers, body encoding, URL query merge) once,
42
43
  // then make a SINGLE fetch call and decide what to do with the response.
43
44
  // This avoids a double-fetch regression for auto-mode binary responses.
44
- const executor = new HttpRequestExecutor(globalThis.fetch, new HttpBodyBuilder(), new HttpUrlBuilder());
45
+ const executor = new HttpRequestExecutor(
46
+ globalThis.fetch,
47
+ new HttpBodyBuilder(),
48
+ new HttpUrlBuilder(),
49
+ new SsrfGuard(ctx.config.allowedOutboundHosts),
50
+ );
45
51
  const { url: resolvedUrl, init } = await executor.buildRequest(spec, item);
46
52
 
47
53
  const response = await globalThis.fetch(resolvedUrl, init);
@@ -175,10 +181,11 @@ export class HttpRequestNode implements RunnableNode<HttpRequest<any, any>> {
175
181
  private async resolveCredential(
176
182
  ctx: NodeExecutionContext<HttpRequest<any, any>>,
177
183
  ): Promise<CredentialSession | undefined> {
178
- const slotKey = ctx.config.args.credentialSlot;
179
- if (!slotKey) {
184
+ const rawSlot = ctx.config.args.credentialSlot;
185
+ if (!rawSlot) {
180
186
  return undefined;
181
187
  }
188
+ const slotKey = typeof rawSlot === "string" ? rawSlot : rawSlot.name;
182
189
  try {
183
190
  return await ctx.getCredential<CredentialSession>(slotKey);
184
191
  } catch {
@@ -1,4 +1,4 @@
1
- import type { Items, TriggerNodeConfig, TypeToken } from "@codemation/core";
1
+ import type { Items, NodeInspectorSummaryRow, TriggerNodeConfig, TypeToken } from "@codemation/core";
2
2
 
3
3
  import { ItemsInputNormalizer } from "@codemation/core";
4
4
 
@@ -42,6 +42,21 @@ export class ManualTrigger<TOutputJson = unknown> implements TriggerNodeConfig<T
42
42
  ): string | undefined {
43
43
  return typeof value === "string" ? value : id;
44
44
  }
45
+
46
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
47
+ const rows: NodeInspectorSummaryRow[] = [{ label: "Trigger", value: "manual" }];
48
+ if (this.defaultItems && this.defaultItems.length > 0) {
49
+ const firstItem = this.defaultItems[0];
50
+ if (firstItem && typeof firstItem.json === "object" && firstItem.json !== null) {
51
+ const keys = Object.keys(firstItem.json as object);
52
+ if (keys.length > 0) {
53
+ rows.push({ label: "Initial input keys", value: keys.join(", ").slice(0, 80) });
54
+ }
55
+ }
56
+ rows.push({ label: "Default items", value: String(this.defaultItems.length) });
57
+ }
58
+ return rows;
59
+ }
45
60
  }
46
61
 
47
62
  export { ManualTriggerNode } from "./ManualTriggerNode";
@@ -0,0 +1,28 @@
1
+ import type { ToolSet } from "ai";
2
+
3
+ export interface FindToolsResult {
4
+ readonly serverId: string;
5
+ readonly toolName: string;
6
+ readonly description: string;
7
+ readonly inputSchema: unknown;
8
+ }
9
+
10
+ export interface ToolLoadingStrategyTurnContext {
11
+ readonly turnIndex: number;
12
+ readonly previousFoundToolIds?: ReadonlyArray<string>;
13
+ }
14
+
15
+ export interface ToolLoadingStrategyInitInput {
16
+ readonly nodeBackedTools: ToolSet;
17
+ readonly mcpToolsByServer: ReadonlyMap<string, ToolSet>;
18
+ readonly pinnedMcpTools?: ReadonlyArray<string>;
19
+ }
20
+
21
+ export interface ToolLoadingStrategy {
22
+ initialize(input: ToolLoadingStrategyInitInput): Promise<void>;
23
+ getToolsForTurn(context: ToolLoadingStrategyTurnContext): ToolSet;
24
+ ownsToolName(toolName: string): boolean;
25
+ executeMetaTool(toolName: string, input: unknown): Promise<unknown>;
26
+ recordFoundTools(results: ReadonlyArray<FindToolsResult>): void;
27
+ getFoundToolIds(): ReadonlyArray<string>;
28
+ }
@@ -1,4 +1,11 @@
1
- import type { HttpMethod, Items, NodeExecutionContext, TriggerNodeConfig, TypeToken } from "@codemation/core";
1
+ import type {
2
+ HttpMethod,
3
+ Items,
4
+ NodeExecutionContext,
5
+ NodeInspectorSummaryRow,
6
+ TriggerNodeConfig,
7
+ TypeToken,
8
+ } from "@codemation/core";
2
9
  import type { ZodType } from "zod";
3
10
  import { WebhookTriggerNode } from "./webhookTriggerNode";
4
11
 
@@ -13,7 +20,7 @@ export class WebhookTrigger<
13
20
  > implements TriggerNodeConfig<unknown> {
14
21
  readonly kind = "trigger" as const;
15
22
  readonly type: TypeToken<unknown> = WebhookTriggerNode;
16
- readonly icon = "lucide:webhook";
23
+ readonly icon = "lucide:globe";
17
24
 
18
25
  constructor(
19
26
  public readonly name: string,
@@ -48,4 +55,11 @@ export class WebhookTrigger<
48
55
  private static defaultHandler(items: Items): Items {
49
56
  return items;
50
57
  }
58
+
59
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
60
+ return [
61
+ { label: "Endpoint key", value: this.args.endpointKey },
62
+ { label: "Methods", value: this.args.methods.join(", ") },
63
+ ];
64
+ }
51
65
  }
@@ -1,5 +1,10 @@
1
- import type { Items, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
-
1
+ import type {
2
+ Items,
3
+ NodeExecutionContext,
4
+ NodeInspectorSummaryRow,
5
+ RunnableNodeConfig,
6
+ TypeToken,
7
+ } from "@codemation/core";
3
8
  import { AggregateNode } from "./AggregateNode";
4
9
 
5
10
  export class Aggregate<TIn = unknown, TOut = unknown> implements RunnableNodeConfig<TIn, TOut> {
@@ -17,6 +22,12 @@ export class Aggregate<TIn = unknown, TOut = unknown> implements RunnableNodeCon
17
22
  ) => TOut | Promise<TOut>,
18
23
  public readonly id?: string,
19
24
  ) {}
25
+
26
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
27
+ const fnName = this.aggregate.name;
28
+ if (!fnName) return undefined;
29
+ return [{ label: "Aggregator", value: fnName }];
30
+ }
20
31
  }
21
32
 
22
33
  export { AggregateNode } from "./AggregateNode";
@@ -17,3 +17,12 @@ export {
17
17
  type PlannedToolCall,
18
18
  type ResolvedTool,
19
19
  } from "./aiAgentSupport.types";
20
+ export { BM25Index } from "./BM25Index";
21
+ export { DeferredMetaToolStrategy } from "./DeferredMetaToolStrategy";
22
+ export { DeferredMetaToolStrategyFactory } from "./DeferredMetaToolStrategyFactory";
23
+ export type {
24
+ FindToolsResult,
25
+ ToolLoadingStrategy,
26
+ ToolLoadingStrategyInitInput,
27
+ ToolLoadingStrategyTurnContext,
28
+ } from "./ToolLoadingStrategy";
@@ -1,4 +1,11 @@
1
- import type { AssertionResult, Item, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
1
+ import type {
2
+ AssertionResult,
3
+ Item,
4
+ NodeExecutionContext,
5
+ NodeInspectorSummaryRow,
6
+ RunnableNodeConfig,
7
+ TypeToken,
8
+ } from "@codemation/core";
2
9
 
3
10
  import { AssertionNode } from "./AssertionNode";
4
11
 
@@ -37,6 +44,12 @@ export class Assertion<TInputJson = unknown> implements RunnableNodeConfig<TInpu
37
44
  this.icon = options.icon ?? "lucide:check-circle";
38
45
  this.assertions = options.assertions;
39
46
  }
47
+
48
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
49
+ const fnName = this.assertions.name;
50
+ if (!fnName) return undefined;
51
+ return [{ label: "Assertions fn", value: fnName }];
52
+ }
40
53
  }
41
54
 
42
55
  export { AssertionNode } from "./AssertionNode";
@@ -10,6 +10,12 @@ export const collectionDeleteNode = defineNode({
10
10
  collectionName: z.string(),
11
11
  id: z.string(),
12
12
  }),
13
+ inspectorSummary({ config }) {
14
+ const name = config.collectionName ?? "";
15
+ if (!name) return [];
16
+ const truncated = name.length > 80 ? `${name.slice(0, 79)}…` : name;
17
+ return [{ label: "Collection", value: truncated }];
18
+ },
13
19
  async execute(_args, { config, execution }) {
14
20
  const store = execution.collections?.[config.collectionName];
15
21
  if (!store) {
@@ -10,6 +10,12 @@ export const collectionFindOneNode = defineNode({
10
10
  collectionName: z.string(),
11
11
  where: z.record(z.string(), z.unknown()),
12
12
  }),
13
+ inspectorSummary({ config }) {
14
+ const name = config.collectionName ?? "";
15
+ if (!name) return [];
16
+ const truncated = name.length > 80 ? `${name.slice(0, 79)}…` : name;
17
+ return [{ label: "Collection", value: truncated }];
18
+ },
13
19
  async execute(_args, { config, execution }) {
14
20
  const store = execution.collections?.[config.collectionName];
15
21
  if (!store) {
@@ -10,6 +10,12 @@ export const collectionGetNode = defineNode({
10
10
  collectionName: z.string(),
11
11
  id: z.string(),
12
12
  }),
13
+ inspectorSummary({ config }) {
14
+ const name = config.collectionName ?? "";
15
+ if (!name) return [];
16
+ const truncated = name.length > 80 ? `${name.slice(0, 79)}…` : name;
17
+ return [{ label: "Collection", value: truncated }];
18
+ },
13
19
  async execute(_args, { config, execution }) {
14
20
  const store = execution.collections?.[config.collectionName];
15
21
  if (!store) {
@@ -10,6 +10,12 @@ export const collectionInsertNode = defineNode({
10
10
  collectionName: z.string(),
11
11
  data: z.record(z.string(), z.unknown()),
12
12
  }),
13
+ inspectorSummary({ config }) {
14
+ const name = config.collectionName ?? "";
15
+ if (!name) return [];
16
+ const truncated = name.length > 80 ? `${name.slice(0, 79)}…` : name;
17
+ return [{ label: "Collection", value: truncated }];
18
+ },
13
19
  async execute(_args, { config, execution }) {
14
20
  const store = execution.collections?.[config.collectionName];
15
21
  if (!store) {
@@ -12,6 +12,12 @@ export const collectionListNode = defineNode({
12
12
  offset: z.number().int().nonnegative().optional(),
13
13
  where: z.record(z.string(), z.unknown()).optional(),
14
14
  }),
15
+ inspectorSummary({ config }) {
16
+ const name = config.collectionName ?? "";
17
+ if (!name) return [];
18
+ const truncated = name.length > 80 ? `${name.slice(0, 79)}…` : name;
19
+ return [{ label: "Collection", value: truncated }];
20
+ },
15
21
  async execute(_args, { config, execution }) {
16
22
  const store = execution.collections?.[config.collectionName];
17
23
  if (!store) {
@@ -11,6 +11,12 @@ export const collectionUpdateNode = defineNode({
11
11
  id: z.string(),
12
12
  patch: z.record(z.string(), z.unknown()),
13
13
  }),
14
+ inspectorSummary({ config }) {
15
+ const name = config.collectionName ?? "";
16
+ if (!name) return [];
17
+ const truncated = name.length > 80 ? `${name.slice(0, 79)}…` : name;
18
+ return [{ label: "Collection", value: truncated }];
19
+ },
14
20
  async execute(_args, { config, execution }) {
15
21
  const store = execution.collections?.[config.collectionName];
16
22
  if (!store) {
@@ -1,5 +1,11 @@
1
- import type { Item, Items, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
-
1
+ import type {
2
+ Item,
3
+ Items,
4
+ NodeExecutionContext,
5
+ NodeInspectorSummaryRow,
6
+ RunnableNodeConfig,
7
+ TypeToken,
8
+ } from "@codemation/core";
3
9
  import { FilterNode } from "./FilterNode";
4
10
 
5
11
  export class Filter<TIn = unknown> implements RunnableNodeConfig<TIn, TIn> {
@@ -18,6 +24,12 @@ export class Filter<TIn = unknown> implements RunnableNodeConfig<TIn, TIn> {
18
24
  ) => boolean,
19
25
  public readonly id?: string,
20
26
  ) {}
27
+
28
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
29
+ const fnName = this.predicate.name;
30
+ if (!fnName) return undefined;
31
+ return [{ label: "Predicate", value: fnName }];
32
+ }
21
33
  }
22
34
 
23
35
  export { FilterNode } from "./FilterNode";
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  RetryPolicy,
3
+ type AnyCredentialType,
3
4
  type CredentialRequirement,
5
+ type NodeInspectorSummaryRow,
4
6
  type RetryPolicySpec,
5
7
  type RunnableNodeConfig,
6
8
  type TypeToken,
7
9
  } from "@codemation/core";
8
-
9
10
  import type { HttpBodySpec } from "../http/httpRequest.types";
10
11
  import {
11
12
  apiKeyCredentialType,
@@ -81,11 +82,19 @@ export class HttpRequest<
81
82
  /** Request body specification. For canvas use, pass a JSON string in `body.data`. */
82
83
  body?: HttpBodySpec;
83
84
  /**
84
- * Credential slot key. When set, the node resolves a credential via
85
- * `ctx.getCredential(credentialSlot)` and applies it to the request.
86
- * The slot must be declared in `getCredentialRequirements()`.
85
+ * Credential slot.
86
+ *
87
+ * **String shorthand** (existing): `credentialSlot: "auth"` the slot accepts all four
88
+ * default HTTP credential types (bearer, API-key, basic, OAuth2).
89
+ *
90
+ * **Object form** (new): narrows the accepted types to the caller-supplied list, useful
91
+ * when only a subset of credential types makes sense for a specific endpoint.
92
+ * ```ts
93
+ * credentialSlot: { name: "auth", acceptedTypes: [bearerTokenCredentialType] }
94
+ * ```
95
+ * The slot must be declared in `getCredentialRequirements()`, which is wired automatically.
87
96
  */
88
- credentialSlot?: string;
97
+ credentialSlot?: string | Readonly<{ name: string; acceptedTypes?: ReadonlyArray<AnyCredentialType> }>;
89
98
  binaryName?: string;
90
99
  downloadMode?: HttpRequestDownloadMode;
91
100
  /**
@@ -109,6 +118,23 @@ export class HttpRequest<
109
118
  * Requests whose `Content-Length` exceeds this cap are rejected before the body is read.
110
119
  */
111
120
  responseSizeCapBytes?: number;
121
+ /**
122
+ * Operator-configurable outbound host allowlist.
123
+ *
124
+ * When set, every HTTP request target must match an entry in this list before the
125
+ * request is made — requests to any other host are rejected with {@link SSRFBlockedError}.
126
+ * Supports exact hostnames (`api.example.com`) and wildcard subdomain patterns
127
+ * (`*.example.com` matches `sub.example.com` but not `example.com` itself).
128
+ *
129
+ * When unset (default), the existing SSRF private-network guard applies:
130
+ * public hosts are allowed and private/loopback ranges are blocked.
131
+ *
132
+ * **Production warning**: when `NODE_ENV === "production"` and this is unset, a one-time
133
+ * warning is logged at workflow startup.
134
+ *
135
+ * Setting this to an empty array `[]` is equivalent to "block everything".
136
+ */
137
+ allowedOutboundHosts?: ReadonlyArray<string>;
112
138
  id?: string;
113
139
  }> = {},
114
140
  public readonly retryPolicy: RetryPolicySpec = RetryPolicy.defaultForHttp,
@@ -146,20 +172,58 @@ export class HttpRequest<
146
172
  return this.args.responseSizeCapBytes ?? DEFAULT_RESPONSE_SIZE_CAP_BYTES;
147
173
  }
148
174
 
175
+ get allowedOutboundHosts(): ReadonlyArray<string> | undefined {
176
+ return this.args.allowedOutboundHosts;
177
+ }
178
+
149
179
  getCredentialRequirements(): ReadonlyArray<CredentialRequirement> {
150
- if (!this.args.credentialSlot) {
180
+ const slot = this.args.credentialSlot;
181
+ if (!slot) {
151
182
  return [];
152
183
  }
184
+ if (typeof slot === "string") {
185
+ return [
186
+ {
187
+ slotKey: slot,
188
+ label: "Authentication",
189
+ acceptedTypes: HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES,
190
+ optional: true,
191
+ helpText: "Optional credential for authenticating the HTTP request.",
192
+ },
193
+ ];
194
+ }
195
+ // Object form: use caller-supplied acceptedTypes (mapped to typeIds), falling back to all defaults.
196
+ const acceptedTypes =
197
+ slot.acceptedTypes && slot.acceptedTypes.length > 0
198
+ ? slot.acceptedTypes.map((ct) => ct.definition.typeId)
199
+ : HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES;
153
200
  return [
154
201
  {
155
- slotKey: this.args.credentialSlot,
202
+ slotKey: slot.name,
156
203
  label: "Authentication",
157
- acceptedTypes: HTTP_REQUEST_ACCEPTED_CREDENTIAL_TYPES,
204
+ acceptedTypes,
158
205
  optional: true,
159
206
  helpText: "Optional credential for authenticating the HTTP request.",
160
207
  },
161
208
  ];
162
209
  }
210
+
211
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> {
212
+ const rows: NodeInspectorSummaryRow[] = [{ label: "Method", value: this.method }];
213
+ if (this.args.url) {
214
+ const url = this.args.url.length > 80 ? `${this.args.url.slice(0, 79)}…` : this.args.url;
215
+ rows.push({ label: "URL", value: url });
216
+ } else if (this.args.urlField) {
217
+ rows.push({ label: "URL field", value: this.args.urlField });
218
+ }
219
+ if (this.args.responseFormat) {
220
+ rows.push({ label: "Response format", value: this.args.responseFormat });
221
+ }
222
+ if (this.args.body && this.args.body.kind !== "none") {
223
+ rows.push({ label: "Body", value: this.args.body.kind });
224
+ }
225
+ return rows;
226
+ }
163
227
  }
164
228
 
165
229
  export { HttpRequestNode } from "./HttpRequestNodeFactory";
package/src/nodes/if.ts CHANGED
@@ -1,5 +1,11 @@
1
- import type { Item, Items, NodeExecutionContext, RunnableNodeConfig, TypeToken } from "@codemation/core";
2
-
1
+ import type {
2
+ Item,
3
+ Items,
4
+ NodeExecutionContext,
5
+ NodeInspectorSummaryRow,
6
+ RunnableNodeConfig,
7
+ TypeToken,
8
+ } from "@codemation/core";
3
9
  import { IfNode } from "./IfNode";
4
10
 
5
11
  export class If<TInputJson = unknown> implements RunnableNodeConfig<TInputJson, TInputJson> {
@@ -18,6 +24,12 @@ export class If<TInputJson = unknown> implements RunnableNodeConfig<TInputJson,
18
24
  ) => boolean,
19
25
  public readonly id?: string,
20
26
  ) {}
27
+
28
+ inspectorSummary(): ReadonlyArray<NodeInspectorSummaryRow> | undefined {
29
+ const fnName = this.predicate.name;
30
+ if (!fnName) return undefined;
31
+ return [{ label: "Predicate", value: fnName }];
32
+ }
21
33
  }
22
34
 
23
35
  export { IfNode } from "./IfNode";