@uipath/maestro-builder-sdk 6.14.3 → 6.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/serialize.js CHANGED
@@ -15,6 +15,7 @@ import { FlowAction, FlowTrigger } from './core/node-classes.js';
15
15
  import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
16
16
  import { dataFabricUnknownInputs } from './core/typed-raw-node.js';
17
17
  import { isAnswerField } from './core/hitl-answers.js';
18
+ import { connectionConnectors } from './bindings-v2.js';
18
19
  import { BindingRefusal, GUID_RE, bindingSelfNameMessage, bindingUndeclaredMessage, undeclaredBinding } from './core/binding-messages.js';
19
20
  import { ByoaRefusal, byoaUnverified } from './core/byoa.js';
20
21
  import { OUTCOME_HANDLE_NODE_TYPES, assertHitlExitInvariant, hitlExitModel, hitlExitViolation, outcomeSlug } from './core/hitl-routing.js';
@@ -979,8 +980,9 @@ const TOOL_RESOURCE_TYPE = {
979
980
  *
980
981
  * * `ixp`, `clientside` — measured to fail exactly that way (`ixp` leaves the
981
982
  * flow failing validate; `clientside` drops the four inputs its node needs).
982
- * * `connector` — `uip agent refresh` GENERATES it from the flow's connector
983
- * node, reconstructing a whole `properties` block the SDK should not duplicate.
983
+ * * `connector` — has its own writer, {@link connectorToolResourceJson}: its
984
+ * node carries the configuration in `inputs.detail`, which the strip keeps,
985
+ * and the file is the one `uip agent refresh` itself derives from it.
984
986
  * * `mcp`, `a2a` — need the server's `availableTools` and the remote agent's
985
987
  * card, which are tenant reads.
986
988
  * * `builtin`, `httpRequest` — need a `properties.toolType` that maps
@@ -1201,6 +1203,225 @@ function connectorToolDef(rc) {
1201
1203
  inputDefaults: {},
1202
1204
  };
1203
1205
  }
1206
+ /** The folder key a connection with no folder binding carries in its resource.json. */
1207
+ const NO_FOLDER_KEY = '00000000-0000-0000-0000-000000000000';
1208
+ /**
1209
+ * The library's operation description without the `(<Connector Name>) ` prefix
1210
+ * the v1def's `display.description` carries — the model reads this text, and
1211
+ * the designer's own resource.json has the bare sentence.
1212
+ */
1213
+ function connectorToolDescription(rc) {
1214
+ const text = String(rc.v1def?.display?.description ?? rc.entry?.display?.description ?? '');
1215
+ const prefix = `(${rc.connectorName}) `;
1216
+ return rc.connectorName && text.startsWith(prefix) ? text.slice(prefix.length) : text;
1217
+ }
1218
+ /** An agent-decided value: `{{prompt: "<what the model is told>"}}`. */
1219
+ function promptChip(f) {
1220
+ const text = f.description || f.displayName || f.name;
1221
+ return `{{prompt: "${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"}}`;
1222
+ }
1223
+ const isPromptChip = (v) => typeof v === 'string' && v.startsWith('{{prompt');
1224
+ /** The text inside `{{prompt: "…"}}`, unescaped; `''` for a bare `{{prompt}}`. */
1225
+ function chipText(value) {
1226
+ const m = /\{\{\s*prompt:\s*"((?:[^"\\]|\\.)*)"/.exec(value);
1227
+ return m ? m[1].replace(/\\(.)/g, '$1') : '';
1228
+ }
1229
+ /** A field's enum as `{ name, value }` rows, the platform's spelling; `undefined` when it has none. */
1230
+ function enumRows(raw) {
1231
+ if (!Array.isArray(raw) || raw.length === 0)
1232
+ return undefined;
1233
+ return raw.map((e) => (typeof e === 'string'
1234
+ ? { name: e, value: e }
1235
+ : { name: String(e?.name ?? e?.value ?? ''), value: String(e?.value ?? e?.name ?? '') }));
1236
+ }
1237
+ /**
1238
+ * The `inputs.detail` of a connector-tool node — the same key set a
1239
+ * `connector()` step's node carries, with the request buckets filled the way
1240
+ * `uip maestro flow node configure` fills them for a `tool.connector.*` node
1241
+ * (uipcli `buildAgentToolParamsFromMetadata`): every BODY field is
1242
+ * agent-decided (`{{prompt: "<description>"}}`) except a single-value enum,
1243
+ * which is its one constant (the model would otherwise guess — `google` for
1244
+ * `GoogleCustomSearch`); a QUERY/PATH parameter with a declared default sends
1245
+ * it, and otherwise is agent-decided too.
1246
+ */
1247
+ function connectorToolDetail(rc, conn, folder, seed) {
1248
+ const params = Array.isArray(rc.entry?.operation?.parameters) ? rc.entry.operation.parameters : [];
1249
+ const buckets = { body: {}, query: {}, path: {} };
1250
+ for (const f of rc.fields ?? []) {
1251
+ const loc = f.location ?? 'body';
1252
+ if (loc === 'body') {
1253
+ const only = enumRows(f.enum);
1254
+ buckets.body[f.name] = only && only.length === 1 ? only[0].value : promptChip(f);
1255
+ }
1256
+ else if (loc === 'query' || loc === 'path') {
1257
+ const dflt = params.find((p) => p?.name === f.name)?.defaultValue;
1258
+ buckets[loc][f.name] = dflt !== undefined && dflt !== null && dflt !== '' ? dflt : promptChip(f);
1259
+ }
1260
+ }
1261
+ const parts = rc.multipartParts ?? [];
1262
+ return {
1263
+ connector: rc.connectorKey,
1264
+ method: transportHttpMethod(rc.httpMethod),
1265
+ endpoint: rc.endpoint,
1266
+ ...(rc.objectName ? { objectName: rc.objectName } : {}),
1267
+ inputMetadata: parts.length
1268
+ ? { type: 'multipart', multipart: { bodyFieldName: multipartBodyFieldName(parts) } }
1269
+ : {},
1270
+ errorState: { hasError: false },
1271
+ telemetryData: { connectorKey: rc.connectorKey, connectorName: rc.connectorName },
1272
+ configuration: buildConfiguration(rc.entry, rc.version, {}, seed),
1273
+ ...(rc.activityTypeId ? { uiPathActivityTypeId: rc.activityTypeId } : {}),
1274
+ ...(conn ? { connectionId: conn, connectionResourceId: conn } : {}),
1275
+ ...(folder ? { connectionFolderKey: folder } : {}),
1276
+ bodyParameters: buckets.body,
1277
+ ...(Object.keys(buckets.path).length ? { pathParameters: buckets.path } : {}),
1278
+ ...(Object.keys(buckets.query).length ? { queryParameters: buckets.query } : {}),
1279
+ ...(parts.length ? { multipartParameters: parts.map((p) => ({ name: p.name, dataType: p.dataType })) } : {}),
1280
+ };
1281
+ }
1282
+ /** Walk (creating) the object chain for a dotted field name; returns the parent schema and the leaf key. */
1283
+ function nestedSlot(root, name) {
1284
+ const segments = name.split('.');
1285
+ let parent = root;
1286
+ for (const seg of segments.slice(0, -1)) {
1287
+ let child = parent.properties[seg];
1288
+ if (!child || child.type !== 'object') {
1289
+ child = { type: 'object', properties: {} };
1290
+ parent.properties[seg] = child;
1291
+ }
1292
+ parent = child;
1293
+ }
1294
+ return { parent, leaf: segments[segments.length - 1] };
1295
+ }
1296
+ /**
1297
+ * The `resources/<id>/resource.json` of a CONNECTOR tool (#867) — the file the
1298
+ * agent runtime registers the tool from, and the one that puts the connection
1299
+ * under the solution's Resources.
1300
+ *
1301
+ * The shape is uipcli's `mapConnectorToolToResource` (the writer behind `uip
1302
+ * agent refresh --inline-in-flow`, itself ported from flow-workbench's
1303
+ * `agent-connector-tool` mapper), in its metadata-enriched form: field types,
1304
+ * enums, `required` and the response fields come from the curated library,
1305
+ * which carries the same declarations that command fetches from Integration
1306
+ * Service. Where that writer and a Studio Web export differ, this follows the
1307
+ * writer: `enum`/`oneOf` only on agent-decided fields (a static field with an
1308
+ * enum breaks the runtime's model build), no `additionalProperties`, and
1309
+ * array-of-object response fields (`results[*]`) left out.
1310
+ *
1311
+ * The values come from the node's own `detail` — the same source refresh reads —
1312
+ * so a later refresh rewrites this file to itself.
1313
+ */
1314
+ function connectorToolResourceJson(ctx, id) {
1315
+ const { rc, detail } = ctx;
1316
+ const valueOf = (raw) => raw !== null && typeof raw === 'object' ? JSON.stringify(raw) : String(raw ?? '');
1317
+ const bucketOf = { body: 'bodyParameters', query: 'queryParameters', path: 'pathParameters' };
1318
+ const fields = [];
1319
+ for (const f of rc.fields ?? []) {
1320
+ const loc = f.location ?? 'body';
1321
+ if (loc === 'multipart')
1322
+ continue;
1323
+ const value = valueOf(detail[bucketOf[loc]]?.[f.name]);
1324
+ fields.push({
1325
+ name: f.name,
1326
+ fieldLocation: loc,
1327
+ type: f.type || 'string',
1328
+ ...(loc === 'body' && enumRows(f.enum)
1329
+ ? { enumValues: enumRows(f.enum) } : {}),
1330
+ required: f.required === true,
1331
+ description: f.description ?? '',
1332
+ ...(f.displayName ? { displayName: f.displayName } : {}),
1333
+ value,
1334
+ isPrompt: isPromptChip(value),
1335
+ });
1336
+ }
1337
+ for (const part of (detail.multipartParameters ?? [])) {
1338
+ fields.push({
1339
+ name: part.name, fieldLocation: 'multipart', type: part.dataType || 'string',
1340
+ required: false, description: '', value: '{{prompt}}', isPrompt: true,
1341
+ });
1342
+ }
1343
+ const inputSchema = { type: 'object', properties: {} };
1344
+ for (const f of fields) {
1345
+ if (f.fieldLocation === 'multipart')
1346
+ continue;
1347
+ const { parent, leaf } = nestedSlot(inputSchema, f.name);
1348
+ const prop = { type: f.type, title: f.displayName ?? leaf, description: f.description };
1349
+ if (f.isPrompt && f.enumValues) {
1350
+ prop.enum = f.enumValues.map((e) => e.value);
1351
+ prop.oneOf = f.enumValues.map((e) => ({ const: e.value, title: e.name }));
1352
+ }
1353
+ parent.properties[leaf] = prop;
1354
+ if (f.required)
1355
+ (parent.required ??= []).push(leaf);
1356
+ }
1357
+ const outputSchema = { type: 'object', properties: {}, definitions: {} };
1358
+ for (const f of (rc.entry?.outputSchema?.fields ?? [])) {
1359
+ if (typeof f?.name !== 'string' || f.name.includes('[*]'))
1360
+ continue;
1361
+ const { parent, leaf } = nestedSlot(outputSchema, f.name);
1362
+ parent.properties[leaf] = {
1363
+ type: f.type || 'string',
1364
+ title: f.displayName ?? leaf,
1365
+ ...(f.description ? { description: f.description } : {}),
1366
+ };
1367
+ }
1368
+ const multipart = detail.inputMetadata?.type === 'multipart';
1369
+ return {
1370
+ $resourceType: 'tool',
1371
+ name: ctx.name,
1372
+ description: ctx.description,
1373
+ location: 'external',
1374
+ type: 'integration',
1375
+ inputSchema,
1376
+ outputSchema,
1377
+ settings: {},
1378
+ properties: {
1379
+ toolPath: rc.endpoint ?? '',
1380
+ objectName: rc.objectName ?? '',
1381
+ toolDisplayName: ctx.name,
1382
+ toolDescription: ctx.description,
1383
+ // The operation KEY (`GETBYID` for a get-by-id), not the transport verb:
1384
+ // the runtime sends it to Integration Service to pick the operation.
1385
+ method: rc.httpMethod,
1386
+ connection: {
1387
+ id: detail.connectionId ?? '',
1388
+ name: rc.connectorName,
1389
+ elementInstanceId: 0,
1390
+ apiBaseUri: '',
1391
+ state: 'enabled',
1392
+ isDefault: false,
1393
+ connector: { key: rc.connectorKey, name: rc.connectorName, image: '' },
1394
+ folder: { key: detail.connectionFolderKey ?? NO_FOLDER_KEY, path: '' },
1395
+ },
1396
+ parameters: fields.map((f, i) => ({
1397
+ name: f.name,
1398
+ displayName: f.displayName ?? f.name,
1399
+ type: f.type,
1400
+ fieldLocation: f.fieldLocation,
1401
+ value: f.value,
1402
+ // uipcli's fallback: an undescribed agent-decided field keeps the chip's own text.
1403
+ description: f.description || (f.isPrompt ? chipText(f.value) : ''),
1404
+ position: f.required ? 'primary' : 'secondary',
1405
+ sortOrder: i + 1,
1406
+ required: f.required,
1407
+ fieldVariant: f.isPrompt ? 'dynamic' : 'static',
1408
+ isCascading: false,
1409
+ dynamic: f.isPrompt,
1410
+ enumValues: f.enumValues ?? null,
1411
+ loadReferenceOptionsByDefault: null,
1412
+ dynamicBehavior: [],
1413
+ reference: null,
1414
+ })),
1415
+ bodyStructure: multipart
1416
+ ? { contentType: 'multipart', jsonBodySection: detail.inputMetadata.multipart?.bodyFieldName ?? 'body' }
1417
+ : { contentType: 'json' },
1418
+ },
1419
+ guardrail: { policies: [] },
1420
+ iconUrl: ctx.iconUrl,
1421
+ id,
1422
+ isEnabled: true,
1423
+ };
1424
+ }
1204
1425
  /**
1205
1426
  * The tool kinds that wrap a DEPLOYED tenant resource, and everything that
1206
1427
  * differs between them. Transcribed from `registry get` over one live example
@@ -2306,9 +2527,15 @@ function connectionResourceBindings(spec, existing) {
2306
2527
  if (values.length === 0)
2307
2528
  return { entries: [] };
2308
2529
  const resourceKey = spec.resourceKey ?? spec.connectionId ?? '';
2309
- const have = (attr) => [...existing.values()].some((b) => b.resource === 'connection' && b.resourceKey === resourceKey && b.propertyAttribute === attr);
2310
- if (have('ConnectionId') && have('FolderKey'))
2311
- return { entries: [] };
2530
+ // Keyed on the NAME as well (flow-builder-sdk#865). The name is the definition's
2531
+ // own `<bindings.NAME>` placeholder, and two definitions sharing one connection
2532
+ // can spell it differently — managed HTTP says `uipath-uipath-http connection`,
2533
+ // the Slack connector `uipath-salesforce-slack connection`. Keyed on
2534
+ // resourceKey + attribute alone, whichever step came first claimed the
2535
+ // connection and the other node's placeholder never resolved: Studio showed
2536
+ // `invalid GUID <bindings.uipath-salesforce-slack connection>`.
2537
+ const have = (name, attr) => [...existing.values()].some((b) => b.resource === 'connection' && b.name === name && b.resourceKey === resourceKey
2538
+ && b.propertyAttribute === attr);
2312
2539
  const slug = spec.connectorKey.replace(/[^A-Za-z0-9]/g, '') || 'Connector';
2313
2540
  let base = `b${slug.charAt(0).toUpperCase()}${slug.slice(1)}`;
2314
2541
  for (let n = 2; existing.has(`${base}Connection`) || existing.has(`${base}FolderKey`); n++) {
@@ -2317,7 +2544,7 @@ function connectionResourceBindings(spec, existing) {
2317
2544
  const entries = values.flatMap((v) => {
2318
2545
  const name = typeof v.name === 'string' ? v.name : '';
2319
2546
  const attr = v.propertyAttribute;
2320
- if (!name || (attr !== 'ConnectionId' && attr !== 'FolderKey') || have(attr))
2547
+ if (!name || (attr !== 'ConnectionId' && attr !== 'FolderKey') || have(name, attr))
2321
2548
  return [];
2322
2549
  const isConnection = attr === 'ConnectionId';
2323
2550
  const dflt = isConnection ? spec.connectionId : spec.folderKey;
@@ -4705,6 +4932,7 @@ export function serialize(built, opts = {}) {
4705
4932
  let tdef;
4706
4933
  let label;
4707
4934
  let extraInputs = {};
4935
+ let connectorTool;
4708
4936
  if (tr.kind === 'builtin') {
4709
4937
  tdef = clone(BUILTIN_TOOL_DEFS[tr.tool]);
4710
4938
  if (!tdef)
@@ -4747,10 +4975,33 @@ export function serialize(built, opts = {}) {
4747
4975
  const rc = opts.library.resolve(tr.connector, tr.operation, tr.version, tr.object);
4748
4976
  tdef = connectorToolDef(rc);
4749
4977
  label = tr.name ?? rc.v1def?.display?.label ?? tr.operation;
4750
- // The connector tool's configuration (connection, folder) is bound at
4751
- // the agent/solution layer, not on the node: the deployed corpus
4752
- // carries `inputs: {}` and a top-level `bindings: []` even for a
4753
- // connector tool, so nothing is emitted here beyond the identity.
4978
+ // The connection rides the same three places it does for a
4979
+ // `connector()` step, because the platform reads it from the same
4980
+ // three (#867):
4981
+ // * the node's `inputs.detail` — what `uip maestro flow node configure`
4982
+ // writes on a `tool.connector.*` node, what `uip agent refresh
4983
+ // --inline-in-flow` builds the tool's resource.json FROM, and what
4984
+ // its shell-ify pass keeps ("where a connector tool's request config
4985
+ // lives"). It is also what `bindings_v2.json` is derived from.
4986
+ // * the top-level `bindings[]` pair the def's `model.context`
4987
+ // placeholders (`<bindings.… connection>` / `<bindings.FolderKey>`)
4988
+ // name.
4989
+ // * the agent's `resources/<id>/resource.json` (below).
4990
+ // Emitting none of them left the tool with no connection anywhere.
4991
+ const where = `inline agent "${step.name}"'s connector tool "${tr.connector}/${tr.operation}"`;
4992
+ const target = { key: tr.connector, action: tr.operation };
4993
+ const conn = resolveBinding(opts, tr.connection, 'connection', where, target);
4994
+ const folder = resolveBinding(opts, tr.folder, 'folder', where, target);
4995
+ const { entries: connBindings } = connectionResourceBindings({ def: rc.v1def, connectorKey: rc.connectorKey, connectionId: conn, folderKey: folder }, resourceBindings);
4996
+ for (const b of connBindings)
4997
+ resourceBindings.set(b.id, b);
4998
+ const detail = connectorToolDetail(rc, conn, folder, `${step.name}Tool`);
4999
+ extraInputs = { detail };
5000
+ connectorTool = {
5001
+ rc, detail, name: label,
5002
+ description: tr.description ?? connectorToolDescription(rc),
5003
+ iconUrl: String(tdef.display?.icon ?? ''),
5004
+ };
4754
5005
  }
4755
5006
  else if (tr.kind === 'mcp') {
4756
5007
  // Per-instance def minted like the ixp family's; the node carries the
@@ -4830,6 +5081,10 @@ export function serialize(built, opts = {}) {
4830
5081
  const tid = uid(scope, `${step.name}Tool`);
4831
5082
  const tnode = makeNode(tdef.nodeType, tid, label, { parentId, def: tdef });
4832
5083
  tnode.display = { ...tnode.display, shape: 'circle' };
5084
+ // `uip agent refresh` reads the tool's description off the node's display
5085
+ // first, so the node carries the one the resource.json does.
5086
+ if (connectorTool)
5087
+ tnode.display.description = connectorTool.description;
4833
5088
  // The resource model lives on the DEFINITION only (CX's rule for
4834
5089
  // context, and what the deployed corpus carries for tools).
4835
5090
  delete tnode.model;
@@ -4848,8 +5103,10 @@ export function serialize(built, opts = {}) {
4848
5103
  addEdge(scope, { node: id, port: 'tool' }, tid, 'input');
4849
5104
  // The other half, for the kinds whose file is DERIVABLE — see
4850
5105
  // `toolResourceJson` for which are not, and why each is left to the
4851
- // generator or the designer rather than guessed.
4852
- const toolResource = toolResourceJson(tr, { id: toolResourceId });
5106
+ // designer rather than guessed.
5107
+ const toolResource = connectorTool
5108
+ ? connectorToolResourceJson(connectorTool, toolResourceId)
5109
+ : toolResourceJson(tr, { id: toolResourceId });
4853
5110
  if (toolResource !== undefined) {
4854
5111
  sidecars.push({
4855
5112
  path: `${source}/resources/${toolResourceId}/resource.json`,
@@ -6280,8 +6537,84 @@ export function serialize(built, opts = {}) {
6280
6537
  // Passing the target is what lets a version-conditional step exist at all —
6281
6538
  // see migrate.ts's VERSION_CONDITIONAL ladder.
6282
6539
  migrateToSchemaVersion(flow, schemaVersion);
6540
+ // Emitted-document invariants over connections, checked on the FINISHED file
6541
+ // so every family and every emission order is covered by one rule, not by
6542
+ // each call site remembering it (flow-builder-sdk#865/#866).
6543
+ assertBindingPlaceholdersResolve(flow);
6544
+ connectionConnectors(flow);
6283
6545
  return flow;
6284
6546
  }
6547
+ /**
6548
+ * Every `<bindings.NAME>` placeholder a node's definition carries in its
6549
+ * `model.context` must resolve to a `bindings[]` entry for THAT node.
6550
+ *
6551
+ * "Resolve" is Studio Web's rule, not a looser one: flow-v1
6552
+ * `matchContextBindings` + `getInstanceBindingKey`
6553
+ * (flow-schema/src/packaging/resolve-context.ts) take the entries whose
6554
+ * `resource` EQUALS the definition's `model.bindings.resource`, scope them to the
6555
+ * node's `inputs.detail.connectionResourceId` when that resource is `connection`
6556
+ * (or undeclared), and look the placeholder up there BY NAME. Any miss blocks
6557
+ * publish (MST-15010), and one that slips through reaches Studio as the literal
6558
+ * placeholder: `invalid GUID <bindings.uipath-salesforce-slack connection>`.
6559
+ * `uip maestro flow validate` does not check this.
6560
+ *
6561
+ * That is how #865 shipped — a per-call-site dedupe that let the first step on a
6562
+ * shared connection claim it — and UV-16774 before it (a `Connection` resource
6563
+ * spelling). Checking the finished document catches that class for every family
6564
+ * and every emission order, whichever call site produced it.
6565
+ *
6566
+ * It holds for real definitions: of the 5,102 in the local registry index
6567
+ * (2026-09-30), 4,999 reference a `<bindings.…>` name and every one of them
6568
+ * declares that name in `model.bindings.values`, which is what the emitters
6569
+ * close. Agent resource definitions are the one exemption (see below).
6570
+ *
6571
+ * @throws BindingRefusal `BINDING_PLACEHOLDER_UNRESOLVED`, naming each dangling
6572
+ * placeholder and the step that carries it.
6573
+ */
6574
+ export function assertBindingPlaceholdersResolve(flow) {
6575
+ const bindings = Array.isArray(flow?.bindings) ? flow.bindings : [];
6576
+ const defs = new Map();
6577
+ for (const def of Array.isArray(flow?.definitions) ? flow.definitions : []) {
6578
+ defs.set(`${def?.nodeType}@${def?.version}`, def);
6579
+ }
6580
+ const nodes = [...(Array.isArray(flow?.nodes) ? flow.nodes : [])];
6581
+ for (const sub of Object.values(flow?.subflows ?? {})) {
6582
+ if (Array.isArray(sub?.nodes))
6583
+ nodes.push(...sub.nodes);
6584
+ }
6585
+ const dangling = [];
6586
+ for (const node of nodes) {
6587
+ // An agent's resource nodes (`uipath.agent.resource.tool.connector.…`) carry
6588
+ // the same placeholders, but they are resolved through the AGENT project —
6589
+ // `uip agent refresh` generates the connector tool's resource — and the
6590
+ // deployed corpus carries no flow `bindings[]` entry for them.
6591
+ if (typeof node?.type !== 'string' || node.type.startsWith('uipath.agent.resource.'))
6592
+ continue;
6593
+ const model = defs.get(`${node.type}@${node.typeVersion}`)?.model;
6594
+ if (!Array.isArray(model?.context))
6595
+ continue;
6596
+ const resource = model.bindings?.resource;
6597
+ const scope = resource === 'connection' || resource === undefined
6598
+ ? node.inputs?.detail?.connectionResourceId ?? node.inputs?.connectionId ?? undefined
6599
+ : undefined;
6600
+ const candidates = resource === undefined ? bindings : bindings.filter((b) => b?.resource === resource);
6601
+ for (const row of model.context) {
6602
+ const name = typeof row?.value === 'string' ? /^<bindings\.(.+)>$/.exec(row.value)?.[1] : undefined;
6603
+ if (name === undefined)
6604
+ continue;
6605
+ if (candidates.some((b) => b?.name === name && (!scope || b.resourceKey === scope)))
6606
+ continue;
6607
+ dangling.push(`step "${node.display?.label ?? node.id}" (${node.type}): "${row.value}"`
6608
+ + `${scope ? ` on connection ${scope}` : ''}`);
6609
+ }
6610
+ }
6611
+ if (dangling.length === 0)
6612
+ return;
6613
+ throw new BindingRefusal('BINDING_PLACEHOLDER_UNRESOLVED', `serialize: ${dangling.join('; ')} — no bindings[] entry of that name${dangling.length === 1 ? '' : ' (each)'} `
6614
+ + `is emitted for the step, so Studio Web reads the placeholder itself as the value (`
6615
+ + `"invalid GUID <bindings.…>") and refuses to publish. This is a serializer defect, not an authoring `
6616
+ + `error; report it with the flow source.`);
6617
+ }
6285
6618
  /**
6286
6619
  * Validate a connector's activity inputs against its declared field list and
6287
6620
  * return them ROUTED to the wire slots the platform reads. Catches the three
package/lib/README.md CHANGED
@@ -90,6 +90,7 @@ points at the materialized `library-json/`. Until you unpack, only
90
90
  | `uipath-atlassian-jira` | `create-issue` | `fields.project.key`, `fields.summary` |
91
91
  | `uipath-salesforce-slack` | `send-message-to-user` | `channel`, `messageToSend` |
92
92
  | `uipath-microsoft-outlook365` | `send-email` | `message.toRecipients` |
93
+ | `uipath-uipath-airdk` | `web-search` | `query` — the inline-agent connector-tool fixture (#867) |
93
94
 
94
95
  ## Refreshing the shipped library (needs an authenticated `uip`)
95
96
 
@@ -163,6 +163,15 @@
163
163
  "objectName": "send_message_to_user_v2",
164
164
  "path": "uipath-salesforce-slack/send-message-to-user@1.0.0.json"
165
165
  },
166
+ {
167
+ "nodeType": "uipath.connector.uipath-uipath-airdk.web-search",
168
+ "version": "1.0.0",
169
+ "connectorKey": "uipath-uipath-airdk",
170
+ "label": "Web Search",
171
+ "activityType": "Curated",
172
+ "objectName": "v2::webSearch",
173
+ "path": "uipath-uipath-airdk/web-search@1.0.0.json"
174
+ },
166
175
  {
167
176
  "nodeType": "uipath.connector.uipath-uipath-dataservice.query-entity-records",
168
177
  "version": "1.0.0",
@@ -0,0 +1,118 @@
1
+ {
2
+ "schemaVersion": "1",
3
+ "nodeType": "uipath.connector.uipath-uipath-airdk.web-search",
4
+ "version": "1.0.0",
5
+ "category": "connector.196640",
6
+ "tags": [
7
+ "connector",
8
+ "activity"
9
+ ],
10
+ "connector": {
11
+ "key": "uipath-uipath-airdk",
12
+ "name": "UiPath GenAI Activities"
13
+ },
14
+ "operation": {
15
+ "name": "Create",
16
+ "objectName": "v2::webSearch",
17
+ "objectDisplayName": "Web Search",
18
+ "httpMethod": "POST",
19
+ "path": "/v2/webSearch",
20
+ "subType": "standard",
21
+ "supportsStreaming": false,
22
+ "activityType": "Curated",
23
+ "pathTemplate": "/v2/webSearch"
24
+ },
25
+ "display": {
26
+ "label": "Web Search",
27
+ "description": "(UiPath GenAI Activities) Web search executes a search of the public domain using a natural language search query. The search results are available in the output. Web search offers near-real time search results, but certain time sensitive data like stock prices or weather will not consistently be current. There could be a few days lag. Be sure to pass in date/time and/or location as arguments in the query if required.",
28
+ "icon": "https://alpha.uipath.com/bc2ddac5-57bc-40e6-93fe-3b319b60ce36/studio_/typecache/icons/d93eefff0af32b964a4ce6fe3823de1d4c7ed71962c6f878520b57ebf8f4497a.svg",
29
+ "iconBackground": "linear-gradient(225deg, #FAFAFB 0%, #ECEDEF 100%)",
30
+ "iconBackgroundDark": "linear-gradient(225deg, #526069 0%, rgba(50, 60, 66, 0.6) 100%)",
31
+ "operationLabel": "Web Search"
32
+ },
33
+ "runtime": {
34
+ "bpmnType": "bpmn:SendTask",
35
+ "serviceType": "Intsvc.ActivityExecution",
36
+ "activityConfigurationVersion": "v1",
37
+ "requiresConnection": true,
38
+ "requiresFolderKey": true,
39
+ "requiresConnectionForSchema": false
40
+ },
41
+ "inputSchema": {
42
+ "fields": [
43
+ {
44
+ "name": "provider",
45
+ "type": "string",
46
+ "displayName": "Search Engine",
47
+ "description": "The search engine to use",
48
+ "enum": [
49
+ "GoogleCustomSearch"
50
+ ],
51
+ "required": true
52
+ },
53
+ {
54
+ "name": "query",
55
+ "type": "string",
56
+ "displayName": "Search",
57
+ "description": "The natural language query to search the web for",
58
+ "required": true
59
+ },
60
+ {
61
+ "name": "num",
62
+ "type": "integer",
63
+ "displayName": "Number of results",
64
+ "description": "The number of results. Default to 10.",
65
+ "required": false
66
+ },
67
+ {
68
+ "name": "start",
69
+ "type": "integer",
70
+ "displayName": "Start",
71
+ "description": "First result index (pagination).",
72
+ "required": false
73
+ },
74
+ {
75
+ "name": "siteSearch",
76
+ "type": "string",
77
+ "displayName": "Site Search",
78
+ "description": "Restrict or exclude results for a domain.",
79
+ "required": false
80
+ },
81
+ {
82
+ "name": "siteSearchFilter",
83
+ "type": "string",
84
+ "displayName": "Site Search Filter",
85
+ "description": "Either \"i\" (include) or \"e\" (exclude).",
86
+ "required": false
87
+ }
88
+ ]
89
+ },
90
+ "outputSchema": {
91
+ "fields": [
92
+ {
93
+ "name": "formattedResults",
94
+ "type": "string",
95
+ "displayName": "FormattedResults",
96
+ "description": "The search results string containing title, URL, and snippet"
97
+ },
98
+ {
99
+ "name": "results[*].title",
100
+ "type": "string",
101
+ "displayName": "Result title",
102
+ "description": "The title of a search result item."
103
+ },
104
+ {
105
+ "name": "results[*].snippet",
106
+ "type": "string",
107
+ "displayName": "Result snippet",
108
+ "description": "A brief summary or excerpt of a search result item."
109
+ },
110
+ {
111
+ "name": "results[*].url",
112
+ "type": "string",
113
+ "displayName": "Result url",
114
+ "description": "The web address of the search result."
115
+ }
116
+ ]
117
+ }
118
+ }