diff --git a/skills/uipath-agents/SKILL.md b/skills/uipath-agents/SKILL.md index ded63218..96bf8327 100644 --- a/skills/uipath-agents/SKILL.md +++ b/skills/uipath-agents/SKILL.md @@ -1,6 +1,6 @@ --- name: uipath-agents -description: "UiPath agent lifecycle — coded (Python: LangGraph/LlamaIndex/OpenAI Agents) and low-code (agent.json from Agent Builder). Setup, auth, build, run, evaluate, deploy, sync. For C# or XAML workflows→uipath-rpa." +description: "UiPath agent lifecycle — coded (Python: LangGraph/LlamaIndex/OpenAI Agents) and low-code (agent.json from Agent Builder). Setup, auth, build, run, evaluate, deploy, sync, bindings. For C# or XAML workflows→uipath-rpa." allowed-tools: Bash, Read, Write, Glob, Grep, AskUserQuestion user-invocable: true --- diff --git a/skills/uipath-agents/assets/solutions/metadata.json b/skills/uipath-agents/assets/solutions/metadata.json new file mode 100644 index 00000000..d7e81d6b --- /dev/null +++ b/skills/uipath-agents/assets/solutions/metadata.json @@ -0,0 +1,199 @@ +{ + "_snapshotDate": "2026-04-24", + "_source": "https:////studio_/backend/api/resourcebuilder/metadata", + "_note": "Trimmed projection: agents only consume {kind, type} for SubType lookup. See bindings-reference.md → SubType Metadata for the refresh recipe.", + "entries": [ + { + "kind": "taskCatalog", + "type": null + }, + { + "kind": "mcpServer", + "type": "Coded" + }, + { + "kind": "mcpServer", + "type": "Command" + }, + { + "kind": "mcpServer", + "type": "Remote" + }, + { + "kind": "mcpServer", + "type": "UiPath" + }, + { + "kind": "remoteA2aAgent", + "type": "RemoteA2aAgent" + }, + { + "kind": "app", + "type": null + }, + { + "kind": "app", + "type": "Coded" + }, + { + "kind": "app", + "type": "CodedAction" + }, + { + "kind": "appVersion", + "type": null + }, + { + "kind": "connection", + "type": null + }, + { + "kind": "connector", + "type": null + }, + { + "kind": "choiceSet", + "type": null + }, + { + "kind": "entity", + "type": null + }, + { + "kind": "entity", + "type": "caseCommentEntity" + }, + { + "kind": "entity", + "type": "caseDocumentEntity" + }, + { + "kind": "entity", + "type": "caseMainEntity" + }, + { + "kind": "entity", + "type": "nativeEntity" + }, + { + "kind": "index", + "type": null + }, + { + "kind": "memorySpace", + "type": null + }, + { + "kind": "asset", + "type": "stringAsset" + }, + { + "kind": "asset", + "type": "integerAsset" + }, + { + "kind": "asset", + "type": "booleanAsset" + }, + { + "kind": "asset", + "type": "credentialAsset" + }, + { + "kind": "asset", + "type": "secretAsset" + }, + { + "kind": "bucket", + "type": "orchestratorBucket" + }, + { + "kind": "bucket", + "type": "amazonBucket" + }, + { + "kind": "bucket", + "type": "azureBucket" + }, + { + "kind": "businessRule", + "type": null + }, + { + "kind": "calendar", + "type": null + }, + { + "kind": "credentialStore", + "type": null + }, + { + "kind": "library", + "type": null + }, + { + "kind": "package", + "type": null + }, + { + "kind": "process", + "type": null + }, + { + "kind": "process", + "type": "agent" + }, + { + "kind": "process", + "type": "api" + }, + { + "kind": "process", + "type": "caseManagement" + }, + { + "kind": "process", + "type": "flow" + }, + { + "kind": "process", + "type": "mcpServer" + }, + { + "kind": "process", + "type": "process" + }, + { + "kind": "process", + "type": "processOrchestration" + }, + { + "kind": "process", + "type": "testAutomationProcess" + }, + { + "kind": "process", + "type": "webApp" + }, + { + "kind": "queue", + "type": null + }, + { + "kind": "trigger", + "type": "connectedEventTrigger" + }, + { + "kind": "trigger", + "type": "queueTrigger" + }, + { + "kind": "trigger", + "type": "timeTrigger" + }, + { + "kind": "webhook", + "type": null + } + ] +} diff --git a/skills/uipath-agents/references/coded/lifecycle/bindings-reference.md b/skills/uipath-agents/references/coded/lifecycle/bindings-reference.md index ff244ec9..883b4a9d 100644 --- a/skills/uipath-agents/references/coded/lifecycle/bindings-reference.md +++ b/skills/uipath-agents/references/coded/lifecycle/bindings-reference.md @@ -42,7 +42,8 @@ The eight bindable resource types are: | SDK Service | Method Pattern | Resource Type | Identifier Param | |------------|---------------|---------------|-----------------| -| `.assets.retrieve` / `.retrieve_async` / `.retrieve_credential` / `.retrieve_credential_async` | `("name", folder_path="folder")` | `asset` | `name` (positional) | +| `.assets.retrieve` / `.retrieve_async` | `("name", folder_path="folder")` | `asset` | `name` (positional) | +| `.assets.retrieve_credential` / `.retrieve_credential_async` | `("name", folder_path="folder")` | `asset` (with `SubType: "credentialAsset"` in metadata) | `name` (positional) | | `.queues.create_item` / `.create_item_async` / `.create_items` / `.create_items_async` / `.create_transaction_item` / `.create_transaction_item_async` | `item={"Name": "queue_name", ...}` or `queue_name="queue_name"` | `queue` | `item["Name"]` or `queue_name` | | `.processes.invoke` / `.invoke_async` | `(name="name", ..., folder_path="folder")` | `process` | `name` | | `.buckets.*` (all methods: `retrieve`, `upload`, `download`, `delete`, `list_files`, etc.) | `(name="name", folder_path="folder")` | `bucket` | `name` | @@ -55,6 +56,10 @@ Use Grep to find calls matching these patterns across all project Python files. **Important:** Only literal or constant-resolvable string arguments can be bound. If a value is truly dynamic (computed at runtime from function calls, f-strings with variables, environment variables, or user input), flag it to the user — it requires manual handling or refactoring. Module-level constants (e.g., `BUCKET = "my-bucket"`) are NOT dynamic — resolve them to their literal values. +**SubType inference during scanning:** + +For `retrieve_credential` / `retrieve_credential_async` calls, always emit `"SubType": "credentialAsset"` — the method name is definitive. For all other calls, follow the full lookup procedure in the **SubType Metadata** section below: fetch metadata → filter by kind → disambiguate from code → fall back to `AskUserQuestion` → omit `SubType` if the user skips. Omitting `SubType` is always safe — `uipath push` still creates a virtual placeholder for supported kinds, just with the base `kind` only. + ### Step 3: Compare with Existing Bindings Read the current `bindings.json` and compare: @@ -98,6 +103,7 @@ For the exact JSON structure of each resource type, consult `bindings-reference. - `ActivityName` in metadata always uses the `_async` variant name - Connection entries use `ConnectionId` instead of `name` and have no `folderPath` - The `app` resource type uses the app name as `DisplayLabel`; all others use `"FullName"` +- `SubType` is an optional metadata field — see the **SubType Metadata** section for the full lookup procedure and the `retrieve_credential*` shortcut. - Entrypoint fields (`EntryPointUniqueId`, `EntryPointPath`) are optional in any resource's `value` block, but when present must include a `displayName` set to the entrypoint's `filePath` from `entry-points.json` ### Step 6: Verify @@ -136,6 +142,8 @@ For detailed bindings.json schema, all eight resource type templates, SDK method | Duplicate key in resources array | Same resource scanned from multiple code paths | Deduplicate — keep one entry per unique key | | Missing project root | No `pyproject.toml` or `uipath.json` found | Verify the working directory is a UiPath agent project | | Stale entries after refactor | Old resource calls removed but bindings.json not updated | Run the full sync workflow to detect and remove orphaned entries | +| Push creates wrong asset type as virtual resource | Missing `SubType` in metadata for a `retrieve_credential*` call | Add `"SubType": "credentialAsset"` to the asset's metadata and re-run `uipath push` | +| Push warns "was not found" for connection/mcpServer/index | These kinds do not support virtual-resource fallback | Create the resource in Orchestrator / Integration Service before running `uipath push` | ## Additional Instructions @@ -217,6 +225,29 @@ credential = sdk.assets.retrieve_credential("cred_name", folder_path="folder_key - `name` — first positional argument to `retrieve_async()` / `retrieve()` - `folder_path` — keyword argument `folder_path=` +**Credential asset variant — `retrieve_credential` / `retrieve_credential_async`:** + +When the SDK call is `retrieve_credential` or `retrieve_credential_async`, add `"SubType": "credentialAsset"` to the metadata block. The rest of the binding entry is identical: + +```json +{ + "resource": "asset", + "key": ".", + "value": { + "name": { "defaultValue": "", "isExpression": false, "displayName": "Name" }, + "folderPath": { "defaultValue": "", "isExpression": false, "displayName": "Folder Path" } + }, + "metadata": { + "ActivityName": "retrieve_async", + "BindingsVersion": "2.2", + "DisplayLabel": "FullName", + "SubType": "credentialAsset" + } +} +``` + +This SubType is required for `uipath push` to create a credential-asset placeholder (rather than a plain string asset) when the asset doesn't yet exist in Orchestrator. + --- ### Queue @@ -530,6 +561,148 @@ server = sdk.mcp.retrieve(slug="mcp_server_slug", folder_path="folder_path") --- +## SubType Metadata + +The `SubType` field in a resource's `metadata` block specifies a sub-classification of the resource `kind`. It is **optional** but should be emitted when determinable, so `uipath push` can create the correct virtual-resource placeholder when the resource isn't found in the catalog (see `Virtual Resource Fallback on uipath push` below). + +### Authoritative source for valid SubType values + +The valid SubType values for each kind are defined by the Studio Web Resource Builder metadata. **Do not hardcode or guess** — always look them up from one of these two sources: + +1. **Preferred — live endpoint** (when authenticated against a UiPath tenant): + ``` + GET https:////studio_/backend/api/resourcebuilder/metadata + ``` + Example: `https://cloud.uipath.com/myorg/studio_/backend/api/resourcebuilder/metadata`. Returns the up-to-date list of supported kinds/types for the target tenant. + +2. **Fallback — bundled snapshot:** `assets/solutions/metadata.json` (relative to the skill root). Use when the live endpoint is unreachable or the agent is not authenticated. May be stale; the file's `_snapshotDate` shows when it was captured. + +### Metadata structure + +The two sources return slightly different shapes. The agent only needs `{kind, type}` from each entry — everything else can be ignored. + +**Live endpoint** — array of full entries: + +```json +[ + { "kind": "asset", "type": "stringAsset", "versions": [ ... ] }, + { "kind": "asset", "type": "credentialAsset", "versions": [ ... ] }, + { "kind": "queue", "versions": [ ... ] } +] +``` + +**Bundled snapshot** — wrapper with trimmed entries (only `kind` and `type` are kept): + +```json +{ + "_snapshotDate": "2026-04-24", + "_source": "https:////studio_/backend/api/resourcebuilder/metadata", + "_note": "...", + "entries": [ + { "kind": "asset", "type": "stringAsset" }, + { "kind": "asset", "type": "credentialAsset" }, + { "kind": "queue", "type": null } + ] +} +``` + +In both shapes: + +- `kind` — maps directly to the binding's `resource` field. +- `type` — optional. When present, this is the `SubType` value to emit. When `null` or absent, the kind has no sub-type. +- A kind may appear in multiple entries (one per valid sub-type). Example: `asset` has 5 entries (`stringAsset`, `integerAsset`, `booleanAsset`, `credentialAsset`, `secretAsset`). + +### Lookup procedure + +For each binding, follow these steps: + +1. **Fetch metadata.** Try the live endpoint first; on any failure (network error, auth failure, non-200 response), read `assets/solutions/metadata.json`. From the bundled snapshot, read the `entries` array; from the live endpoint, use the top-level array directly. +2. **Filter by `kind`.** Select all entries where `kind` equals the binding's `resource` value. +3. **Collect candidate `type` values.** Build a set from the `type` field of the matched entries, dropping `null`/absent values. +4. **Choose a SubType:** + - **No candidates** (all entries lack `type`) → omit `SubType`. + - **One candidate** → emit it as `SubType`. + - **Multiple candidates** → try code-based disambiguation first (see rules below). If no rule matches, **ask the user via `AskUserQuestion`**: present the candidate `type` values as a multiple-choice list plus a `None / skip` option, and include the resource name and folder in the prompt for context. If the user picks `None / skip`, omit `SubType`. + +### Refreshing the bundled snapshot + +When the live endpoint adds new kinds or types, regenerate `assets/solutions/metadata.json` from an authenticated tenant: + +```bash +curl -s -H "Authorization: Bearer " \ + "https:////studio_/backend/api/resourcebuilder/metadata" \ +| jq --arg date "$(date -u +%Y-%m-%d)" '{ + _snapshotDate: $date, + _source: "https:////studio_/backend/api/resourcebuilder/metadata", + _note: "Trimmed projection: agents only consume {kind, type} for SubType lookup.", + entries: [.[] | {kind, type}] + }' > skills/uipath-agents/assets/solutions/metadata.json +``` + +### Known code-based disambiguation rules + +Apply these rules in order. Only fall through to the `AskUserQuestion` prompt if none of them produces a confident match. + +| Kind | Rule | SubType | Confidence | +|------|------|---------|------------| +| `asset` | SDK call is `retrieve_credential` or `retrieve_credential_async` | `credentialAsset` | **High** — method name is definitive | +| `asset` | The returned value is used/type-annotated as `str` (plain text context, no password/secret naming) | `stringAsset` | Medium — confirm with user if unsure | +| `asset` | The returned value is used/type-annotated as `int` | `integerAsset` | Medium | +| `asset` | The returned value is used/type-annotated as `bool` | `booleanAsset` | Medium | +| `asset` | The variable name or surrounding context suggests a password/API key/credential (e.g. `password`, `api_key`, `secret`, `token`, `pwd`), even without `retrieve_credential*` | `credentialAsset` or `secretAsset` — **ask the user which** | Medium — presence of sensitive naming is a strong signal but `credentialAsset` vs `secretAsset` is not distinguishable from code | + +**How to apply the code-based heuristics for `retrieve` / `retrieve_async`:** + +1. Find the assignment target: `x = await sdk.assets.retrieve_async("my_asset", folder_path="Finance")`. +2. Check for a type annotation: `x: str = ...` → `stringAsset`; `x: int = ...` → `integerAsset`; `x: bool = ...` → `booleanAsset`. +3. If no annotation, check how `x` is used downstream: + - Passed to a function expecting `str`, concatenated with strings, used in string formatting → `stringAsset`. + - Used in arithmetic, compared numerically → `integerAsset`. + - Used in `if x:` as a truth check where Orchestrator stores it as boolean → `booleanAsset`. +4. Check the variable name and surrounding context against sensitive patterns (`password`, `pwd`, `api_key`, `secret`, `token`, `credential`). If a match is found, prompt the user to choose between `credentialAsset` and `secretAsset` (they differ in how Orchestrator exposes the value). +5. If the heuristic is inconclusive or produces only medium confidence, fall through to the `AskUserQuestion` prompt with the heuristic's best guess pre-highlighted. + +### Asking the user (example prompts) + +When multiple candidates remain, prompt with the full candidate list from the metadata. Example: + +- **Asset** (after excluding the credential shortcut above): *"Select the asset sub-type for `db_config` in folder `Finance`:"* → `stringAsset` / `integerAsset` / `booleanAsset` / `secretAsset` / *None (skip)* +- **Bucket**: *"Select the backing storage for bucket `reports` in folder `Shared`:"* → `orchestratorBucket` / `amazonBucket` / `azureBucket` / *None (skip)* +- **App (`sdk.tasks.*`)**: *"Select the app sub-type for `ApprovalApp` in folder `HR`:"* → `Coded` / `CodedAction` / *None (skip — default app type)* +- **MCP Server**: *"Select the MCP server sub-type for slug `my-server`:"* → `Coded` / `Command` / `Remote` / `UiPath` / *None (skip)* +- **Process (`sdk.processes.invoke` / `sdk.jobs.resume`)**: *"Select the target process sub-type for `InvoiceProcessor` in folder `Finance/Invoices`:"* → `process` / `agent` / `flow` / `api` / `caseManagement` / `processOrchestration` / `testAutomationProcess` / `webApp` / `mcpServer` / *None (skip)* + +Always include a `None / skip` option — it means "I don't know, emit no SubType." Batch prompts when possible (one question per binding, sent together) to minimize interruption. + +> **Preserve user-supplied SubType values.** When updating an existing `bindings.json`, do not overwrite a SubType value that is already present unless the referenced resource no longer exists in code. Do not re-prompt the user for bindings whose `SubType` is already set. + +### Behavior of SubType during push + +- If the resource is **found in the catalog**, `SubType` in metadata is ignored — the catalog's sub-type is used. +- If the resource is **not found in the catalog** and the kind supports virtual creation, `SubType` is passed as the `type` field of the virtual-resource creation request. A missing `SubType` for an ambiguous kind results in a virtual resource with only a base `kind`, which may fail or create a placeholder of the wrong type. + +--- + +## Virtual Resource Fallback on uipath push + +Starting with `uipath` 2.10.52, `uipath push` creates a virtual resource placeholder when a binding's resource isn't found in the Resource Catalog — the project can be deployed before its dependencies exist. The fallback only works for a subset of kinds. + +The push command fetches the supported-kinds list from `/studio_/backend/api/resourcebuilder/metadata` at push time. The static fallback (used when that endpoint is unreachable) is `app, asset, bucket, process, queue, taskCatalog, trigger`. + +| Catalog-miss behavior | Kinds | +|----------------------|---------| +| **Creates a virtual resource placeholder** (uses `SubType` from metadata for the placeholder's `type`). Project still deploys. | `app`, `asset`, `bucket`, `process`, `queue`, `taskCatalog`, `trigger` (static fallback; the live endpoint may add more) | +| **Warns and skips.** The resource must exist in Integration Service before push. | `connection` | +| **Warns and skips.** The resource must exist in Orchestrator before push. | `mcpServer`, `index`, and any kind absent from the supported list | + +### Implications for binding generation + +1. **Emit `SubType` where determinable.** Always emit `"SubType": "credentialAsset"` for `retrieve_credential*` asset calls. Without it, the virtual-resource fallback will create a plain string asset placeholder, causing runtime failures when the agent expects a credential. +2. **Warn the user about non-fallback kinds.** For `connection`, `mcpServer`, and `index` bindings, the referenced resource must exist in Orchestrator before `uipath push`. If it doesn't, the binding is skipped with a warning and the agent will fail at runtime. Flag this to the user when generating such bindings. +3. **Optional `SubType` for bucket.** The bucket's backing storage is not inferable from code. Consider asking the user which storage type their buckets use, so virtual-resource fallback creates the correct kind. + +--- + ## Entrypoint Binding Any resource in `bindings.json` can optionally be linked to an entrypoint defined in `entry-points.json`. This is done by adding `EntryPointUniqueId` and/or `EntryPointPath` to the resource's `value` block. @@ -612,17 +785,20 @@ Key fields for binding: ## SDK Method to Resource Type Mapping -| SDK Property | Methods | Resource Type | Resource Identifier Param | ActivityName | -|-------------|---------|---------------|--------------------------|-------------| -| `sdk.assets` | `retrieve`, `retrieve_async`, `retrieve_credential`, `retrieve_credential_async` | `asset` | `name` (1st positional) | `retrieve_async` | -| `sdk.queues` | `create_item`, `create_item_async`, `create_items`, `create_items_async`, `create_transaction_item`, `create_transaction_item_async` | `queue` | `item["Name"]` or `queue_name` | `create_item_async` | -| `sdk.processes` | `invoke`, `invoke_async` | `process` | `name` | `invoke_async` | -| `sdk.jobs` | `resume`, `resume_async` | `process` | `process_name` | `invoke_async` | -| `sdk.buckets` | ALL methods (`retrieve`, `upload`, `download`, `delete`, `list_files`, etc.) | `bucket` | `name` | `retrieve_async` | -| `sdk.tasks` | `create`, `create_async`, `retrieve`, `retrieve_async` | `app` | `app_name` | `create_async` | -| `sdk.context_grounding` | ALL methods (`retrieve`, `search`, `add_to_index`, `create_index`, `delete`, `ingest_data`, etc.) | `index` | `name` or `index_name` | `retrieve_async` | -| `sdk.connections` | `retrieve`, `retrieve_async` | `connection` | `key` (1st positional) | *(none)* | -| `sdk.mcp` | `retrieve`, `retrieve_async` | `mcpServer` | `slug` | `retrieve_async` | +The `SubType (default)` column below is a quick-reference for the most common case. **The authoritative list of valid SubType values per kind is the Resource Builder metadata** — always run the lookup procedure from `SubType Metadata` before emitting a value. + +| SDK Property | Methods | Resource Type | Resource Identifier Param | ActivityName | SubType (default) | +|-------------|---------|---------------|--------------------------|-------------|---------------------| +| `sdk.assets` | `retrieve`, `retrieve_async` | `asset` | `name` (1st positional) | `retrieve_async` | *(omit — code can't disambiguate `stringAsset` / `integerAsset` / `booleanAsset` / `secretAsset`)* | +| `sdk.assets` | `retrieve_credential`, `retrieve_credential_async` | `asset` | `name` (1st positional) | `retrieve_async` | `credentialAsset` | +| `sdk.queues` | `create_item`, `create_item_async`, `create_items`, `create_items_async`, `create_transaction_item`, `create_transaction_item_async` | `queue` | `item["Name"]` or `queue_name` | `create_item_async` | *(omit — kind has no sub-type)* | +| `sdk.processes` | `invoke`, `invoke_async` | `process` | `name` | `invoke_async` | *(omit — target process type not known here)* | +| `sdk.jobs` | `resume`, `resume_async` | `process` | `process_name` | `invoke_async` | *(omit — target process type not known here)* | +| `sdk.buckets` | ALL methods (`retrieve`, `upload`, `download`, `delete`, `list_files`, etc.) | `bucket` | `name` | `retrieve_async` | *(omit — ask user for `orchestratorBucket` / `amazonBucket` / `azureBucket`)* | +| `sdk.tasks` | `create`, `create_async`, `retrieve`, `retrieve_async` | `app` | `app_name` | `create_async` | *(omit — user chooses `Coded` / `CodedAction` / default)* | +| `sdk.context_grounding` | ALL methods (`retrieve`, `search`, `add_to_index`, `create_index`, `delete`, `ingest_data`, etc.) | `index` | `name` or `index_name` | `retrieve_async` | *(omit — must exist in Orchestrator; no virtual fallback)* | +| `sdk.connections` | `retrieve`, `retrieve_async` | `connection` | `key` (1st positional) | *(none)* | *(omit — must exist in Integration Service; no virtual fallback)* | +| `sdk.mcp` | `retrieve`, `retrieve_async` | `mcpServer` | `slug` | `retrieve_async` | *(omit — sub-type is one of `Coded` / `Command` / `Remote` / `UiPath`; ask user)* | **Note on ActivityName:** Always use the `_async` variant in the `ActivityName` metadata field, regardless of whether the code uses the sync or async version. @@ -725,6 +901,7 @@ async def main() -> Response: uipath = UiPath() asset = await uipath.assets.retrieve_async("my_asset", folder_path="Finance") + password = await uipath.assets.retrieve_credential_async("db_password", folder_path="Finance") conn = await uipath.connections.retrieve_async("salesforce_conn") result = await uipath.processes.invoke_async( name="InvoiceProcessor", input_arguments={"id": "123"}, folder_path="Finance/Invoices" @@ -750,6 +927,16 @@ async def main() -> Response: }, "metadata": { "ActivityName": "retrieve_async", "BindingsVersion": "2.2", "DisplayLabel": "FullName" } }, + { + "resource": "asset", + "key": "db_password.Finance", + "value": { + "name": { "defaultValue": "db_password", "isExpression": false, "displayName": "Name" }, + "folderPath": { "defaultValue": "Finance", "isExpression": false, "displayName": "Folder Path" }, + "EntryPointUniqueId": { "defaultValue": "708b62c7-15f1-46d8-9564-5d03c6a8668f", "isExpression": false, "displayName": "agent" } + }, + "metadata": { "ActivityName": "retrieve_async", "BindingsVersion": "2.2", "DisplayLabel": "FullName", "SubType": "credentialAsset" } + }, { "resource": "connection", "key": "salesforce_conn", diff --git a/tests/tasks/uipath-agents/subtype_credential_asset/check_subtype_credential_asset.py b/tests/tasks/uipath-agents/subtype_credential_asset/check_subtype_credential_asset.py new file mode 100644 index 00000000..e0d16781 --- /dev/null +++ b/tests/tasks/uipath-agents/subtype_credential_asset/check_subtype_credential_asset.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""SubType=credentialAsset binding check. + +Validates that the agent, given a Python file containing +`await sdk.assets.retrieve_credential_async("billing_api_key", +folder_path="Finance")`, produced a bindings.json that: + + 1. is well-formed JSON with `version == "2.0"` and a `resources` + array, + 2. contains exactly one resource entry whose `resource == "asset"` + and whose `key == "billing_api_key.Finance"`, + 3. carries the correct asset shape — `name.defaultValue == + "billing_api_key"` and `folderPath.defaultValue == "Finance"`, + 4. emits `metadata.SubType == "credentialAsset"`. This is the + specific rule introduced by the SubType inference change in + bindings-reference.md (high-confidence, method-name-definitive). + +A missing or wrong SubType is the primary failure mode the change +exists to prevent: without it, `uipath push` creates a plain +string-asset virtual placeholder for what should be a credential. +""" + +import json +import os +import sys +from pathlib import Path + +ROOT = Path(os.getcwd()) +BINDINGS = ROOT / "bindings.json" + +EXPECTED_NAME = "billing_api_key" +EXPECTED_FOLDER = "Finance" +EXPECTED_KEY = f"{EXPECTED_NAME}.{EXPECTED_FOLDER}" + + +def load(path: Path) -> dict: + if not path.is_file(): + sys.exit(f"FAIL: Missing {path}") + try: + return json.loads(path.read_text()) + except json.JSONDecodeError as e: + sys.exit(f"FAIL: {path} is not valid JSON: {e}") + + +def assert_envelope(bindings: dict) -> list: + version = bindings.get("version") + if version != "2.0": + sys.exit(f'FAIL: bindings.json version should be "2.0", got {version!r}') + resources = bindings.get("resources") + if not isinstance(resources, list) or not resources: + sys.exit(f"FAIL: bindings.json resources must be a non-empty list, got {resources!r}") + print(f'OK: bindings.json envelope is version="2.0" with {len(resources)} resource(s)') + return resources + + +def find_asset_entry(resources: list) -> dict: + matches = [ + r for r in resources + if isinstance(r, dict) + and r.get("resource") == "asset" + and r.get("key") == EXPECTED_KEY + ] + if not matches: + sys.exit( + f'FAIL: no resource with resource=="asset" and key=="{EXPECTED_KEY}" in ' + f"resources: {json.dumps(resources, indent=2)}" + ) + if len(matches) > 1: + sys.exit(f"FAIL: expected exactly one matching asset entry, got {len(matches)}") + print(f'OK: found asset entry with key="{EXPECTED_KEY}"') + return matches[0] + + +def assert_value_shape(entry: dict) -> None: + value = entry.get("value") + if not isinstance(value, dict): + sys.exit(f"FAIL: asset entry value must be an object: {value!r}") + name_block = value.get("name", {}) + if name_block.get("defaultValue") != EXPECTED_NAME: + sys.exit( + f'FAIL: value.name.defaultValue should be "{EXPECTED_NAME}", ' + f"got {name_block.get('defaultValue')!r}" + ) + folder_block = value.get("folderPath", {}) + if folder_block.get("defaultValue") != EXPECTED_FOLDER: + sys.exit( + f'FAIL: value.folderPath.defaultValue should be "{EXPECTED_FOLDER}", ' + f"got {folder_block.get('defaultValue')!r}" + ) + print( + f'OK: value.name="{EXPECTED_NAME}", value.folderPath="{EXPECTED_FOLDER}"' + ) + + +def assert_subtype(entry: dict) -> None: + metadata = entry.get("metadata") + if not isinstance(metadata, dict): + sys.exit(f"FAIL: asset entry metadata must be an object: {metadata!r}") + subtype = metadata.get("SubType") + if subtype != "credentialAsset": + sys.exit( + f'FAIL: metadata.SubType should be "credentialAsset" for a ' + f"retrieve_credential_async call, got {subtype!r}. Without " + "this, `uipath push` creates a plain string-asset placeholder " + "instead of a credential." + ) + print('OK: metadata.SubType == "credentialAsset"') + + +def main() -> None: + bindings = load(BINDINGS) + resources = assert_envelope(bindings) + entry = find_asset_entry(resources) + assert_value_shape(entry) + assert_subtype(entry) + + +if __name__ == "__main__": + main() diff --git a/tests/tasks/uipath-agents/subtype_credential_asset/subtype_credential_asset.yaml b/tests/tasks/uipath-agents/subtype_credential_asset/subtype_credential_asset.yaml new file mode 100644 index 00000000..4a28b44e --- /dev/null +++ b/tests/tasks/uipath-agents/subtype_credential_asset/subtype_credential_asset.yaml @@ -0,0 +1,78 @@ +task_id: skill-agent-subtype-credential-asset +description: > + Coded-agent bindings sync — verifies the agent emits + `SubType: "credentialAsset"` in bindings.json metadata for an + asset binding produced from an `sdk.assets.retrieve_credential_async` + call. Exercises the SubType inference rule (high-confidence, + method-name-definitive path) added to bindings-reference.md, which + is required so `uipath push` creates a credential-asset virtual + placeholder (not a string asset) when the asset doesn't yet exist + in Orchestrator. +tags: [uipath-agents, e2e, coded, bindings] +max_iterations: 1 + +agent: + type: claude-code + permission_mode: acceptEdits + allowed_tools: ["Skill", "Bash", "Read", "Write", "Edit", "Glob", "Grep"] + turn_timeout: 1200 + max_turns: 30 + +sandbox: + driver: tempdir + python: {} + +initial_prompt: | + Inside the working directory, create a minimal UiPath coded-agent + project layout (no `uip codedagent new` — author the files directly): + + - `pyproject.toml` with a `[project]` section (name "billing-agent", + version "0.1.0"). Do NOT include a `[build-system]` section. + - `uipath.json` with `{"entryPoints": []}` (empty is fine — bindings + sync doesn't depend on entry-point linking for this test). + - `entry-points.json` with one entrypoint named `main` and `filePath` + `main.py`. + - `main.py` containing exactly one bindable SDK call: + + ```python + from uipath.platform import UiPath + + async def main(input): + sdk = UiPath() + api_key = await sdk.assets.retrieve_credential_async( + "billing_api_key", folder_path="Finance" + ) + return {"ok": True} + ``` + + Then generate `bindings.json` by following the bindings sync + workflow in the uipath-agents skill (consult + `references/coded/lifecycle/bindings-reference.md` if needed). + The output must include the credential asset binding with + `metadata.SubType` set correctly. + + Do NOT scaffold via CLI. Do NOT publish, upload, or deploy. Do NOT + ask for approval, confirmation, or feedback. Do NOT pause between + planning and implementation. Complete the entire task end-to-end in + a single pass. + +success_criteria: + - type: file_exists + description: "main.py was created with the credential SDK call" + path: "main.py" + weight: 1.0 + pass_threshold: 1.0 + + - type: file_exists + description: "bindings.json was created" + path: "bindings.json" + weight: 2.0 + pass_threshold: 1.0 + + - type: run_command + description: "bindings.json emits SubType=credentialAsset for the retrieve_credential_async call" + command: "python3 $TASK_DIR/check_subtype_credential_asset.py" + timeout: 30 + expected_exit_code: 0 + weight: 5.0 + pass_threshold: 1.0