> ## Documentation Index
> Fetch the complete documentation index at: https://gateway.forceaisecurity.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Microsoft 365 Tool Adapter

> Let Copilot Studio and declarative agents call governed ForceAI capability as a tool, authenticated with a Microsoft Entra delegated token.

This is the **inbound** direction: Microsoft calls ForceAI. A Copilot Studio REST tool or a Microsoft 365 declarative-agent API plugin invokes an approved ForceAI logical agent over `POST /api/v1/agents/{agent_id}/invoke`, authenticating with a delegated Entra token. The adapter fixes the model and policy server-side and forwards to the gateway with a per-tenant key.

<Note>
  Do not confuse this with [Microsoft 365 Copilot (A2A)](/agents/m365-copilot), which is the **outbound** direction, where the gateway calls a Microsoft agent hosted as an A2A server. That page is for reaching into Microsoft; this page is for Microsoft reaching into ForceAI.
</Note>

## What it governs, and what it does not

<Warning>
  The adapter governs the **actions** a Microsoft-orchestrated agent takes when it calls ForceAI, not the whole conversation. A Copilot Studio or declarative agent runs on Microsoft's own foundation model, and only the specific tool-call reaches ForceAI. The chat, the agent's other tools, and every answer Microsoft composes never pass through ForceAI guardrails. To govern **every turn** of a conversation you need a custom engine agent, where ForceAI is the agent's model engine, which is a separate integration.
</Warning>

When a call does arrive through the adapter it is fully governed: the caller cannot choose the model, gateway guardrails and per-tenant budget apply, the data classification is enforced, and every call is audited with a correlation id and a hashed user id.

## How the connection works

```mermaid theme={null}
flowchart LR
  U[Enterprise user] --> M[Copilot Studio / Declarative agent]
  M -->|delegated Entra token| A[ForceAI Tool Adapter]
  A --> R[(Logical-agent registry)]
  A -->|per-tenant virtual key| G[ForceAI Gateway]
  G --> P[Approved provider]
  A -.audit.-> AU[(ForceAI audit + spend)]
```

There is no persistent connection. Microsoft imports the adapter's OpenAPI document as a tool and calls it on demand. The Microsoft access token is verified and stops at the adapter; it is never forwarded to the gateway or a provider.

## Set up

<Steps>
  <Step title="Register an Entra app for the adapter">
    In Microsoft Entra ID create an app registration, single tenant. Under **Expose an API**, set the Application ID URI (`api://<client-id>`) and add a delegated scope named `ForceAI.Invoke` (Admins and users, Enabled). Note the client id and tenant id. The full runbook, including the Copilot Studio OAuth wiring, is in the repo at `docs/forceai/how-to-m365-adapter-entra-setup.md`.
  </Step>

  <Step title="Configure the adapter">
    Set the environment on the control plane and restart it:

    ```bash theme={null}
    FORCEAI_M365_ADAPTER_AUDIENCE=api://<client-id>
    FORCEAI_M365_ADAPTER_REQUIRED_SCOPE=ForceAI.Invoke
    FORCEAI_M365_ADAPTER_ALLOWED_TENANTS=<tenant-guid>
    FORCEAI_M365_ADAPTER_PUBLIC_URL=https://<externally-reachable-host>
    FORCEAI_M365_ADAPTER_TENANT_KEYS=<tenant-guid>=<a restricted ForceAI virtual key>
    ```

    The endpoint returns `503` until `FORCEAI_M365_ADAPTER_AUDIENCE` is set, so an unconfigured adapter never accepts a token by accident.
  </Step>

  <Step title="Mint a per-tenant virtual key">
    Scope it to the model group the logical agents use and give it a budget, so spend attributes to the tenant and a leaked key reaches no further.

    ```bash theme={null}
    curl -X POST https://YOUR_GATEWAY_DOMAIN/key/generate \
      -H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" \
      -d '{"key_alias":"m365-<tenant>","models":["claude-haiku"],"max_budget":50}'
    ```
  </Step>

  <Step title="Create a logical agent">
    The caller selects only this agent id; the server decides everything else.

    ```bash theme={null}
    curl -X POST https://YOUR_GATEWAY_DOMAIN/v1/m365/logical-agents \
      -H "Authorization: Bearer $ADMIN_KEY" -H "Content-Type: application/json" \
      -d '{
        "agent_id": "forceai-security-review",
        "model_alias": "claude-haiku",
        "description": "Security Review",
        "allowed_tenants": ["<tenant-guid>"],
        "allowed_data_classifications": ["public", "internal"],
        "max_output_tokens": 500
      }'
    ```

    The response carries `usable`. If it is `false`, the agent has no tenant or no classification granted and every call would return `404`.
  </Step>

  <Step title="Import into Copilot Studio">
    Fetch the generated Swagger 2.0 document, which is served pre-filled with your host, tenant and scope, and import it as a REST API tool. Configure the OAuth 2.0 connection with the client secret and redirect URI from the app registration.

    ```
    https://<your-adapter-host>/api/v1/m365/openapi/copilot-studio-v2.json
    ```

    For a declarative agent, use the OpenAPI 3.0 document and the plugin manifest instead:

    ```
    https://<your-adapter-host>/api/v1/m365/openapi/declarative-agent-v3.json
    https://<your-adapter-host>/api/v1/m365/openapi/ai-plugin.json
    ```
  </Step>
</Steps>

## The invoke contract

The caller supplies only content and a classification. It never names a model or generation settings; sending any of `model`, `provider`, `temperature`, `max_tokens` or similar is refused with `400`, so an attempt to route around policy is recorded rather than silently dropped.

<CodeGroup>
  ```http Request theme={null}
  POST /api/v1/agents/{agent_id}/invoke
  Authorization: Bearer <Microsoft Entra delegated token>
  Content-Type: application/json

  {
    "message": "Review this architecture for security risks.",
    "task": "security-review",
    "dataClassification": "internal",
    "context": [{ "role": "user", "content": "Previously approved context" }]
  }
  ```

  ```json Response theme={null}
  {
    "requestId": "correlation-guid",
    "title": "ForceAI Security Review response",
    "answer": "The highest-priority risks are...",
    "agentId": "forceai-security-review",
    "modelAlias": "claude-haiku",
    "policyVersion": "2026-07-24.1",
    "providerModel": "provider-approved-name",
    "usage": { "promptTokens": 540, "completionTokens": 260, "totalTokens": 800 },
    "status": "completed"
  }
  ```
</CodeGroup>

`sourceUrl` is present only when a real user-accessible page exists; it is never fabricated to satisfy the response shape.

## Troubleshooting by rejection reason

The adapter names why it rejected a request, and each reason points at a specific fix.

| Reason                      | HTTP | Fix                                                                                                                   |
| --------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------- |
| `wrong_audience`            | 401  | `FORCEAI_M365_ADAPTER_AUDIENCE` does not match the token's `aud`, or the token was minted for a different app         |
| `missing_scope`             | 403  | The delegated `ForceAI.Invoke` scope was not consented; grant admin consent                                           |
| `app_only_token`            | 403  | An application (client-credentials) token was sent; the connection must use delegated permissions and a user sign-in  |
| `tenant_not_allowed`        | 403  | The token's tenant is not in `FORCEAI_M365_ADAPTER_ALLOWED_TENANTS`                                                   |
| `tenant_not_provisioned`    | 403  | Authentication passed but no virtual key is mapped for the tenant; add it to `FORCEAI_M365_ADAPTER_TENANT_KEYS`       |
| `not_found` on a real agent | 404  | The logical agent is disabled, or the tenant or classification is not granted; check its `usable` flag and allowlists |
| `model_selection_refused`   | 400  | The request carried a model or generation field; remove it, the agent determines them                                 |
