From 886248f38e970f03376f8f560ed979f6b7a7ff3b Mon Sep 17 00:00:00 2001 From: Pranami Jhawar Date: Mon, 8 Jun 2026 13:05:03 -0700 Subject: [PATCH] feat(aca-sandboxes): use metadata URL for Swagger and operation discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the legacy ARM managedApis apiOperations/export endpoints with the connection's runtime metadata URL (apim -> metadata + ?export=true) for all Swagger and operation discovery in the aca-sandboxes skill. This skill targets the design experience from outside the sandbox, so swagger discovery uses: - User-ACL on the connection (idempotent — GET first, PUT only if missing) - API Hub token (az rest --resource https://apihub.azure.com) Highlights: - All apiOperations and ARM managedApis?export=true URLs replaced with the metadata URL pattern - New references/swagger-discovery.md as the single source of truth for fetch + auth + parsing + operation matching (jq-based examples) - Idempotent user-ACL creation (GET first, PUT only if missing) — scoped to user-ACL only - Narrate progress rule so the agent announces each step with URL/objectId in chat (not just inside collapsed shell blocks) - Trimmed SKILL.md descriptions to stay under the 1024-char dev-time loader limit (--plugin-dir enforces this; marketplace install does not — but the trim keeps local dev unblocked) Scoped to aca-sandboxes skill. connectors skill is unchanged except for the same description trim. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- plugin/skills/aca-sandboxes/SKILL.md | 48 +++--- .../aca-sandboxes/references/direct-api.md | 34 +--- .../references/dynamic-values.md | 26 +--- .../aca-sandboxes/references/quickstart.md | 4 +- .../references/runtime-url-examples.md | 2 +- .../references/swagger-discovery.md | 147 ++++++++++++++++++ .../aca-sandboxes/references/trigger-flow.md | 9 +- .../aca-sandboxes/references/trigger-setup.md | 8 +- .../scripts/trigger-getting-started.py | 109 ++++++++++--- plugin/skills/connectors/SKILL.md | 24 ++- 10 files changed, 278 insertions(+), 133 deletions(-) create mode 100644 plugin/skills/aca-sandboxes/references/swagger-discovery.md diff --git a/plugin/skills/aca-sandboxes/SKILL.md b/plugin/skills/aca-sandboxes/SKILL.md index 0c93ca6..98e6bc1 100644 --- a/plugin/skills/aca-sandboxes/SKILL.md +++ b/plugin/skills/aca-sandboxes/SKILL.md @@ -2,24 +2,19 @@ name: azure-connectornamespace-aca-sandbox description: | Azure Connector Namespace — ACA sandbox edition. Manage connector namespaces, - connections, and triggers that wire external services (Office 365, Teams, Microsoft - Forms, SharePoint, OneDrive, GitHub, Azure Blob) into Azure Container Apps sandbox - apps via event-driven triggers or direct API calls using connection runtime URLs. + connections, and triggers that wire external services (Office 365, Teams, Forms, + SharePoint, OneDrive, GitHub, Azure Blob) into ACA sandbox apps via event-driven + triggers or direct API calls using connection runtime URLs. Use when: - - Creating or managing connector namespaces and connections for ACA sandboxes - - Creating or managing trigger configs whose callbacks target sandbox endpoints - - Subscribing to connector events (email, file, webhook, form submission, Teams message) - - Wiring event sources to ACA sandbox callbacks via `gatewayConnections[]` - - Managing trigger lifecycle (enable, disable, delete) - - Building sandbox apps that call connector APIs (send email, upload files, post Teams message, etc.) - - Reacting to events from one service and calling another (e.g., "when a form is submitted, send a Teams message") - - Automating workflows across Microsoft 365 services (Forms, Teams, Outlook, SharePoint, OneDrive) from within an ACA sandbox - Triggers: "create trigger", "trigger config", "webhook trigger", - "connector namespace", "connector", "connection", "email trigger", "send email", - "onedrive", "sharepoint", "teams", "teams message", "post message", - "microsoft forms", "forms", "form response", "form submission", "aca sandbox", - "sandbox group", "container apps sandbox", - "notify", "notification", "automate", "when", "on new" + - Managing connector namespaces and connections for ACA sandboxes + - Creating trigger configs whose callbacks target sandbox endpoints + - Wiring event sources to ACA sandbox callbacks via gatewayConnections + - Building sandbox apps that call connector APIs (send email, post Teams message, etc.) + - Reacting to events from one service and calling another + Triggers: "create trigger", "trigger config", "webhook trigger", "connector namespace", + "connector", "connection", "email trigger", "send email", "onedrive", "sharepoint", + "teams", "teams message", "microsoft forms", "form response", "aca sandbox", + "sandbox group", "container apps sandbox", "notify", "automate", "when", "on new" --- # Azure Connector Namespace — ACA sandbox edition @@ -53,6 +48,9 @@ to sandbox apps via direct API calls or event-driven triggers. | **SSL/stderr** | `REQUESTS_CA_BUNDLE` preferred. `verify=False` needs `disable_warnings()`. stderr = trigger failure. See [handler-guide.md](references/handler-guide.md). | | **Parallel execution** | Run independent ops (connections, ACLs, gatewayConnections wiring, dynamic values) as parallel tool calls. | | **Sandbox ↔ connection wiring** | Use the declarative **gatewayConnections** pattern (SG-level PATCH + per-sandbox PUT body). See [gateway-connections.md](references/gateway-connections.md). | +| **Swagger / operation discovery** | Always fetch the connector Swagger via the connection's runtime metadata URL — see [swagger-discovery.md](references/swagger-discovery.md) for the full pattern (auth, ACL idempotency, parsing, picking the right operation). | +| **ACL idempotency** | When creating a **user-ACL** on a connection (for swagger discovery from outside a sandbox), ALWAYS GET the policy first and only PUT if it doesn't exist. Never blindly recreate a user-ACL. (This does not apply to `gateway-acl` / `sandbox-acl` — those are typically PUT directly during setup.) | +| **Narrate progress** | The user must know what's happening at each step **before** you run the shell. Print a short chat message (NOT inside the shell block) with: (a) what you're doing in one line, (b) the exact URL or resource ID being touched, and (c) why. Never run a shell command silently or with only a collapsed shell block. | **When to STOP and ask the user:** Any parameter with dynamic values (teams, channels, folders, sites, lists), choosing integration pattern, OAuth consent. **You must NEVER skip this — always fetch the list and present it.** @@ -191,9 +189,9 @@ Ask the user: ### Step 5A: Direct API calls via dynamicInvoke -→ **Full details:** [direct-api.md](references/direct-api.md) | **Dynamic values:** [dynamic-values.md](references/dynamic-values.md) +→ **Full details:** [direct-api.md](references/direct-api.md) | **Swagger discovery:** [swagger-discovery.md](references/swagger-discovery.md) | **Dynamic values:** [dynamic-values.md](references/dynamic-values.md) -1. Get the connector Swagger (`managedApis/{connector}?export=true`) → extract operationId-to-path table +1. Fetch the connector Swagger and pick an operation — see [swagger-discovery.md](references/swagger-discovery.md). 2. Call `dynamicInvoke` on the connection with the resolved `method` + `path` 3. If parameters have `x-ms-dynamic-*` → resolve via dynamicInvoke, show display names to user, store values @@ -205,9 +203,9 @@ Ask the user: ### Step 5B: Event-driven triggers -→ **Full trigger setup (Steps 5B–9B):** [trigger-setup.md](references/trigger-setup.md) | **Dynamic values:** [dynamic-values.md](references/dynamic-values.md) +→ **Full trigger setup (Steps 5B–9B):** [trigger-setup.md](references/trigger-setup.md) | **Swagger discovery:** [swagger-discovery.md](references/swagger-discovery.md) | **Dynamic values:** [dynamic-values.md](references/dynamic-values.md) -1. Discover trigger operations: `GET .../managedApis/{connector}/apiOperations?api-version=2016-06-01` → filter for `properties.trigger` +1. Discover trigger operations — fetch the Swagger and filter paths whose operation has `x-ms-trigger` set. See [swagger-discovery.md](references/swagger-discovery.md). 2. If trigger type is `batch` (polling): inform user it polls every ~3 minutes by default. Ask if they want a different interval. 3. Collect parameters (resolve `x-ms-dynamic-*` via Swagger + dynamicInvoke) 4. Ask user: sandbox (existing/new) + callback type (ShellCommand / ExecuteCommand / InvokePort) @@ -256,11 +254,9 @@ az rest --method GET --url ".../connectorGateways/{gw}?api-version=2026-05-01-pr # Connections az rest --method GET --url ".../connectorGateways/{gw}/connections?api-version=2026-05-01-preview" -# List operations (summaries + trigger types) -az rest --method GET --url ".../providers/Microsoft.Web/locations/{location}/managedApis/{connector}/apiOperations?api-version=2016-06-01" - -# Get Swagger (full paths, parameters, x-ms-dynamic-* extensions) -az rest --method GET --url ".../providers/Microsoft.Web/locations/{location}/managedApis/{connector}" --url-parameters "api-version=2016-06-01" "export=true" +# Connector Swagger (paths, parameters, x-ms-dynamic-* extensions) — use to list operations or pick a specific one. +# See references/swagger-discovery.md for the full fetch pattern (metadata URL + user-ACL + API Hub token): +# az rest --method GET --url "?export=true" --resource "https://apihub.azure.com" # Dynamic invoke az rest --method POST --url ".../connectorGateways/{gw}/connections/{conn}/dynamicInvoke?api-version=2026-05-01-preview" --body '{"request":{"method":"GET","path":"/..."}}' diff --git a/plugin/skills/aca-sandboxes/references/direct-api.md b/plugin/skills/aca-sandboxes/references/direct-api.md index a6bda8d..b68c59c 100644 --- a/plugin/skills/aca-sandboxes/references/direct-api.md +++ b/plugin/skills/aca-sandboxes/references/direct-api.md @@ -7,39 +7,7 @@ Gateway injects stored OAuth credentials. **Use `request` format (NOT `parameter ## 1. Get the Swagger and select the operation -```powershell -# Get connector Swagger — save to file (ConvertFrom-Json fails on piped output) -az rest --method GET ` - --url "https://management.azure.com/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/managedApis/{connector}" ` - --url-parameters "api-version=2016-06-01" "export=true" -o json > $env:TEMP\swagger.json - -# Extract operationId → path table -python -c " -import json -with open(r'$env:TEMP\swagger.json') as f: - data = json.load(f) -paths = data.get('properties',{}).get('apiDefinitions',{}).get('value',{}).get('paths',{}) -for path, methods in paths.items(): - for method, details in methods.items(): - if isinstance(details, dict) and 'operationId' in details: - clean_path = path.replace('/{connectionId}', '') - print(f'{details[\"operationId\"]:40s} {method.upper():6s} {clean_path}') -" -``` - -To list available operations (for presenting choices or matching user intent): -```powershell -# Quick list of operations with summaries (lighter than full swagger) -az rest --method GET ` - --url "https://management.azure.com/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/managedApis/{connector}/apiOperations?api-version=2016-06-01" ` - --query "value[].{name:name, summary:properties.summary, trigger:properties.trigger}" -o table -``` - -Match user's intent to the best operation. If ambiguous, ask with specific choices. -Do NOT dump all operations for the user — choose the right one yourself. - -**To find the HTTP path for a chosen operationId:** Search the Swagger `paths` for the matching -`operationId`. Strip the `/{connectionId}` prefix — that's the path you pass to `dynamicInvoke`. +See [swagger-discovery.md](swagger-discovery.md) for the full pattern — auth (user-ACL idempotency + API Hub token), metadata URL derivation, Swagger parsing, and the keyword-matching algorithm for picking the right operation. ## 2. Collect parameter values interactively diff --git a/plugin/skills/aca-sandboxes/references/dynamic-values.md b/plugin/skills/aca-sandboxes/references/dynamic-values.md index cf5dcd2..1fa1f91 100644 --- a/plugin/skills/aca-sandboxes/references/dynamic-values.md +++ b/plugin/skills/aca-sandboxes/references/dynamic-values.md @@ -4,31 +4,9 @@ How to resolve connector parameters that require dynamic API calls. ## Step 1: Get the connector's Swagger (REQUIRED FIRST) -Before resolving any dynamic value, fetch the connector's **full Swagger definition**. -This gives you operationId → HTTP method + path mappings for all operations. +Before resolving any dynamic value, fetch the connector's **full Swagger definition** via the connection's runtime metadata URL. This gives you operationId → HTTP method + path mappings for all operations. -```powershell -# Fetch the full Swagger — MUST save to file first (ConvertFrom-Json fails on piped output) -az rest --method GET ` - --url "https://management.azure.com/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/managedApis/{connector}" ` - --url-parameters "api-version=2016-06-01" "export=true" -o json > $env:TEMP\swagger.json - -# Parse the swagger and extract operationId → path table -python -c " -import json -with open(r'$env:TEMP\swagger.json') as f: - data = json.load(f) -paths = data.get('properties',{}).get('apiDefinitions',{}).get('value',{}).get('paths',{}) -for path, methods in paths.items(): - for method, details in methods.items(): - if isinstance(details, dict) and 'operationId' in details: - clean_path = path.replace('/{connectionId}', '') - print(f'{details[\"operationId\"]:40s} {method.upper():6s} {clean_path}') -" -``` - -> **⚠️ PowerShell parsing issue:** `az rest` with `export=true` returns raw swagger that -> breaks `ConvertFrom-Json` when piped. Always save to file with `-o json > file.json` first. +→ See [swagger-discovery.md](swagger-discovery.md) for the full fetch pattern (user-ACL idempotency + API Hub token + parsing). **To find the path for an operationId** (e.g., `GetFolders`): - Look through `paths` → each path key (e.g., `/{connectionId}/datasets/default/folders`) has method entries (get, post, etc.) diff --git a/plugin/skills/aca-sandboxes/references/quickstart.md b/plugin/skills/aca-sandboxes/references/quickstart.md index e1dab0e..0f4bdd5 100644 --- a/plugin/skills/aca-sandboxes/references/quickstart.md +++ b/plugin/skills/aca-sandboxes/references/quickstart.md @@ -15,9 +15,7 @@ az rest --method GET \ --url "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/connectorGateways/{gw}/triggerConfigs?api-version=2026-05-01-preview" \ --query "value[].{name:name, state:properties.state, connector:properties.connectionDetails.connectorName}" -o table -# 4. Discover operations for a connector (classic locations endpoint) -az rest --method GET \ - --url "https://management.azure.com/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/managedApis/office365/apiOperations?api-version=2016-06-01" +# 4. Discover operations for a connector — see references/swagger-discovery.md. ``` For an end-to-end walkthrough, see [`tutorial-welcome-emailer.md`](tutorial-welcome-emailer.md). diff --git a/plugin/skills/aca-sandboxes/references/runtime-url-examples.md b/plugin/skills/aca-sandboxes/references/runtime-url-examples.md index bfad1b1..5bdd12d 100644 --- a/plugin/skills/aca-sandboxes/references/runtime-url-examples.md +++ b/plugin/skills/aca-sandboxes/references/runtime-url-examples.md @@ -13,7 +13,7 @@ Content-Type: application/json ## How to map Swagger operations to runtime URL calls -1. **Find the operation** from the connector's Swagger (use `GET .../locations/{location}/managedApis/{connector}/apiOperations?api-version=2016-06-01`) +1. **Find the operation** from the connector's Swagger — see [swagger-discovery.md](swagger-discovery.md). 2. **Build the URL**: - Base: `connectionRuntimeUrl` (from connection properties) diff --git a/plugin/skills/aca-sandboxes/references/swagger-discovery.md b/plugin/skills/aca-sandboxes/references/swagger-discovery.md new file mode 100644 index 0000000..0a949fb --- /dev/null +++ b/plugin/skills/aca-sandboxes/references/swagger-discovery.md @@ -0,0 +1,147 @@ +# Swagger Discovery — Connector Operations via Metadata URL + +How to fetch a connector's full Swagger and find the right operation to use. +This is the canonical reference for operation/trigger discovery in this skill. + +## Endpoint + +Fetch the Swagger from the **connection's runtime metadata URL**: + +- Take the connection's `connectionRuntimeUrl` (e.g., `https://{host}/apim/teams/{id}`) +- Replace `/apim/` with `/metadata/` +- Append `?export=true` + +Result: `https://{host}/metadata/teams/{id}?export=true` + +**Do NOT** use the legacy ARM endpoint +`https://management.azure.com/.../providers/Microsoft.Web/locations/{location}/managedApis/{connector}?api-version=2016-06-01&export=true`. +The ARM endpoint is a different API surface and should not be used by this skill, +even if it returns Swagger-shaped content. + +## Auth (outside-sandbox swagger discovery) + +To call the metadata URL from outside a sandbox (the typical design-time path), two things are required: + +1. A **user-ACL** on the connection for the signed-in user's objectId +2. An **API Hub token** (`az rest --resource "https://apihub.azure.com"`) + +### Step 1 — Ensure the user-ACL exists (idempotent: GET first, PUT only if missing) + +```powershell +$USER_OID = (az ad signed-in-user show --query id -o tsv) +$TENANT_ID = (az account show --query tenantId -o tsv) +$CONN_URL = "https://management.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/connectorGateways/{gw}/connections/{conn}" +$ACL_URL = "$CONN_URL/accessPolicies/$USER_OID`?api-version=2026-05-01-preview" + +# GET first — only create if missing +$existing = az rest --method GET --url $ACL_URL 2>$null +if (-not $existing) { + $connInfo = az rest --method GET --url "$CONN_URL`?api-version=2026-05-01-preview" | ConvertFrom-Json + $aclBody = @{ + location = $connInfo.location + properties = @{ + principal = @{ + type = "ActiveDirectory" + identity = @{ objectId = $USER_OID; tenantId = $TENANT_ID } + } + } + } | ConvertTo-Json -Depth 5 -Compress + $tmp = New-TemporaryFile; Set-Content $tmp $aclBody + az rest --method PUT --url $ACL_URL --body "@$tmp" + Remove-Item $tmp +} +``` + +> **Never blindly recreate a user-ACL.** Always GET first; PUT only if missing. +> This rule applies specifically to user-ACL creation for swagger discovery. +> `gateway-acl` and `sandbox-acl` are typically PUT directly during setup — +> see [gateway-connections.md](gateway-connections.md) — and may need updates +> when the gateway/sandbox-group principal changes. + +### Step 2 — Fetch the Swagger + +```powershell +$runtimeUrl = (az rest --method GET --url "$CONN_URL`?api-version=2026-05-01-preview" --query "properties.connectionRuntimeUrl" -o tsv) +$metadataUrl = $runtimeUrl -replace '/apim/', '/metadata/' +az rest --method GET --url "$metadataUrl`?export=true" --resource "https://apihub.azure.com" -o json > $env:TEMP\swagger.json +``` + +## Parse and find operations + +The response is a raw Swagger 2.0 document — `paths` is at the top level (no envelope wrapper). All operations live under `paths[][]`. Each operation entry has: + +- `operationId` — string identifier (use this when calling `dynamicInvoke`) +- `summary` / `description` — human-readable text +- `x-ms-visibility` — `important` / `default` / `advanced` / `internal` +- `x-ms-trigger` — object like `{"type": "polling"}` if the operation is a trigger; absent for callable actions + +### Quick listing + +Use `jq` to stream the swagger and print each operation (operationId, method, path). No temp file beyond what the metadata fetch already wrote, no separate parse process. Substitute the path you wrote the swagger to (`%TEMP%\swagger.json` in PowerShell, `/tmp/swagger.json` in bash): + +```bash +# Bash +jq -r '.paths | to_entries[] as $p | $p.value | to_entries[] + | select(.value.operationId) + | "\(.value.operationId)\t\(.key | ascii_upcase)\t\($p.key | sub("/\\{connectionId\\}"; ""))"' \ + /tmp/swagger.json +``` + +```powershell +# PowerShell — same jq, just the env var differs +jq -r '.paths | to_entries[] as $p | $p.value | to_entries[] + | select(.value.operationId) + | "\(.value.operationId)\t\(.key | ascii_upcase)\t\($p.key | sub("/\\{connectionId\\}"; ""))"' ` + "$env:TEMP\swagger.json" +``` + +### Find the right operation for the user's intent + +Filter the swagger by keyword on `operationId + summary + description`. For **direct API** (callable actions), exclude operations that have `x-ms-trigger` set and skip `x-ms-visibility: internal`. For **trigger discovery**, invert the filter — keep only operations with `x-ms-trigger` set. + +```bash +# Bash +jq -r --arg kw 'message|chat|post|send' ' + .paths | to_entries[] as $p | $p.value | to_entries[] + | select(.value.operationId) + | select(.value["x-ms-trigger"] | not) + | select(.value["x-ms-visibility"] != "internal") + | select((.value.operationId + " " + (.value.summary // "") + " " + (.value.description // "")) | ascii_downcase | test($kw)) + | "\(.value.operationId)\t\(.key | ascii_upcase)\t\($p.key | sub("/\\{connectionId\\}"; ""))\n \(.value.summary // "")"' \ + /tmp/swagger.json +``` + +```powershell +# PowerShell +jq -r --arg kw 'message|chat|post|send' ' + .paths | to_entries[] as $p | $p.value | to_entries[] + | select(.value.operationId) + | select(.value[\"x-ms-trigger\"] | not) + | select(.value[\"x-ms-visibility\"] != \"internal\") + | select((.value.operationId + \" \" + (.value.summary // \"\") + \" \" + (.value.description // \"\")) | ascii_downcase | test($kw)) + | \"\(.value.operationId)\t\(.key | ascii_upcase)\t\($p.key | sub(\"/\\{connectionId\\}\"; \"\"))\n \(.value.summary // \"\")\"' ` + "$env:TEMP\swagger.json" +``` + +### Picking the best match + +- **`x-ms-visibility`** — prefer `important` > `default` > `advanced`. Skip `internal`. +- **Versioned variants** — when V2/V3 exist, prefer the highest-numbered unless the user asks otherwise. +- **Intent match** — "send to user" → 1:1 chat operation; "post to channel" → channel operation; etc. +- **Ambiguity** — if multiple operations are plausible, ask the user with specific choices. Don't dump all of them. + +### Resolving operationId → HTTP path + +Look up the chosen operationId under the top-level `paths` object (e.g., `$swag.paths` in PowerShell). Strip the `/{connectionId}` prefix — that's the path you pass to `dynamicInvoke`. + +## Narrate progress + +Before running the shell commands above, print a short chat message naming what +you're doing and the exact URL/objectId being touched. Examples: + +- `Ensuring user-ACL exists on connection {conn} for objectId {oid} (GET {ACL_URL})` +- `Fetching Swagger from {metadataUrl}?export=true (--resource https://apihub.azure.com)` +- `Found {N} operations matching '{keywords}'. Picking {chosen_op}.` + +Never run a swagger-discovery shell command silently — the user shouldn't have to +expand a collapsed shell block to see what URL you're hitting. diff --git a/plugin/skills/aca-sandboxes/references/trigger-flow.md b/plugin/skills/aca-sandboxes/references/trigger-flow.md index 40688c9..e3e4458 100644 --- a/plugin/skills/aca-sandboxes/references/trigger-flow.md +++ b/plugin/skills/aca-sandboxes/references/trigger-flow.md @@ -102,12 +102,7 @@ az rest --method PUT ` ### Step 6: Discover Trigger Operations (optional) -```bash -# Discover operations for the connector -az rest --method GET \ - --url "https://management.azure.com/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/managedApis/office365/apiOperations?api-version=2016-06-01" -# Filter: trigger operations have non-empty "properties.trigger" field -``` +Fetch the connector's Swagger and filter operations with `x-ms-trigger` set. See [swagger-discovery.md](swagger-discovery.md) for the full pattern. ### Step 7: Manage Trigger Lifecycle @@ -179,5 +174,5 @@ objectIds to authenticate. The proxy URL uses `audience: https://auth.adcproxy.i | Handler runtime-URL calls 401/403 | Ensure (a) sandbox-group has SystemAssigned MI, (b) `sandbox-acl` exists on the connection, (c) sandbox-group `properties.gatewayConnections[]` references this connection, (d) the sandbox was created with `gatewayConnections: [{resourceId}]` in its data-plane PUT body. See [gateway-connections.md](gateway-connections.md) | | Sandbox not responding | Ensure sandbox is Running; for ShellCommand, use `activationMode: OnDemand` | | Port auth failure | Add gateway principalId to port's `auth.entraId.objectIds` on the sandbox | -| Parameters rejected | Get exact parameter names from the connector Swagger (`managedApis/{connector}?export=true`) | +| Parameters rejected | Get exact parameter names from the connector Swagger — see [swagger-discovery.md](swagger-discovery.md) | | Cleanup order | Delete trigger config → access policies → connection → sandbox → gateway. Always delete triggers first. | diff --git a/plugin/skills/aca-sandboxes/references/trigger-setup.md b/plugin/skills/aca-sandboxes/references/trigger-setup.md index 8d2e71e..a13ff3e 100644 --- a/plugin/skills/aca-sandboxes/references/trigger-setup.md +++ b/plugin/skills/aca-sandboxes/references/trigger-setup.md @@ -4,12 +4,8 @@ Detailed commands for creating event-driven triggers on a connector. ## Step 5B: Discover trigger operations -```bash -# Discover operations for the connector -az rest --method GET \ - --url "https://management.azure.com/subscriptions/{sub}/providers/Microsoft.Web/locations/{location}/managedApis/office365/apiOperations?api-version=2016-06-01" -# Filter: trigger operations have non-empty "properties.trigger" field -``` +Fetch the connector's Swagger and filter operations that have `x-ms-trigger` set. See [swagger-discovery.md](swagger-discovery.md) for the full fetch pattern (user-ACL idempotency + API Hub token + parsing). + Present operations as choices (summary + operationId). Let user pick. > **⚠️ Identifying recurrence/polling triggers:** Check the Swagger definition for the selected operation. diff --git a/plugin/skills/aca-sandboxes/scripts/trigger-getting-started.py b/plugin/skills/aca-sandboxes/scripts/trigger-getting-started.py index 4cfd05e..14d6791 100644 --- a/plugin/skills/aca-sandboxes/scripts/trigger-getting-started.py +++ b/plugin/skills/aca-sandboxes/scripts/trigger-getting-started.py @@ -58,9 +58,17 @@ print(f"Connection: {connection_name}") -def az_rest(method, url, body=None): - """Call az rest and return parsed JSON. Exits with helpful message on failure.""" +def az_rest(method, url, body=None, resource=None, ignore_errors=False): + """Call az rest and return parsed JSON. + + Prints an error message on failure and re-raises `CalledProcessError` so + callers can decide whether to handle or propagate it. Pass + `ignore_errors=True` to suppress the print + raise and return None instead + (useful for existence checks like 'ACL exists?'). + """ cmd = ["az", "rest", "--method", method, "--url", url] + if resource: + cmd += ["--resource", resource] if body: # Use temp file to avoid PowerShell/shell quoting issues tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) @@ -71,6 +79,8 @@ def az_rest(method, url, body=None): result = subprocess.run(cmd, capture_output=True, text=True, check=True) return json.loads(result.stdout) if result.stdout.strip() else {} except subprocess.CalledProcessError as e: + if ignore_errors: + return None error_msg = e.stderr[:300] if e.stderr else "unknown error" print(f" ❌ az rest {method} failed: {error_msg}") raise @@ -79,46 +89,105 @@ def az_rest(method, url, body=None): os.unlink(tmp.name) +def ensure_user_acl(conn_arm_base, conn_location): + """Idempotently ensure the signed-in user has an ACL on the connection. + Required to call the metadata URL with the API Hub audience. + """ + user_oid = subprocess.run( + ["az", "ad", "signed-in-user", "show", "--query", "id", "-o", "tsv"], + capture_output=True, text=True, check=True).stdout.strip() + tenant_id = subprocess.run( + ["az", "account", "show", "--query", "tenantId", "-o", "tsv"], + capture_output=True, text=True, check=True).stdout.strip() + acl_url = f"{conn_arm_base}/accessPolicies/{user_oid}?{API_VERSION}" + # GET first — only create if missing + if az_rest("GET", acl_url, ignore_errors=True) is None: + print(f" Creating user-ACL on connection for objectId {user_oid}...") + az_rest("PUT", acl_url, body={ + "location": conn_location, + "properties": { + "principal": { + "type": "ActiveDirectory", + "identity": {"objectId": user_oid, "tenantId": tenant_id}, + } + }, + }) + else: + print(f" User-ACL already exists for objectId {user_oid}.") + + # ========================================================================= -# Step 1: Discover Trigger Operations +# Step 1: Discover Trigger Operations (via connector Swagger from metadata URL) # ========================================================================= print("\n" + "=" * 60) print(f"Step 1: Discover Trigger Operations for {connector}") print("=" * 60) -# Use the classic locations API to discover operations -# First get gateway location +# Get the gateway info — used later for ACL location + gateway MI identity gw_info = az_rest("GET", f"{ARM_BASE}?{API_VERSION}") location = gw_info.get("location", "westcentralus") -ops_url = f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Web/locations/{location}/managedApis/{connector}/apiOperations?api-version=2016-06-01" -operations = az_rest("GET", ops_url) - -raw_ops = operations.get("value", []) if isinstance(operations, dict) else operations -trigger_ops = [op for op in raw_ops if op.get("properties", {}).get("trigger")] +# Get the connection's runtime URL (requires connection to exist) +conn_info = az_rest("GET", f"{ARM_BASE}/connections/{connection_name}?{API_VERSION}") +runtime_url = conn_info.get("properties", {}).get("connectionRuntimeUrl") +if not runtime_url: + print(" ❌ Connection has no connectionRuntimeUrl yet. Complete OAuth consent first.") + sys.exit(1) +conn_location = conn_info.get("location", location) + +# Ensure the signed-in user has an ACL on the connection (idempotent — GET first, PUT only if missing). +# The metadata URL requires connection-level auth. +ensure_user_acl(f"{ARM_BASE}/connections/{connection_name}", conn_location) + +# Derive the metadata URL: replace /apim/ with /metadata/ +metadata_url = runtime_url.replace("/apim/", "/metadata/") + "?export=true" + +# Call metadata URL with the API Hub audience +swagger = az_rest("GET", metadata_url, resource="https://apihub.azure.com") + +# Filter paths for operations with x-ms-trigger +trigger_ops = [] +for path, methods in swagger.get("paths", {}).items(): + for method, details in methods.items(): + if not isinstance(details, dict): + continue + trigger = details.get("x-ms-trigger") + if not trigger: + continue + op_id = details.get("operationId") + if not op_id: + # Skip operations without an operationId — can't address them downstream + continue + # x-ms-trigger is typically an object like {"type": "polling"} or a string + trigger_type_str = trigger.get("type") if isinstance(trigger, dict) else str(trigger) + trigger_ops.append({ + "name": op_id, + "path": path, + "method": method.upper(), + "summary": details.get("summary", ""), + "trigger": trigger_type_str or "unknown", + }) if not trigger_ops: print(" ❌ No trigger operations found for this connector.") - print(f" Verify connector name '{connector}' is correct and available in location '{location}'.") + print(f" Verify connector name '{connector}' is correct.") sys.exit(1) print(f" {len(trigger_ops)} trigger operations available:") for i, op in enumerate(trigger_ops, 1): - props = op.get("properties", {}) - trigger_type = props.get("trigger", "") - print(f" {i}. {op['name']}: {props.get('summary', '')} [{trigger_type}]") + print(f" {i}. {op['name']}: {op['summary']} [{op['trigger']}]") # Auto-select first trigger for demo purposes (interactive scripts should prompt user) selected_op = trigger_ops[0] -selected_name = selected_op.get("name", selected_op.get("operationId")) -trigger_type = selected_op.get("properties", {}).get("trigger", "") +selected_name = selected_op["name"] +trigger_type = selected_op["trigger"] print(f"\n Selected: {selected_name}") # Polling/recurrence trigger detection: -# If the trigger operation does NOT have x-ms-notification AND does NOT have -# x-ms-notification-content in its Swagger definition, it is a recurrence trigger. -# Only in that case, inform the user about the default polling interval. -if trigger_type in ("batch", "Batch", "single", "Single"): +# Metadata Swagger commonly yields x-ms-trigger.type values like "polling" or "webhook" +# (or "batch"/"single" in older swaggers). Polling triggers fire on a schedule and +# accept a recurrence parameter; inform the user about the default interval. +if trigger_type.lower() in ("polling", "batch", "single"): # NOTE: To determine if this is a recurrence trigger, check the Swagger for # the operation. If it lacks both x-ms-notification and x-ms-notification-content, # it's a recurrence/polling trigger regardless of single/batch. diff --git a/plugin/skills/connectors/SKILL.md b/plugin/skills/connectors/SKILL.md index 31d10f9..ba1c69e 100644 --- a/plugin/skills/connectors/SKILL.md +++ b/plugin/skills/connectors/SKILL.md @@ -3,22 +3,20 @@ name: azure-connectornamespace description: | Azure Connector Namespace — manage namespaces, connections, triggers, and MCP server configs. Connect external SaaS services (Office 365, Teams, SharePoint, OneDrive, Forms, GitHub, - Azure Blob, ...) to any user-provided webhook URL via event-driven triggers, expose + Azure Blob) to any user-provided webhook URL via event-driven triggers, expose selected connector operations as an MCP server endpoint, or call connector operations - on demand via `dynamicInvoke`. + on demand via dynamicInvoke. Use when: - - Creating or managing connector namespaces and connections - - Creating or managing trigger configs that POST to an arbitrary callback URL - - Subscribing to connector events (email, file, list-item, form response, Teams message) - - Wiring event sources to a customer-owned webhook, Function App, Logic App, or API + - Managing connector namespaces and connections + - Creating trigger configs that POST to a callback URL + - Subscribing to connector events (email, file, list-item, form response) + - Wiring event sources to a webhook, Function App, Logic App, or API - Recurrence / sliding-window triggers that fire on a schedule - - Exposing connector operations as Model Context Protocol (MCP) tools at a namespace endpoint - - Calling connector APIs (send email, post Teams message, upload files, list items, ...) - Triggers: "connector namespace", "connector", "create trigger", "trigger config", "webhook trigger", - "recurrence trigger", "schedule trigger", "on new email", "on new file", - "on new item", "on form response", "callback url", "notification url", - "mcp", "mcp server", "mcp tools", "model context protocol", - "send email", "post teams message", "upload to onedrive", "automate" + - Exposing connector operations as MCP tools at a namespace endpoint + - Calling connector APIs (send email, post Teams message, upload files) + Triggers: "connector namespace", "connector", "trigger config", "webhook trigger", + "recurrence trigger", "on new email", "callback url", + "mcp", "send email", "post teams message" --- # Azure Connector Namespace (generic)