From f1521d34cb2c7d21d091e38d5edbe35d1694722b Mon Sep 17 00:00:00 2001 From: Esteban Chavez Date: Wed, 2 Sep 2026 13:49:22 -0700 Subject: [PATCH 1/6] feat: teach agents to author allowedActions for shared connections PRs #1004 and #1077 in bic/PowerPlatform-Managed-Host added a connector runtime policy contract that the plugin knew nothing about. For a shared connection reference (sharedConnectionId set), ms.config.json must declare allowedActions -- per table for tabular sources, connector-level for action connectors -- and both `ms app pack` and `ms app deploy` hard-fail without it. sharedConnectionId is written automatically by the CLI whenever the connector and auth type are shareable, so agents hit this failure without ever opting in. Adds shared/allowed-actions.md as the single source of truth and wires it into the two moments that matter: immediately after a data source is added, and as a blocking preflight gate in /deploy. Because all eleven /add-* skills delegate to add-data-source, one step there covers every connector. - shared/allowed-actions.md: new canonical reference -- why the field exists (RP translates it into an executionRestrictions policy and APIHub runs every connector request through it; the managed apps host only addresses the policy, the app SDK is uninvolved, and app code never changes), how to detect a shared reference, both authoring shapes, least-privilege inference, the verbatim validation errors, and the deferral path when no app code exists yet - add-data-source: new Step 4 reads the config back after every add and runs infer -> propose -> confirm -> write; defers rather than guessing during /create-app, which adds data sources before any UI exists - deploy: new Step 4 preflight gate catches a missing policy before it can reach a failing deploy - list-connectors: documents the real list-actions output columns and that the id values feed connector-level allowedActions, filtered to behavior: Allow - schemas/ms.config.schema.json: adds allowedActions at both nodes, copied from the product repo's regenerated JsonSchema snapshot. The published schema was stale and closed (additionalProperties: false), so a correctly authored config drew a red squiggle in VS Code - version 2.0.2 -> 2.1.0 (new required agent behavior, not a fix) Stage overlays deliberately cannot carry allowedActions -- StageConnectionReferenceSchema is .strict() and does not whitelist it -- so stage-overlay.schema.json is intentionally unchanged. The deploy gate is a node -e script rather than jq: jq is not on PATH on Windows and the plugin treats PowerShell as a first-class host, whereas Node 22+ is already a hard prerequisite for every Microsoft App. Verified against fixtures covering missing per-table, whitespace-only, missing connector-level, clean, and no-reference configs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .claude-plugin/marketplace.json | 2 +- .../.claude-plugin/plugin.json | 2 +- plugins/microsoft-managed-apps/AGENTS.md | 1 + .../agents/microsoft-apps-architect.md | 23 ++ .../shared/allowed-actions.md | 308 ++++++++++++++++++ .../shared/connector-reference.md | 22 ++ .../shared/memory-bank.md | 17 +- .../shared/shared-instructions.md | 38 +++ .../skills/add-data-source/SKILL.md | 75 ++++- .../skills/create-app/SKILL.md | 1 + .../skills/deploy/SKILL.md | 53 ++- .../skills/list-connectors/SKILL.md | 32 +- schemas/ms.config.schema.json | 12 + 13 files changed, 569 insertions(+), 17 deletions(-) create mode 100644 plugins/microsoft-managed-apps/shared/allowed-actions.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5e2eb80..c892f38 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "microsoft-managed-apps", "source": "./plugins/microsoft-managed-apps", "description": "[Preview] Create and manage Microsoft Managed Apps using the @microsoft/managed-apps-cli (ms) toolchain.", - "version": "2.0.2" + "version": "2.1.0" } ] } diff --git a/plugins/microsoft-managed-apps/.claude-plugin/plugin.json b/plugins/microsoft-managed-apps/.claude-plugin/plugin.json index dec4388..30c30da 100644 --- a/plugins/microsoft-managed-apps/.claude-plugin/plugin.json +++ b/plugins/microsoft-managed-apps/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "microsoft-managed-apps", - "version": "2.0.2", + "version": "2.1.0", "description": "[Preview] Create and manage Microsoft Managed Apps using the @microsoft/managed-apps-cli (ms) toolchain.", "author": { "name": "Microsoft", diff --git a/plugins/microsoft-managed-apps/AGENTS.md b/plugins/microsoft-managed-apps/AGENTS.md index 29ad78a..1a4a151 100644 --- a/plugins/microsoft-managed-apps/AGENTS.md +++ b/plugins/microsoft-managed-apps/AGENTS.md @@ -27,6 +27,7 @@ This plugin uses a memory bank (`memory-bank.md`) to persist state across sessio | `shared/planning-policy.md` | When to enter plan mode and what to include in the plan. | | `shared/memory-bank.md` | Memory bank schema + read/update protocol. | | `shared/development-standards.md` | Versioning, theme, CLI install pattern, build rules, TypeScript strict mode. | +| `shared/allowed-actions.md` | `allowedActions` authoring for shared connection references — required before pack/deploy. | | `shared/version-check.md` | Daily plugin-version check against the marketplace. | ### Adding new shared instructions diff --git a/plugins/microsoft-managed-apps/agents/microsoft-apps-architect.md b/plugins/microsoft-managed-apps/agents/microsoft-apps-architect.md index 63b4232..c222a18 100644 --- a/plugins/microsoft-managed-apps/agents/microsoft-apps-architect.md +++ b/plugins/microsoft-managed-apps/agents/microsoft-apps-architect.md @@ -21,6 +21,7 @@ You are a Microsoft Apps Architect with deep expertise in building web apps on t - **Microsoft Apps platform**: How `ms app create` provisions app metadata + a remote git repository, how `ms app dev` runs a two-server local stack (dev + config) against the App Player, and how `ms app deploy` gets the app into the cloud. - **Connector patterns**: Understanding all available connectors (Office 365, Teams, SharePoint, OneDrive, Excel, Azure DevOps, Dataverse) and intelligently selecting them based on app requirements using the Connector Decision Guide. - **Connector Decision Guide** ([shared/connector-decision-guide.md](../shared/connector-decision-guide.md)): You must reference this guide when recommending connectors. Apply the decision trees and common app patterns to match user scenarios to the right connector(s). +- **Shared connection policies** ([shared/allowed-actions.md](../shared/allowed-actions.md)): When a connection reference is shared (`sharedConnectionId` set in `ms.config.json`), the app must declare `allowedActions` or the deploy fails validation. Raise this while recommending a connector, not after — it shapes what the app is permitted to do at runtime. ## Your Role @@ -72,6 +73,28 @@ ms --version # Bin name has flipped between dev builds **See** [Connector Decision Guide](../shared/connector-decision-guide.md) for decision trees, common app patterns, and scenario examples. +### Shared Connections Need an Action Policy + +The CLI records a `sharedConnectionId` on a connection reference automatically whenever the +connector's authentication type is shareable. Such a reference **must** declare +`allowedActions` in `ms.config.json`, or `ms app pack` / `ms app deploy` fails validation. + +- **Tabular** references (those with `dataSets[*].dataSources[*]`) declare per-table actions + from a fixed vocabulary: `"get"`, `"post"`, `"patch"`, `"delete"`. Every table needs one. +- **Action** connectors declare connector-level Action IDs from + `ms connector list-actions --connector --json`. + +Architecturally this is a least-privilege boundary, not a formality: RP translates the +declaration into an `executionRestrictions` runtime policy, and APIHub runs every connector +request through that policy — anything not declared is refused. When you design a data model +against a shared connection, design the action set with it — a reference that reads one table +and writes another should say so. + +It is authoring-only. It is never read at runtime and requires no app code, no SDK upgrade, and +no client-side check. + +**See** [allowed-actions.md](../shared/allowed-actions.md). + ### Generated Code Pattern `ms app add data-source` (with `--as table` or `--as action`) writes generated TypeScript to the `generated/` directory at the project root. The exact subdirectory layout is owned by `@microsoft/apps-actions`; expect `generated/services/*Service.ts` and `generated/models/*Model.ts` files. Import them from your `src/` code using relative paths like `../../generated/services/`. Always use these generated services for data access. diff --git a/plugins/microsoft-managed-apps/shared/allowed-actions.md b/plugins/microsoft-managed-apps/shared/allowed-actions.md new file mode 100644 index 0000000..4a6dc69 --- /dev/null +++ b/plugins/microsoft-managed-apps/shared/allowed-actions.md @@ -0,0 +1,308 @@ +# Allowed Actions (shared connection runtime policies) + +Applies to every `/add-*` skill and to `/deploy`. + +When a connection reference in `ms.config.json` is **shared**, the app must declare which +connector actions it is allowed to invoke. This is not optional: `ms app pack` and +`ms app deploy` both validate it and **fail the deploy** when it's missing. + +--- + +## Why this exists + +A shared connection is exactly that — shared. The same connection can back several apps, and +it carries whatever permissions the person who created it has. Left unrestricted, any app +holding the reference could perform any operation that connection allows: an app that only +needs to read a list could delete from it. + +**Runtime policies close that gap.** A shared connection is limited to the operations the app +declares in its config, and that limit is enforced **per app** by the Connectors infrastructure. +Two apps sharing one connection get two independent policies — a read-only dashboard stays +read-only even if the connection itself could write, and neither app's policy affects the other. + +That's what `allowedActions` is: the app's declaration of the operations it needs. Anything +not on the list is refused. + +``` +ms.config.json ──► RP at deploy time ──► executionRestrictions policy + allowedActions (translates it) on AppConnectionReference + │ + ▼ + host addresses the policy on each request + │ + ▼ + APIHub runs the request through the + policy and allows or refuses it +``` + +Enforcement lives in the **Connectors infrastructure**, not in the app and not in the managed +apps host. APIHub runs every connector request through the runtime policy and refuses anything +the policy doesn't permit. The host's part is only addressing: it rewrites the outgoing +connector URL to point at the policy rather than the connection, and stamps an +`x-ms-shared-connection-id` header. + +So **`allowedActions` is never read at runtime and app code never changes.** Apps keep +addressing connections by id — no SDK upgrade, no rebuild, no import, no client-side +allow-list check. + +Do not write runtime code that reads, checks, or reacts to `allowedActions`. There is nothing +to hook. Declaring the list in config *is* the implementation. + +--- + +## When is it required? + +Only for **shared** connection references. A reference is shared when its +`sharedConnectionId` is present and non-empty in `ms.config.json`: + +```jsonc +"connectionReferences": { + "shared_office365": { + "id": "/providers/Microsoft.PowerApps/apis/shared_office365", + "displayName": "Office 365 Outlook", + "sharedConnectionId": "abc123...", // ← shared, so allowedActions is required + }, +} +``` + +**The user never asks for this.** The CLI writes `sharedConnectionId` automatically whenever +the connector's authentication type is shareable, so an ordinary +`ms app add data-source` can produce a shared reference without anyone opting in. Always read +the config back after an add rather than assuming. + +Non-shared references (no `sharedConnectionId`, or `null`, or empty) may omit `allowedActions` +entirely. Don't add it to them. + +### Detecting shared references + +Node 22+ is already a prerequisite for every Microsoft App, so this check runs identically in +bash and PowerShell (unlike `jq`, which isn't on PATH by default on Windows): + +```bash +node -e ' +const fs = require("fs"); +const refs = JSON.parse(fs.readFileSync("ms.config.json", "utf8")).connectionReferences || {}; +const ok = (a) => Array.isArray(a) && a.length > 0 && a.every((x) => String(x).trim()); +let bad = 0; +for (const [name, r] of Object.entries(refs)) { + if (!String(r.sharedConnectionId || "").trim()) continue; + const t = Object.entries(r.dataSets || {}).flatMap(([d, s]) => + Object.entries(s.dataSources || {}).map(([k, v]) => [d + "/" + k, v])); + if (t.length) { + for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } + } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } +} +console.log(bad ? bad + " issue(s): fix before deploy" : "OK: all shared references declare allowedActions"); +' +``` + +This mirrors the CLI's own validation, so a clean result here means `ms app pack` / +`ms app deploy` will pass this check. The output also tells you which shape to author: +`per-table` means the reference has dataset tables, `connector-level` means it doesn't. + +Non-shared references, and `sharedConnectionId` values that are `null` or whitespace, are +skipped — matching the CLI. + +--- + +## The two authoring shapes + +Which shape applies depends on whether the reference has **dataset tables** +(`dataSets[*].dataSources[*]`), not on what kind of connector it nominally is. + +### Tabular reference — per-table actions + +If the reference has dataset tables, **every table** must declare its own non-empty +`allowedActions`. Connector-level `allowedActions` does not satisfy the requirement for them. + +The allowed values are a **fixed vocabulary of four HTTP-shaped verbs**: + +| Value | Covers | +| ---------- | -------------------------------------- | +| `"get"` | reading / listing rows | +| `"post"` | creating rows | +| `"patch"` | updating rows | +| `"delete"` | deleting rows | + +```jsonc +{ + "connectionReferences": { + "shared_sql": { + "id": "/providers/Microsoft.PowerApps/apis/shared_sql", + "displayName": "SQL Server", + "sharedConnectionId": "...", + "dataSets": { + "default": { + "dataSources": { + "Orders": { + "tableName": "Orders", + "allowedActions": ["get", "patch"], + }, + "Customers": { + "tableName": "Customers", + "allowedActions": ["get"], + }, + }, + }, + }, + }, + }, +} +``` + +> Do **not** run `ms connector list-actions` for a table and paste operation IDs like +> `GetItems` or `PatchItem` into a per-table list. Tables take the four verbs above, nothing +> else. + +### Action connector — connector-level actions + +If the reference has no dataset tables, connector-level `allowedActions` is required. The +values are **Action IDs** exposed by the connector: + +```bash +ms connector list-actions --connector --json +``` + +Take the `id` field, and only from entries whose `behavior` is `Allow` — a `Deny` action is +already blocked by org DLP policy, so listing it grants nothing and misleads a later reader. + +```jsonc +{ + "connectionReferences": { + "shared_office365": { + "id": "/providers/Microsoft.PowerApps/apis/shared_office365", + "displayName": "Office 365 Outlook", + "sharedConnectionId": "...", + "allowedActions": ["SendEmailV2", "GetEmailsV3"], + }, + }, +} +``` + +### Both — a tabular connector's non-table actions + +Connector-level `allowedActions` is still meaningful on a tabular reference: it covers +actions that aren't scoped to a table (for example `ExecuteProcedure` on SQL). Add it +alongside the per-table declarations when the app calls such an action. It does not replace +them. + +--- + +## Choosing the values: least privilege + +The point of the policy is to narrow what the shared connection can do on this app's behalf. +Declaring every verb, or every action the connector exposes, passes validation and grants +exactly nothing — don't do it. + +Work out what the app actually calls, propose it, and let the user confirm. + +**1. Find the generated service for the data source.** Codegen lands in `generated/` at the +project root, e.g. `generated/services/Office365OutlookService.ts`. + +**2. Grep `src/` for calls into it.** Per +[connector-reference.md](./connector-reference.md), never read the whole generated file: + +```bash +# Which service methods does the app actually call? +grep -rhoE '\b\.[A-Za-z0-9_]+' src/ | sort -u +``` + +**3. Map the calls to values.** + +For a **table**, map by what the method does: + +| Method shape | Verb | +| ------------------------------------------------- | ---------- | +| `Get*`, `List*`, anything that reads or queries | `"get"` | +| `Create*`, `Post*`, `Insert*` | `"post"` | +| `Update*`, `Patch*` | `"patch"` | +| `Delete*`, `Remove*` | `"delete"` | + +For an **action connector**, the generated method name corresponds to the connector's Action +ID. Confirm the exact casing against `ms connector list-actions --connector --json` +rather than guessing from the TypeScript name — use the `id` from that output verbatim. + +**4. Propose and confirm.** Show the user what you inferred and what it will permit, per +reference and per table: + +> "`shared_sql` is a shared connection, so it needs an action policy before deploy. From the +> code, the app reads and updates `Orders`, and only reads `Customers`. I'd declare: +> `Orders: ["get", "patch"]`, `Customers: ["get"]`. Anything else it should be allowed to do?" + +Wait for confirmation. If the app doesn't call the data source at all yet, say so and ask what +to grant rather than inventing a set — an unused binding still has to declare something +non-empty to pass validation. + +**5. Write it into `ms.config.json`** and record the decision in `memory-bank.md` so the next +session doesn't re-litigate it. + +### Deferring when there's no app code yet + +Inference needs code to inspect. When a shared reference is created before the app is written +— the normal case during `/create-app`, which adds data sources before generating the UI — +don't guess a set and don't prompt for one. + +Leave `allowedActions` unset, record the reference as **shared, policy pending** in +`memory-bank.md`, and decide once the app code exists. This is safe because: + +- `ms app dev` does **not** validate — only `ms app pack` and `ms app deploy` do + (`assertAppConfigSchemaValid` is called from the pack/deploy path, not the dev path). Local + iteration is completely unaffected. +- `/deploy` runs the same check as a preflight gate, so a pending policy surfaces before it can + reach a failing deploy. + +A deferred policy is a tracked to-do, not a silent gap. An *undeferred* guess is worse than +both: it either over-grants or breaks the app at runtime. + +--- + +## Validation rules + +The CLI enforces these before build/upload, on both `ms app pack` and `ms app deploy`: + +| Condition | Rule | +| ------------------------------------------------------------ | ------------------------------------------------------------- | +| Shared reference **with** dataset tables | **every** table must declare non-empty `allowedActions` | +| Shared reference **without** dataset tables | connector-level `allowedActions` required, non-empty | +| Any `allowedActions` that is present | must have ≥1 entry, and no entry may be blank/whitespace | +| Non-shared reference | may omit `allowedActions` entirely | + +Failure surfaces as: + +``` +Invalid ms.config.json for shared connection policy enforcement: + +``` + +with one or more of: + +| Issue | Fix | +| --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `allowedActions must contain at least one non-empty action identifier.` | The array is empty or has a blank entry. Populate it, or remove it if the reference isn't shared. | +| `Shared connection references with dataset tables must declare allowedActions for every table.` | At least one table under `dataSets[*].dataSources[*]` is missing it. The message's path names the dataset and data source. | +| `Shared connection references without dataset tables must declare connector-level allowedActions.` | Add connector-level `allowedActions` with Action IDs from `ms connector list-actions`. | + +Never work around a failure by deleting `sharedConnectionId` — that field reflects how the +connection was actually created, and removing it misrepresents the binding. + +--- + +## Gotchas + +- **Adding a table later invalidates the config.** A new table on an already-shared tabular + reference must declare its own `allowedActions`. The next deploy fails until it does, so + handle it as part of the add rather than at deploy time. +- **Stage overlays cannot carry `allowedActions`.** A deployment overlay + (`ms..config.json`) whitelists only `sharedConnectionId`, `dataSets`, + `xrmConnectionReferenceLogicalName`, and `dataSourcesShareLinks`, and every object is + closed — an `allowedActions` key there is a schema error, not a silent override. Author it + in the base `ms.config.json`. If an overlay makes a reference shared for one stage, the base + config still has to satisfy the policy. +- **There is no CLI command for this yet.** `allowedActions` is hand-edited into + `ms.config.json` today. A flag or command is a possible fast-follow; if a future CLI version + exposes one, prefer it over hand-editing. +- **Editor squiggles.** `ms.config.json` is validated against the published + [`ms.config.schema.json`](https://raw.githubusercontent.com/microsoft/Managed-Apps/main/schemas/ms.config.schema.json). + If the project pins an older `$schema` copy that predates this field, the editor may flag it + even though the CLI accepts it — point the user at the current URL rather than dropping the + field. diff --git a/plugins/microsoft-managed-apps/shared/connector-reference.md b/plugins/microsoft-managed-apps/shared/connector-reference.md index 3a663bb..91c7484 100644 --- a/plugins/microsoft-managed-apps/shared/connector-reference.md +++ b/plugins/microsoft-managed-apps/shared/connector-reference.md @@ -39,6 +39,24 @@ explicitly. If no connection exists yet, **run the command once interactively** So interactively you do NOT need to pre-create connections or pass `--connection-id`; you only need it to bypass the picker or to script a non-interactive run. +### Shared connections require an action policy + +When the connection's authentication type is shareable, the CLI **automatically** records a +`sharedConnectionId` on the connection reference it writes to `ms.config.json`. Nobody opts +into this — an ordinary `ms app add data-source` can produce a shared reference. + +A shared reference must declare `allowedActions`, or `ms app pack` / `ms app deploy` fails +validation. So after every add, read `ms.config.json` back and check the reference you just +created: + +- `sharedConnectionId` empty / absent → nothing to do. +- `sharedConnectionId` set → declare the actions before moving on. + +Per-table `allowedActions` (`"get"` / `"post"` / `"patch"` / `"delete"`) for tabular +references, connector-level Action IDs for action connectors. Full rules, the least-privilege +inference procedure, and failure recovery live in +[allowed-actions.md](./allowed-actions.md). + ### Dataverse is different The tabular Dataverse connector (`--connector dataverse`) doesn't use the connection-id model — @@ -79,3 +97,7 @@ When a connector skill is invoked from another skill (e.g., `/create-app` plans ## Build After, Don't Deploy Every `/add-*` skill runs `npm run build` after the `ms app add ...` call to catch type errors in the generated services. **None of them push or deploy.** Deployment happens only via `/deploy`, which always requires explicit user confirmation. + +The one thing every `/add-*` skill must do *before* that build is the shared-connection check +above — a missing `allowedActions` doesn't surface as a build error, it surfaces much later as +a deploy failure. diff --git a/plugins/microsoft-managed-apps/shared/memory-bank.md b/plugins/microsoft-managed-apps/shared/memory-bank.md index 7283939..eb09fd2 100644 --- a/plugins/microsoft-managed-apps/shared/memory-bank.md +++ b/plugins/microsoft-managed-apps/shared/memory-bank.md @@ -136,9 +136,20 @@ Update after: ### Data Sources -| Source | Type | Details | -| ------------ | --------- | ---------------------------- | -| [TABLE_NAME] | Dataverse | Columns: name, status, ... | +| Source | Type | Details | Shared? | allowedActions | +| ------------ | --------- | ---------------------------- | ------- | ------------------------- | +| [TABLE_NAME] | Dataverse | Columns: name, status, ... | No | n/a | +| [TABLE_NAME] | SQL Server | Table: Orders | Yes | `get`, `patch` | +| [CONNECTOR] | Office 365 Outlook | (entire connector) | Yes | `SendEmailV2` | +| [CONNECTOR] | Teams | (entire connector) | Yes | **policy pending** | + +Record `allowedActions` for every **shared** reference (one with a non-empty +`sharedConnectionId`) so the agreed least-privilege set survives across sessions and isn't +re-litigated on the next add. Non-shared references record `n/a`. + +Use **policy pending** when the reference is shared but the actions haven't been decided yet +(typically added during `/create-app`, before app code exists). `/deploy` gates on these, so a +pending entry is a to-do, not a silent gap. ### Generated Files diff --git a/plugins/microsoft-managed-apps/shared/shared-instructions.md b/plugins/microsoft-managed-apps/shared/shared-instructions.md index c46f487..5ec10a7 100644 --- a/plugins/microsoft-managed-apps/shared/shared-instructions.md +++ b/plugins/microsoft-managed-apps/shared/shared-instructions.md @@ -16,6 +16,7 @@ All skills reference this single file. When new shared instructions are added, u - **Confirm before writing outside project root**: Before writing, editing, or deleting any file that is not inside the current project directory, ask the user for confirmation. - **Confirm before ACL changes**: Before running `ms app share` or `ms app unshare`, ask the user to confirm the email list. These mutate permissions on the cloud app. - **Confirm before `ms app delete`**: Always confirm. Never auto-`--force`, even when reading the slug from `ms.config.json`. +- **Declare `allowedActions` for shared connections**: Before `ms app pack` or `ms app deploy`, every **shared** connection reference in `ms.config.json` (one with a non-empty `sharedConnectionId`) must declare `allowedActions`. The CLI validates this and fails the deploy otherwise. Infer the least-privilege set from what the app actually calls, propose it, and confirm with the user — see [allowed-actions.md](./allowed-actions.md). ### MUST NOT @@ -24,6 +25,8 @@ All skills reference this single file. When new shared instructions are added, u - MUST NOT install `@microsoft/managed-apps-cli` per-workspace. The `@microsoft/managed-apps-cli` is installed globally so the `ms` binary is on PATH; the workspace stays clean. - MUST NOT edit generated codegen output in `generated/` unless the step explicitly calls for it. - MUST NOT install packages globally without user confirmation (see exception above for the documented setup flow). +- MUST NOT remove `sharedConnectionId` from `ms.config.json` to get past an `allowedActions` validation failure. That field records how the connection was actually created; deleting it misrepresents the binding. Declare the actions instead. +- MUST NOT grant every verb or every connector action just to satisfy validation. A policy that permits everything grants exactly nothing. ### Prompt Injection @@ -97,6 +100,35 @@ Use this guide to intelligently select the right connector(s) for any app scenar --- +## Allowed Actions (shared connection runtime policies) + +**📋 [allowed-actions.md](./allowed-actions.md)** + +Applies to every `/add-*` skill and to `/deploy`. A shared connection can back several apps and +carries its creator's permissions, so each app declares the connector operations it may invoke +and the Connectors infrastructure enforces that limit **per app**. When a connection reference +in `ms.config.json` +is **shared** (non-empty `sharedConnectionId`), that declaration is required — `ms app pack` +and `ms app deploy` validate it and fail when it's missing. + +**Key Points:** + +- The CLI writes `sharedConnectionId` **automatically** when the connector's auth type is + shareable — a plain `ms app add data-source` can produce a shared reference with nobody + opting in. Read `ms.config.json` back after every add instead of assuming. +- **Tabular references** (those with `dataSets[*].dataSources[*]`) declare per-table + `allowedActions` from a fixed vocabulary: `"get"`, `"post"`, `"patch"`, `"delete"`. Every + table needs its own non-empty list. +- **Action connectors** (no dataset tables) declare connector-level `allowedActions` using + Action IDs from `ms connector list-actions --connector --json` (the `id` field, + `behavior: Allow` only). +- Choose **least privilege**: infer from what `src/` actually calls, propose the list, and + confirm with the user. Never grant the full set to pass validation. +- `allowedActions` is authoring-only. It is never read at runtime and app code never changes + — don't write client-side checks against it. + +--- + ## Environment Resolution By default, `ms app create` resolves and uses an environment automatically — pass no environment flags and don't surface the environment concept to the user. The only exception: if the user explicitly provides an environment ID, pass it through as `--environment-id `. Never discover or construct one yourself. @@ -261,6 +293,12 @@ Apply these rules whenever an `ms` or `npm` command exits non-zero. Do NOT retry | `connectionId not found` | Ask the user to discover the right connection (`ms connector list-actions --connector `) and retry. | | `api-id` not recognized | Run `ms connector list --search ` to confirm the api-id spelling, then retry. | +### `ms app pack` / `ms app deploy` policy validation failures + +| Condition | Action | +| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `Invalid ms.config.json for shared connection policy enforcement:` | One or more shared connection references are missing `allowedActions`. The listed issues name the reference (and dataset/table) at fault. Declare the actions per [allowed-actions.md](./allowed-actions.md), then retry. Do not delete `sharedConnectionId` to bypass it. | + --- ## Ready-to-Ship Gate (preview before deploy) diff --git a/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md b/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md index 0c1d038..5862503 100644 --- a/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md @@ -10,6 +10,8 @@ model: sonnet **Reference:** [connector-reference.md](${CLAUDE_PLUGIN_ROOT}/shared/connector-reference.md) — Inline connection creation, Grep-first for large generated files. +**Reference:** [allowed-actions.md](${CLAUDE_PLUGIN_ROOT}/shared/allowed-actions.md) — Shared connections must declare `allowedActions` before deploy (Step 4). + # Add Data Source (Canonical) This is the **single implementation** for all connector-binding skills. @@ -34,8 +36,9 @@ This ensures developers have complete context for implementing features, not jus 1. Verify workspace + auth. 2. Resolve `api-id` (discover via `ms connector list` if the user didn't supply one), `mode`, and required arguments. 3. Run the matching `ms app add ...` command. -4. Run `npm run build`. -5. Record the binding in `memory-bank.md` if present. +4. Check for a shared connection and declare `allowedActions` if so. +5. Run `npm run build`. +6. Record the binding in `memory-bank.md` if present. --- @@ -147,15 +150,77 @@ available connections and then errors. Dataverse (`--connector dataverse --as ta > **SQL stored procedures** have no `ms app add procedure` command and `--sql-stored-procedure` is not > accepted by `ms app add data-source`; binding a specific stored procedure is not currently supported. -### Step 4: Build +### Step 4: Shared Connection Policy (`allowedActions`) + +**Do this before the build, on every add.** The CLI writes `sharedConnectionId` automatically +when the connection's auth type is shareable, so this can trigger without the user asking for +it — and it doesn't surface as a build error, it surfaces later as a deploy failure. + +**4a. Read the config back** and check the reference that was just written. Node 22+ is a +project prerequisite, so this works in bash and PowerShell alike: + +```bash +node -e ' +const fs = require("fs"); +const refs = JSON.parse(fs.readFileSync("ms.config.json", "utf8")).connectionReferences || {}; +const ok = (a) => Array.isArray(a) && a.length > 0 && a.every((x) => String(x).trim()); +let bad = 0; +for (const [name, r] of Object.entries(refs)) { + if (!String(r.sharedConnectionId || "").trim()) continue; + const t = Object.entries(r.dataSets || {}).flatMap(([d, s]) => + Object.entries(s.dataSources || {}).map(([k, v]) => [d + "/" + k, v])); + if (t.length) { + for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } + } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } +} +console.log(bad ? bad + " issue(s): fix before deploy" : "OK: all shared references declare allowedActions"); +' +``` + +If it prints `OK`, **skip to Step 5** — the connection isn't shared, or is already covered. + +**4b. If it is shared, determine the shape:** + +| Reference has… | Declare | +| ----------------------------------- | ----------------------------------------------------------------------- | +| dataset tables (`dataSets`) | per-table `allowedActions` — every table, from `"get"` / `"post"` / `"patch"` / `"delete"` | +| no dataset tables | connector-level `allowedActions` — Action IDs from `ms connector list-actions --connector --json` (`id`, `behavior: Allow` only) | + +**4c. Infer least privilege, then confirm.** Grep `src/` for calls into the generated service +and map them to values (read/list → `get`, create → `post`, update → `patch`, delete → +`delete`). Present the inferred list and ask the user to confirm or adjust: + +> "`` is a shared connection, so it needs an action policy before deploy. From the +> code, I'd declare ``. Anything else it should be allowed to do?" + +**Never grant the full set just to pass validation.** + +**Deferral — when there is no app code yet.** If `src/` has no calls into this service because +the app hasn't been written yet (the usual case when `/create-app` invokes this skill before +generating the UI), do **not** guess and do **not** prompt. Instead: + +- Leave `allowedActions` unset for now. `ms app dev` does not validate it, so local iteration + is unaffected — only `ms app pack` / `ms app deploy` do. +- Record the reference as **shared, policy pending** in `memory-bank.md`. +- Note it in your summary so the decision happens once the app code exists. + +`/deploy` re-runs this same check as a preflight gate, so a deferred policy is caught before it +can reach a failing deploy — not silently forgotten. + +**4d. Write the confirmed values into `ms.config.json`** (skip when deferring). + +Full rules, worked examples, and failure recovery: [allowed-actions.md](${CLAUDE_PLUGIN_ROOT}/shared/allowed-actions.md). + +### Step 5: Build ```bash npm run build ``` -### Step 5: Memory Update +### Step 6: Memory Update -If `memory-bank.md` exists, record `api-id`, mode, and parameters used. +If `memory-bank.md` exists, record `api-id`, mode, parameters used, and — when the reference is +shared — the `allowedActions` that were agreed, so the next session doesn't re-litigate them. --- diff --git a/plugins/microsoft-managed-apps/skills/create-app/SKILL.md b/plugins/microsoft-managed-apps/skills/create-app/SKILL.md index eeb9426..4cf8fe8 100644 --- a/plugins/microsoft-managed-apps/skills/create-app/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/create-app/SKILL.md @@ -190,6 +190,7 @@ Run them sequentially. After each one: - `/add-workiq` → See "Work IQ Integration: MCP Session Pattern" for session management and response parsing - Other `/add-*` skills have similar guidance - Capture the connection ID + service path so Step 9 can import them. +- **If the sub-skill reports a shared connection** (`sharedConnectionId` in `ms.config.json`), it will defer the `allowedActions` policy rather than prompting — there's no app code to infer from yet. Record it as *shared, policy pending* and carry it into Step 11's summary. This does not block `ms app dev`; `/deploy` gates on it later. See [allowed-actions.md](${CLAUDE_PLUGIN_ROOT}/shared/allowed-actions.md). **Forward all captured context to each sub-skill so its own gather-info prompts are suppressed.** The per-service skills (`/add-dataverse`, `/add-sharepoint`, etc.) and `/add-data-source` each have their own prompt sequences (pick connection, pick table/list/site, choose api-id, etc.). The plan you got the user to approve in Step 4 already contains those answers, so pass them through as `$ARGUMENTS` (or whatever invocation surface is available) when dispatching: api-id, connection ID or name, table/list/site identifiers, environment URL, and the project root. If a sub-skill still needs an input you didn't capture, that's a Step 4 gap — go back and ask the user once, then update the plan, rather than letting the sub-skill ask interactively. diff --git a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md index 8a3c5b9..d194f06 100644 --- a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md @@ -16,7 +16,7 @@ Before any deploy, sync source control first: stage all changes, create a commit ## Workflow -1. Memory Bank → 2. Verify Project + Env → 3. Pick Path → 4. Build Verification → 5. Git Sync (Add + Commit + Push) → 6. Confirm + Deploy → 7. Update Memory Bank +1. Memory Bank → 2. Verify Project + Env → 3. Pick Path → 4. Shared Connection Policy Preflight → 5. Build Verification → 6. Git Sync (Add + Commit + Push) → 7. Confirm + Deploy → 8. Update Memory Bank --- @@ -46,7 +46,47 @@ Ask the user which path: `ms app build -c ` + `ms app build-status -o ` is a third (older) two-step variant for the cloud-built path. The `build` subcommand is marked deprecated in the CLI source but still works; use it only when the user wants an explicit build-id to track. Otherwise, `ms app deploy` is the single-step replacement. -### Step 4: Build Verification +### Step 4: Shared Connection Policy Preflight + +`ms app deploy` validates that every **shared** connection reference declares `allowedActions`, +and fails the deploy when one doesn't. Catch it here — before spending a build and a push on a +deploy that can't succeed. + +```bash +node -e ' +const fs = require("fs"); +const refs = JSON.parse(fs.readFileSync("ms.config.json", "utf8")).connectionReferences || {}; +const ok = (a) => Array.isArray(a) && a.length > 0 && a.every((x) => String(x).trim()); +let bad = 0; +for (const [name, r] of Object.entries(refs)) { + if (!String(r.sharedConnectionId || "").trim()) continue; + const t = Object.entries(r.dataSets || {}).flatMap(([d, s]) => + Object.entries(s.dataSources || {}).map(([k, v]) => [d + "/" + k, v])); + if (t.length) { + for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } + } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } +} +console.log(bad ? bad + " issue(s): fix before deploy" : "OK: all shared references declare allowedActions"); +' +``` + +This mirrors the CLI's own validation and works in bash and PowerShell alike (Node 22+ is a +project prerequisite; `jq` is not). + +Any `MISSING` line means the deploy will fail. **Stop and fix it** rather than proceeding: + +- `per-table` → the reference has dataset tables. Every table needs its own non-empty list + from `"get"` / `"post"` / `"patch"` / `"delete"`. +- `connector-level` → no dataset tables. Declare Action IDs from + `ms connector list-actions --connector --json`. + +Infer the least-privilege set from what `src/` actually calls, propose it, and confirm with the +user before writing. Do **not** grant everything to get past the gate, and do **not** delete +`sharedConnectionId`. See [allowed-actions.md](${CLAUDE_PLUGIN_ROOT}/shared/allowed-actions.md). + +`OK` means every shared reference is covered — continue. + +### Step 5: Build Verification ```bash npm run build @@ -61,7 +101,7 @@ Failure handling per [shared-instructions.md](${CLAUDE_PLUGIN_ROOT}/shared/share Verify the build output directory (`./dist` by default, or whatever `--build-path` resolved to during `ms app create`) is populated before continuing. -### Step 5: Git Sync (Add + Commit + Push) +### Step 6: Git Sync (Add + Commit + Push) Deploy must use a pushed commit. Always sync local changes first. @@ -84,7 +124,7 @@ chore: prepare deploy If `git commit` reports "nothing to commit", continue to push/verify the current `HEAD`. -### Step 6: Confirm + Deploy +### Step 7: Confirm + Deploy **Always ask explicitly before deploy** — there is no baseline-deploy exemption in this plugin: @@ -121,7 +161,7 @@ If the user wants to verify the code before making it live, remind them they can $BIN app play --mode preview # opens the latest code on main (no deploy needed) ``` -### Step 7: Update Memory Bank +### Step 8: Update Memory Bank If `memory-bank.md` exists: @@ -139,9 +179,10 @@ If no memory bank exists, create one per [memory-bank.md](${CLAUDE_PLUGIN_ROOT}/ | Error | Fix | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | | `ms app deploy`: 401 / token expired | `$BIN auth login` and retry. | +| `ms app deploy`: `Invalid ms.config.json for shared connection policy enforcement` | A shared connection reference is missing `allowedActions`. The listed issues name the reference (and dataset/table) at fault. Declare them per [allowed-actions.md](${CLAUDE_PLUGIN_ROOT}/shared/allowed-actions.md) and retry. Never remove `sharedConnectionId` to bypass this. | | `ms app deploy`: env mismatch | Active CLI env doesn't match `ms.config.json`. Either re-create/redeploy against the matching environment, or update `ms.config.json`. | | `git push`: no upstream configured | Run `git push -u origin HEAD`, then retry deploy. | | `git commit`: author identity unknown | Configure Git identity (`git config --global user.name` / `git config --global user.email`) and retry commit. | -| `ms app deploy`: commit not in remote | Ensure Step 5 completed and push succeeded, then retry. | +| `ms app deploy`: commit not in remote | Ensure Step 6 (Git Sync) completed and push succeeded, then retry. | | `ms app build-status`: 404 | Build was submitted to a different cluster than you're querying. Verify the `--cloud` value matches the cluster used at build time. | | `ms app deploy`: 403 / not authorized on environment | The account lacks Maker permissions in the target env. Confirm with `$BIN app list --json` — if the app doesn't appear, you're not authorized. | diff --git a/plugins/microsoft-managed-apps/skills/list-connectors/SKILL.md b/plugins/microsoft-managed-apps/skills/list-connectors/SKILL.md index 26610e8..f4e53d7 100644 --- a/plugins/microsoft-managed-apps/skills/list-connectors/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/list-connectors/SKILL.md @@ -85,7 +85,37 @@ $BIN connector list-actions --connector shared_office365 --search Mail $BIN connector list-actions --connector shared_sharepointonline --search GetItems ``` -Output is a list of operation names and their summaries. Use it to confirm an api-id supports the user's intent before invoking the relevant `/add-*` skill. +Output is a table with three columns: + +| Column | Meaning | +| ----------- | ----------------------------------------------------------------------- | +| `Summary` | Human-readable description (prefixed `[Deprecated]` where applicable). | +| `Behavior` | `Allow` or `Deny` — whether org DLP policy permits the operation. | +| `Action ID` | The operation identifier. | + +Allowed actions sort first. Use this to confirm an api-id supports the user's intent before +invoking the relevant `/add-*` skill. + +For scripting, `--json` returns objects with `id`, `summary`, `behavior`, and `deprecated`: + +```bash +$BIN connector list-actions --connector --json +``` + +**These `id` values are exactly what goes into connector-level `allowedActions`** in +`ms.config.json` when a connection reference is shared. Use only entries whose `behavior` is +`Allow` — a `Deny` action is already blocked by policy, so listing it grants nothing and +misleads whoever reads the config next. + +Read the JSON output directly, or filter it if `jq` is available: + +```bash +$BIN connector list-actions --connector --json | jq -r '.[] | select(.behavior == "Allow") | .id' +``` + +> This applies to **action** connectors only. Tabular data sources take a fixed vocabulary of +> `"get"` / `"post"` / `"patch"` / `"delete"` per table — never operation IDs from this +> command. See [allowed-actions.md](${CLAUDE_PLUGIN_ROOT}/shared/allowed-actions.md). --- diff --git a/schemas/ms.config.schema.json b/schemas/ms.config.schema.json index aba8b32..7007d8d 100644 --- a/schemas/ms.config.schema.json +++ b/schemas/ms.config.schema.json @@ -106,6 +106,12 @@ "displayName": { "type": "string" }, + "allowedActions": { + "type": "array", + "items": { + "type": "string" + } + }, "dataSources": { "type": "array", "items": { @@ -151,6 +157,12 @@ "tableName": { "type": "string" }, + "allowedActions": { + "type": "array", + "items": { + "type": "string" + } + }, "tableNameOverride": { "type": "object", "properties": { From 5cbad24e9aa06b339b894d961dd6301e1f6ed7d4 Mon Sep 17 00:00:00 2001 From: Esteban Chavez Date: Mon, 14 Sep 2026 11:10:53 -0700 Subject: [PATCH 2/6] Fix logging for allowedActions validation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/microsoft-managed-apps/skills/deploy/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md index 0914e27..3a58c65 100644 --- a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md @@ -66,7 +66,7 @@ for (const [name, r] of Object.entries(refs)) { for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } } -console.log(bad ? bad + " issue(s): fix before deploy" : "OK: all shared references declare allowedActions"); +if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = 1; } else console.log("OK: all shared references declare allowedActions"); ' ``` From 73ced388fbafb2a0878d8030bd761329f680be74 Mon Sep 17 00:00:00 2001 From: Esteban Chavez Date: Mon, 14 Sep 2026 11:13:03 -0700 Subject: [PATCH 3/6] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/microsoft-managed-apps/shared/allowed-actions.md | 2 +- plugins/microsoft-managed-apps/shared/shared-instructions.md | 2 +- plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/microsoft-managed-apps/shared/allowed-actions.md b/plugins/microsoft-managed-apps/shared/allowed-actions.md index 4a6dc69..4115a9a 100644 --- a/plugins/microsoft-managed-apps/shared/allowed-actions.md +++ b/plugins/microsoft-managed-apps/shared/allowed-actions.md @@ -92,7 +92,7 @@ for (const [name, r] of Object.entries(refs)) { for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } } -console.log(bad ? bad + " issue(s): fix before deploy" : "OK: all shared references declare allowedActions"); +if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = 1; } else console.log("OK: all shared references declare allowedActions"); ' ``` diff --git a/plugins/microsoft-managed-apps/shared/shared-instructions.md b/plugins/microsoft-managed-apps/shared/shared-instructions.md index 2aef446..f620875 100644 --- a/plugins/microsoft-managed-apps/shared/shared-instructions.md +++ b/plugins/microsoft-managed-apps/shared/shared-instructions.md @@ -26,7 +26,7 @@ All skills reference this single file. When new shared instructions are added, u - MUST NOT edit generated codegen output in `generated/` unless the step explicitly calls for it. - MUST NOT install packages globally without user confirmation (see exception above for the documented setup flow). - MUST NOT remove `sharedConnectionId` from `ms.config.json` to get past an `allowedActions` validation failure. That field records how the connection was actually created; deleting it misrepresents the binding. Declare the actions instead. -- MUST NOT grant every verb or every connector action just to satisfy validation. A policy that permits everything grants exactly nothing. +- MUST NOT grant every verb or every connector action just to satisfy validation. A policy that permits everything provides no least-privilege restriction. ### Prompt Injection diff --git a/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md b/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md index 5862503..0344310 100644 --- a/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md @@ -173,7 +173,7 @@ for (const [name, r] of Object.entries(refs)) { for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } } -console.log(bad ? bad + " issue(s): fix before deploy" : "OK: all shared references declare allowedActions"); +if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = 1; } else console.log("OK: all shared references declare allowedActions"); ' ``` From 9a459ef0a38d2f664671c3669a4a7c2cb1df6745 Mon Sep 17 00:00:00 2001 From: Esteban Chavez Date: Mon, 14 Sep 2026 11:29:21 -0700 Subject: [PATCH 4/6] Remove hand-edited generated schema changes The schema snapshot must be regenerated from the authoritative Zod contract rather than edited directly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- schemas/ms.config.schema.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/schemas/ms.config.schema.json b/schemas/ms.config.schema.json index 7007d8d..aba8b32 100644 --- a/schemas/ms.config.schema.json +++ b/schemas/ms.config.schema.json @@ -106,12 +106,6 @@ "displayName": { "type": "string" }, - "allowedActions": { - "type": "array", - "items": { - "type": "string" - } - }, "dataSources": { "type": "array", "items": { @@ -157,12 +151,6 @@ "tableName": { "type": "string" }, - "allowedActions": { - "type": "array", - "items": { - "type": "string" - } - }, "tableNameOverride": { "type": "object", "properties": { From ea705337a09be55c74a87ed8eeabea1f34b90c63 Mon Sep 17 00:00:00 2001 From: Esteban Chavez Date: Mon, 14 Sep 2026 11:34:07 -0700 Subject: [PATCH 5/6] Regenerate managed app config schema Generated from the authoritative Zod contract updated in bic/PowerPlatform-Managed-Host#1288 at 8e6f0591fa0fa6a20814b7dd38d643474d3ba3cd. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- schemas/ms.config.schema.json | 60 ++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/schemas/ms.config.schema.json b/schemas/ms.config.schema.json index aba8b32..57ffba6 100644 --- a/schemas/ms.config.schema.json +++ b/schemas/ms.config.schema.json @@ -45,7 +45,22 @@ "buildEntryPoint": { "type": "string" }, + "data": { + "description": "Schema source file configuration. If db.schemaPath is also configured, both path values must match exactly without normalization.", + "type": "object", + "properties": { + "schemaPath": { + "type": "string" + } + }, + "required": [ + "schemaPath" + ], + "additionalProperties": false + }, "db": { + "deprecated": true, + "description": "Legacy alias for data. If data.schemaPath is also configured, both path values must match exactly without normalization.", "type": "object", "properties": { "schemaPath": { @@ -162,6 +177,14 @@ } }, "additionalProperties": false + }, + "allowedActions": { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "pattern": "\\S" + } } }, "additionalProperties": false @@ -218,35 +241,6 @@ "additionalProperties": false } }, - "workflowDetails": { - "type": "object", - "properties": { - "workflowEntityId": { - "type": "string" - }, - "workflowDisplayName": { - "type": "string" - }, - "workflowName": { - "type": "string" - }, - "dependencies": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "workflowEntityId", - "workflowDisplayName", - "workflowName" - ], - "additionalProperties": false - }, "isOnPremiseConnection": { "type": "boolean" }, @@ -277,6 +271,14 @@ ], "additionalProperties": false } + }, + "allowedActions": { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "pattern": "\\S" + } } }, "required": [ From cb4b8c424090e101471ff4a8857f560f5b0f9880 Mon Sep 17 00:00:00 2001 From: Esteban Chavez Date: Tue, 15 Sep 2026 11:19:44 -0700 Subject: [PATCH 6/6] Validate all allowedActions values Require nonblank string entries and validate optional connector-level policies independently across the three documented preflight checks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../shared/allowed-actions.md | 13 ++++++++++--- .../skills/add-data-source/SKILL.md | 9 +++++++-- .../microsoft-managed-apps/skills/deploy/SKILL.md | 11 ++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/plugins/microsoft-managed-apps/shared/allowed-actions.md b/plugins/microsoft-managed-apps/shared/allowed-actions.md index 4115a9a..dd94b71 100644 --- a/plugins/microsoft-managed-apps/shared/allowed-actions.md +++ b/plugins/microsoft-managed-apps/shared/allowed-actions.md @@ -82,22 +82,29 @@ bash and PowerShell (unlike `jq`, which isn't on PATH by default on Windows): node -e ' const fs = require("fs"); const refs = JSON.parse(fs.readFileSync("ms.config.json", "utf8")).connectionReferences || {}; -const ok = (a) => Array.isArray(a) && a.length > 0 && a.every((x) => String(x).trim()); +const ok = (a) => Array.isArray(a) && a.length > 0 && + a.every((x) => typeof x === "string" && /\S/.test(x)); let bad = 0; for (const [name, r] of Object.entries(refs)) { if (!String(r.sharedConnectionId || "").trim()) continue; const t = Object.entries(r.dataSets || {}).flatMap(([d, s]) => Object.entries(s.dataSources || {}).map(([k, v]) => [d + "/" + k, v])); + if (r.allowedActions !== undefined && !ok(r.allowedActions)) { + bad++; + console.log("INVALID connector-level allowedActions: " + name); + } if (t.length) { for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } - } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } + } else if (r.allowedActions === undefined) { bad++; console.log("MISSING connector-level allowedActions: " + name); } } if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = 1; } else console.log("OK: all shared references declare allowedActions"); ' ``` This mirrors the CLI's own validation, so a clean result here means `ms app pack` / -`ms app deploy` will pass this check. The output also tells you which shape to author: +`ms app deploy` will pass this check. `MISSING` identifies a required declaration that is +absent; `INVALID` identifies a present connector-level declaration that is empty or contains +a non-string or blank entry. The output also tells you which shape to author: `per-table` means the reference has dataset tables, `connector-level` means it doesn't. Non-shared references, and `sharedConnectionId` values that are `null` or whitespace, are diff --git a/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md b/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md index 0344310..1e52abf 100644 --- a/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/add-data-source/SKILL.md @@ -163,15 +163,20 @@ project prerequisite, so this works in bash and PowerShell alike: node -e ' const fs = require("fs"); const refs = JSON.parse(fs.readFileSync("ms.config.json", "utf8")).connectionReferences || {}; -const ok = (a) => Array.isArray(a) && a.length > 0 && a.every((x) => String(x).trim()); +const ok = (a) => Array.isArray(a) && a.length > 0 && + a.every((x) => typeof x === "string" && /\S/.test(x)); let bad = 0; for (const [name, r] of Object.entries(refs)) { if (!String(r.sharedConnectionId || "").trim()) continue; const t = Object.entries(r.dataSets || {}).flatMap(([d, s]) => Object.entries(s.dataSources || {}).map(([k, v]) => [d + "/" + k, v])); + if (r.allowedActions !== undefined && !ok(r.allowedActions)) { + bad++; + console.log("INVALID connector-level allowedActions: " + name); + } if (t.length) { for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } - } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } + } else if (r.allowedActions === undefined) { bad++; console.log("MISSING connector-level allowedActions: " + name); } } if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = 1; } else console.log("OK: all shared references declare allowedActions"); ' diff --git a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md index 3a58c65..8147595 100644 --- a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md @@ -56,15 +56,20 @@ deploy that can't succeed. node -e ' const fs = require("fs"); const refs = JSON.parse(fs.readFileSync("ms.config.json", "utf8")).connectionReferences || {}; -const ok = (a) => Array.isArray(a) && a.length > 0 && a.every((x) => String(x).trim()); +const ok = (a) => Array.isArray(a) && a.length > 0 && + a.every((x) => typeof x === "string" && /\S/.test(x)); let bad = 0; for (const [name, r] of Object.entries(refs)) { if (!String(r.sharedConnectionId || "").trim()) continue; const t = Object.entries(r.dataSets || {}).flatMap(([d, s]) => Object.entries(s.dataSources || {}).map(([k, v]) => [d + "/" + k, v])); + if (r.allowedActions !== undefined && !ok(r.allowedActions)) { + bad++; + console.log("INVALID connector-level allowedActions: " + name); + } if (t.length) { for (const [p, v] of t) if (!ok(v.allowedActions)) { bad++; console.log("MISSING per-table allowedActions: " + name + " -> " + p); } - } else if (!ok(r.allowedActions)) { bad++; console.log("MISSING connector-level allowedActions: " + name); } + } else if (r.allowedActions === undefined) { bad++; console.log("MISSING connector-level allowedActions: " + name); } } if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = 1; } else console.log("OK: all shared references declare allowedActions"); ' @@ -73,7 +78,7 @@ if (bad) { console.log(bad + " issue(s): fix before deploy"); process.exitCode = This mirrors the CLI's own validation and works in bash and PowerShell alike (Node 22+ is a project prerequisite; `jq` is not). -Any `MISSING` line means the deploy will fail. **Stop and fix it** rather than proceeding: +Any `MISSING` or `INVALID` line means the deploy will fail. **Stop and fix it** rather than proceeding: - `per-table` → the reference has dataset tables. Every table needs its own non-empty list from `"get"` / `"post"` / `"patch"` / `"delete"`.