diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 80665cb..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.3" + "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 b292360..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.3", + "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 10c8e02..25647be 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` | 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..dd94b71 --- /dev/null +++ b/plugins/microsoft-managed-apps/shared/allowed-actions.md @@ -0,0 +1,315 @@ +# 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) => 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 (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. `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 +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 332b70b..91f4700 100644 --- a/plugins/microsoft-managed-apps/shared/memory-bank.md +++ b/plugins/microsoft-managed-apps/shared/memory-bank.md @@ -134,9 +134,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 058b7bd..f620875 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 provides no least-privilege restriction. ### Prompt Injection @@ -96,6 +99,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. @@ -260,6 +292,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..1e52abf 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,82 @@ 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) => 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 (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"); +' +``` + +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 0696e2f..8848a2c 100644 --- a/plugins/microsoft-managed-apps/skills/create-app/SKILL.md +++ b/plugins/microsoft-managed-apps/skills/create-app/SKILL.md @@ -196,6 +196,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-connector` each have their own prompt sequences (pick connection, pick table/list/site, choose api-id, etc.). The approved plan and discovery results should contain 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 a required input that cannot be discovered or safely inferred, ask the user one focused question and record the answer as an amendment to the approved plan rather than letting multiple sub-skills ask interactively. diff --git a/plugins/microsoft-managed-apps/skills/deploy/SKILL.md b/plugins/microsoft-managed-apps/skills/deploy/SKILL.md index 3924783..8147595 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,52 @@ 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) => 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 (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 and works in bash and PowerShell alike (Node 22+ is a +project prerequisite; `jq` is not). + +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"`. +- `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 +106,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 +129,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 +166,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: @@ -138,9 +183,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..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": [