{
  "schema": "aaes/capability-pack/1",
  "provider": "dynamics",
  "version": "1.0.0",
  "description": "Microsoft Dynamics 365 / Dataverse: read accounts, contacts, opportunities, and caller identity; update an opportunity’s fields, status, or owner. The general update can reassign ownership and requires the adapter’s If-Match protection against record creation.",
  "notes": "The endpoint host 'dynamics-org.invalid' is a reserved non-production placeholder (an RFC 2606 reserved TLD): substitute the customer's own Dataverse environment host (Microsoft's documentation writes it as [Organization URI], of the form yourorg.crm.dynamics.com or yourorg.api.crm.dynamics.com) before enabling, in every endpoint, in every resource locator and in every egress allowlist entry. The allowlist matches exactly, so endpoints and allowlist must be substituted together, and substitution is a required deployment step — the reserved host being allowlisted does not make an unsubstituted pack safe to enable.\n\nThe Web API version segment v9.2 is pinned as documented. This pack registers only endpoints Microsoft's Dataverse Web API documentation describes: collection reads on the accounts, contacts and opportunities entity sets, the WhoAmI unbound function, and PATCH update of one opportunity record. All reads are collection reads only: single-record reads (accounts(GUID), contacts(GUID), opportunities(GUID)) exist in the API and are deliberately not registered. The opportunity update registers PATCH /opportunities(OPPORTUNITY_ID): Microsoft's update guidance documents that PATCH without If-Match behaves as an upsert that can create a record, so the deployment's adapter must send If-Match: * to keep this capability an update only.\n\nThis registration does not restrict which writable columns a request may carry, and Microsoft's opportunity reference documents that status changes run through the same PATCH (statecode and statuscode, the SetState message) and that ownership reassignment (Assign) is PATCH of ownerid — so this capability can mark an opportunity won or lost (the dated opportunity reference lists statecode 1 Won and 2 Lost and states won and lost opportunities are read-only until reactivated) and can reassign it — the update therefore declares transfer at the minimum R3 tier, because the registered PATCH permits ownership reassignment and no reviewed restriction removes it.\n\nThe WinOpportunity and LoseOpportunity actions — unbound actions accepting an opportunity-close payload (an opportunityclose record bound to the opportunity, plus a status code) — are deliberately not registered, and nothing in this pack creates or deletes any record: record create (POST) and delete (DELETE) are documented in the API and are deliberately not registered. Authentication is OAuth 2.0 client credentials against Microsoft Entra ID: an external deployment process registers an application in the customer's tenant, creates the corresponding application user in the Dataverse environment with a least-privilege custom security role, and obtains access tokens from https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token with grant_type=client_credentials and scope set to the environment URL suffixed with '/.default' (the org URL is the resource).\n\nThe token endpoint host is deliberately not in the egress allowlist: the executor never calls it, because token acquisition and renewal are the external process's job and the injector only injects the stored value verbatim. Dataverse enforces the application user's security roles on every call, so a capability works only if that user is permitted the operation; the registration records the endpoint, not the environment's authorization decision.",
  "credential": {
    "secret_ref_id": "secret-ref-dynamics-dataverse-access-token",
    "custody": "customer_vault",
    "inject": "header",
    "inject_name": "Authorization",
    "note": "An OAuth2 access token for the customer's own Dataverse environment, held in the client's own secret manager. The stored material must be the complete Authorization header value (scheme included): 'Bearer', one separating space, then the access token. An external deployment process obtains the token with the OAuth 2.0 client credentials grant — POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token with the environment URL as the resource (scope '<org-url>/.default') — and renews it before expiry, storing the new complete header value each time.\n\nStoring the app registration's client secret under this reference does not make the HTTP injector perform OAuth: the injector injects the stored value verbatim, so token lifecycle (including secret rotation on the app registration) sits entirely with that external process, and the client secret itself must never be the stored value. customer_vault is the only generally-supported production custody model here — identity_federation and customer_broker are refused at startup and inline is lab-only.\n\nAccess tokens are short-lived (the token response carries expires_in). Distinguish the revocation paths: disabling the application user in the environment or removing its security role stops the environment from honoring further calls; removing the app registration's secret stops future token acquisition — but secret removal does not by itself invalidate already-issued access tokens, which remain usable until they expire (typically about an hour). Plan any emergency revocation around that residual validity window.\n\nAn expired value fails closed.\n\nLeast privilege: the application user's custom security role should grant read on accounts, contacts and opportunities, update on opportunities, and nothing more — no create, delete, append-to or assign privileges beyond what the registered capabilities need.\n\nExecutor hygiene: the value must live only in executor memory for the lifetime of a call, must never be written to logs, traces or diagnostics, and must not appear in crash dumps; a deployment that cannot meet that bar must not enable the pack."
  },
  "capabilities": [
    {
      "capability_id": "dynamics.accounts.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "pii",
      "egress_allowlist": [
        "dynamics-org.invalid"
      ],
      "endpoint": "https://dynamics-org.invalid/api/data/v9.2/accounts",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-dynamics-dataverse-access-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "dynamics-org-accounts",
      "description": "Read the accounts entity set: organization records carrying addresses, phone numbers and primary-contact references, classified as pii (personal data) because account data routinely identifies individual people.\n\nThe registered path covers the collection read only — single-record reads (accounts(GUID)) and the separate navigation-property endpoints (for example accounts(GUID)/primarycontactid) are not registered by this pack. The API permits $expand and other query options on collection requests; this registration places no restriction on query options.\n\nThe host is a reserved placeholder: substitute the customer's environment host before enabling.",
      "requires_review": true
    },
    {
      "capability_id": "dynamics.contacts.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "pii",
      "egress_allowlist": [
        "dynamics-org.invalid"
      ],
      "endpoint": "https://dynamics-org.invalid/api/data/v9.2/contacts",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-dynamics-dataverse-access-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "dynamics-org-contacts",
      "description": "Read the contacts entity set: person records with names, email addresses, phone numbers and job titles, which are personal data.\n\nThe registered path covers the collection read only — single-record reads (contacts(GUID)) and the separate navigation-property endpoints are not registered by this pack. The API permits $expand and other query options on collection requests; this registration places no restriction on query options.\n\nThe host is a reserved placeholder: substitute the customer's environment host before enabling.",
      "requires_review": true
    },
    {
      "capability_id": "dynamics.opportunities.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "internal",
      "egress_allowlist": [
        "dynamics-org.invalid"
      ],
      "endpoint": "https://dynamics-org.invalid/api/data/v9.2/opportunities",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-dynamics-dataverse-access-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "dynamics-org-opportunities",
      "description": "Read the opportunities entity set: sales pipeline records with topics, customers and revenue estimates, classified as internal data — a deployment whose policy treats revenue projections as financial data should raise the class, which is a supported review outcome.\n\nThe registered path covers the collection read only — single-record reads (opportunities(GUID)) are not registered by this pack.\n\nThe host is a reserved placeholder: substitute the customer's environment host before enabling.",
      "requires_review": true
    },
    {
      "capability_id": "dynamics.opportunities.update",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R3",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "internal",
      "egress_allowlist": [
        "dynamics-org.invalid"
      ],
      "endpoint": "https://dynamics-org.invalid/api/data/v9.2/opportunities(OPPORTUNITY_ID)",
      "method": "PATCH",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-dynamics-dataverse-access-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "update",
        "transfer"
      ],
      "resource_id": "dynamics-org-opportunities",
      "description": "Update an opportunity’s properties, status, or owner. The general PATCH permits ownership reassignment, so the capability declares update and transfer at the minimum R3 transfer tier.\n\nRequest limits: the registration does not restrict writable columns. The dated opportunity reference lists statecode 0 Open, 1 Won, and 2 Lost; won and lost records are read-only until reactivated. Status changes use statecode and statuscode (SetState). Changing the writable ownerid performs ownership reassignment (Assign); no reviewed restriction excludes that effect.\n\nSetup: bind OPPORTUNITY_ID to a concrete GUID before registration; this pack uses no call-time endpoint templating. The adapter must send If-Match: * so PATCH cannot create a record through upsert. Substitute the customer’s environment host before enabling.\n\nWinOpportunity and LoseOpportunity are excluded unbound actions. They take an opportunityclose record bound to the opportunity plus a status code.\n\nOn supported AAES-mediated execution paths, the declared irreversible actions require an authorized person’s approval. Observation-only and bypass calls cannot be stopped by this control.",
      "requires_review": true
    },
    {
      "capability_id": "dynamics.whoami.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "internal",
      "egress_allowlist": [
        "dynamics-org.invalid"
      ],
      "endpoint": "https://dynamics-org.invalid/api/data/v9.2/WhoAmI",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-dynamics-dataverse-access-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "dynamics-org-identity",
      "description": "Invoke the WhoAmI unbound function: returns the UserId, BusinessUnitId and OrganizationId of the caller — for this pack, the application user the client-credentials token maps to. It verifies which identity the stored credential resolves to and changes nothing.\n\nThe host is a reserved placeholder: substitute the customer's environment host before enabling.",
      "requires_review": true
    }
  ],
  "resources": [
    {
      "resource_id": "dynamics-org-accounts",
      "kind": "api",
      "locator": "https://dynamics-org.invalid/api/data/v9.2/accounts",
      "data_class": "pii",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    },
    {
      "resource_id": "dynamics-org-contacts",
      "kind": "api",
      "locator": "https://dynamics-org.invalid/api/data/v9.2/contacts",
      "data_class": "pii",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    },
    {
      "resource_id": "dynamics-org-opportunities",
      "kind": "api",
      "locator": "https://dynamics-org.invalid/api/data/v9.2/opportunities",
      "data_class": "internal",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    },
    {
      "resource_id": "dynamics-org-identity",
      "kind": "api",
      "locator": "https://dynamics-org.invalid/api/data/v9.2/WhoAmI",
      "data_class": "internal",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    }
  ],
  "review": {
    "state": "library",
    "date": "2026-09-22",
    "sources": [
      "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/retrieve-entity-using-web-api (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/update-delete-entities-using-web-api (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/use-web-api-functions (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/web-api-basic-operations-sample (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/web-api-service-documents (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/dynamics365/developer/reference/entities/opportunity (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/execute (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/power-apps/developer/data-platform/use-single-tenant-server-server-authentication (retrieved 2026-09-21)",
      "https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow (retrieved 2026-09-21)"
    ],
    "reviewer_id": "aaes-eng"
  }
}
