feat(claude-in-office): Outlook manifest support
build-manifest.mjs now takes a host argument (office | outlook). The outlook host fetches manifest-outlook-3p.xml, which uses Microsoft's MailApp schema and cannot share a file with the TaskPaneApp manifest Excel/Word/PowerPoint use. - URL_SLOTS regexes are now /g so all three Outlook taskpane URL entries (FormSettings + V1_0 + V1_1 Resources) receive the config params, not just the first match. - New graph_client_id key for orgs that bring their own Entra app for Microsoft Graph instead of consenting to Anthropic's multi-tenant app. - aws_* keys with the outlook host fail fast: Bedrock is not currently supported for Outlook. - consent.md gains the Outlook Graph admin-consent URL (Mail.ReadWrite, Calendars.Read, People.Read, User.Read, offline_access) and the own-Entra-app alternative. - manifest.md and setup.md document the host arg, the per-host file split, and route admins to the consent step before generating the Outlook manifest.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Azure admin consent URL — one-time tenant approval for Entra SSO
|
||||
description: Azure admin consent URLs — one-time tenant approval for Entra SSO and Outlook Graph access
|
||||
---
|
||||
|
||||
# Azure admin consent
|
||||
@@ -33,4 +33,28 @@ az ad sp show --id c2995f31-11e7-4882-b7a7-ef9def0a0266 --query appId -o tsv
|
||||
```
|
||||
|
||||
If that returns the same GUID, the service principal exists in their tenant —
|
||||
consent worked. If it errors with "does not exist", consent didn't complete.
|
||||
consent worked. If it errors with "does not exist", consent didn't complete.
|
||||
|
||||
## Outlook — Microsoft Graph consent
|
||||
|
||||
**Only needed when deploying the Outlook manifest.** Separate from `entra_sso`
|
||||
above; required even if `entra_sso` is off.
|
||||
|
||||
Claude for Outlook reads mail and calendar through Microsoft Graph. The Graph
|
||||
token stays in the user's Outlook client and is never sent to the gateway or to
|
||||
Anthropic, so this consent is the same regardless of which cloud serves the
|
||||
model. A Global Admin opens the URL, clicks Accept, done.
|
||||
|
||||
```
|
||||
https://login.microsoftonline.com/organizations/v2.0/adminconsent?client_id=c2995f31-11e7-4882-b7a7-ef9def0a0266&scope=https://graph.microsoft.com/Mail.ReadWrite%20https://graph.microsoft.com/Calendars.Read%20https://graph.microsoft.com/People.Read%20https://graph.microsoft.com/User.Read%20offline_access&redirect_uri=https://pivot.claude.ai/auth/callback
|
||||
```
|
||||
|
||||
Without this, every user hits a "Need admin approval" wall the first time
|
||||
Claude tries to read mail.
|
||||
|
||||
**If their policy forbids consenting to a third-party app:** they can register
|
||||
their own single-tenant Entra app with the same delegated Graph permissions
|
||||
(Mail.ReadWrite, Calendars.Read, People.Read, User.Read, offline_access), grant
|
||||
admin consent on it, and pass its client ID as `graph_client_id` when generating
|
||||
the Outlook manifest. Same data flow; approval lives under their app instead of
|
||||
Anthropic's.
|
||||
@@ -4,9 +4,16 @@ description: Generate the add-in manifest XML with your cloud config baked in
|
||||
|
||||
# Generate add-in manifest
|
||||
|
||||
The script fetches the canonical manifest from `pivot.claude.ai/manifest.xml`
|
||||
and appends your config as URL query parameters. The add-in reads them at
|
||||
startup.
|
||||
The script fetches the canonical manifest and appends your config as URL query
|
||||
parameters. The add-in reads them at startup. Outlook uses a separate template
|
||||
because Microsoft's `MailApp` schema is distinct from the `TaskPaneApp` schema
|
||||
Excel/Word/PowerPoint share, so ask which apps they're deploying and generate
|
||||
one file per host.
|
||||
|
||||
| Host arg | Apps | Template |
|
||||
|---|---|---|
|
||||
| `office` | Excel, Word, PowerPoint | `pivot.claude.ai/manifest.xml` |
|
||||
| `outlook` | Outlook (mail + calendar) | `pivot.claude.ai/manifest-outlook-3p.xml` |
|
||||
|
||||
## Keys by cloud
|
||||
|
||||
@@ -20,6 +27,22 @@ Prompt only for the keys their cloud path needs. Don't ask for all eight.
|
||||
| Gateway | `gateway_url` `gateway_token` `gateway_auth_header` `gateway_api_format` |
|
||||
| Gateway (`gateway_api_format=vertex`) | also `gcp_project_id` `gcp_region` |
|
||||
|
||||
Amazon Bedrock is **not currently supported for the `outlook` host**; the script
|
||||
exits with an error if you pass `aws_*` keys with `outlook`.
|
||||
|
||||
## Outlook — Microsoft Graph
|
||||
|
||||
Outlook reads the user's mailbox and calendar via Microsoft Graph, which
|
||||
requires a one-time tenant-wide admin consent regardless of which cloud serves
|
||||
the model. Run [consent](consent.md#outlook--microsoft-graph-consent) before
|
||||
deploying — otherwise every user hits "Need admin approval" on first open.
|
||||
|
||||
If their policy forbids consenting to a third-party app, prompt for
|
||||
`graph_client_id` (their own single-tenant Entra app's client ID with
|
||||
Mail.ReadWrite, Calendars.Read, People.Read, User.Read, offline_access
|
||||
delegated permissions and admin consent granted). Otherwise leave it unset and
|
||||
the add-in uses Anthropic's multi-tenant app.
|
||||
|
||||
## Entra SSO
|
||||
|
||||
`entra_sso=1` makes the add-in acquire an Entra ID token at startup. Set it
|
||||
@@ -149,11 +172,16 @@ template's version as-is.
|
||||
## Run
|
||||
|
||||
```bash
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/build-manifest.mjs" manifest.xml \
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/build-manifest.mjs" office manifest.xml \
|
||||
gcp_project_id=<value> \
|
||||
gcp_region=<value> \
|
||||
auto_connect=0 \
|
||||
...
|
||||
|
||||
# and if they're also deploying Outlook:
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/build-manifest.mjs" outlook manifest-outlook.xml \
|
||||
<same provider keys as above> \
|
||||
graph_client_id=<value> # only if NOT using Anthropic's app via the consent URL
|
||||
```
|
||||
|
||||
The script validates key names (unknown keys fail hard) and shape-hints values
|
||||
@@ -173,3 +201,4 @@ match the symptom below. Edit `manifest.xml` directly, then re-validate.
|
||||
| "An add-in with this ID already exists" | Replace the text inside `<Id>` with a fresh UUID. The template carries the marketplace install's ID. |
|
||||
| Re-upload accepted but nothing changes | M365 caches by ID + version. Edit `<Version>` to a higher fourth segment (e.g. `1.0.0.9` → `1.0.0.10`) and re-validate. |
|
||||
| Only want Excel (not PowerPoint) | Remove `<Host>` elements for `Presentation`. **Two parallel lists:** the top-level `<Hosts>` uses `Name="Presentation"`, the one under `<VersionOverrides>` uses `xsi:type="Presentation"` — both must go or the manifest is inconsistent. The `xsi:type` block is multi-line, delete the whole `<Host xsi:type="Presentation">...</Host>`. |
|
||||
| Only want Excel/PPT, not Outlook | Nothing to remove — Outlook is a separate file. Just don't generate it. |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Setup wizard — provision Vertex/Bedrock/gateway, admin consent, generate manifest
|
||||
description: Setup wizard — provision Vertex/Bedrock/Foundry/gateway, admin consent, generate manifest(s)
|
||||
---
|
||||
|
||||
# Claude in Office — Direct Cloud Setup
|
||||
@@ -47,6 +47,21 @@ Bedrock and per-user config (bootstrap endpoint or extension attrs) need
|
||||
`entra_sso=1` — the add-in acquires the user's Entra ID token to authenticate
|
||||
those flows. See the Entra SSO section in [manifest](manifest.md).
|
||||
|
||||
## Step 1b — Which Office apps?
|
||||
|
||||
Ask: **Excel/Word/PowerPoint, Outlook, or both?** Outlook is a separate
|
||||
manifest and has one extra prerequisite.
|
||||
|
||||
If they're deploying Outlook:
|
||||
- **Bedrock is not currently supported for Outlook.** If they picked `bedrock`
|
||||
in Step 1, Outlook is off the table for now — generate only the `office`
|
||||
manifest.
|
||||
- **Microsoft Graph admin consent is required.** Run
|
||||
[consent](consent.md#outlook--microsoft-graph-consent) — a Global Admin opens
|
||||
one URL and clicks Accept. Do this before generating the manifest so you can
|
||||
ask whether they're using Anthropic's app (no `graph_client_id` needed) or
|
||||
their own Entra app (capture `graph_client_id`).
|
||||
|
||||
Branch to the matching section below.
|
||||
|
||||
---
|
||||
@@ -250,16 +265,18 @@ Write the split into the setup log so Step 4 and Step 5 each know their subset.
|
||||
## Step 4 — Generate the manifest
|
||||
|
||||
Read `${CLAUDE_PLUGIN_ROOT}/commands/manifest.md` and follow it with the
|
||||
**org-wide** values from Step 3. The command wraps:
|
||||
**org-wide** values from Step 3. Generate one file per host from Step 1b:
|
||||
|
||||
```bash
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/build-manifest.mjs" manifest.xml <key>=<value> ...
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/build-manifest.mjs" office manifest.xml <key>=<value> ...
|
||||
node "${CLAUDE_PLUGIN_ROOT}/scripts/build-manifest.mjs" outlook manifest-outlook.xml <key>=<value> ...
|
||||
```
|
||||
|
||||
Then validate:
|
||||
Then validate each:
|
||||
|
||||
```bash
|
||||
npx -y office-addin-manifest validate manifest.xml
|
||||
npx -y office-addin-manifest validate manifest-outlook.xml
|
||||
```
|
||||
|
||||
## Step 5 — Per-user config
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
// Fetches the canonical add-in manifest and writes a customized copy with your
|
||||
// org's config baked into the taskpane URL as query parameters.
|
||||
//
|
||||
// Usage: node build-manifest.mjs <out.xml> key=value [key=value ...]
|
||||
// Example: node build-manifest.mjs acme.xml gcp_project_id=acme gcp_region=us-east5
|
||||
// Usage: node build-manifest.mjs <office|outlook> <out.xml> key=value [key=value ...]
|
||||
// Example: node build-manifest.mjs office acme.xml gcp_project_id=acme gcp_region=us-east5
|
||||
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const MANIFEST_URL = process.env.MANIFEST_URL || "https://pivot.claude.ai/manifest.xml";
|
||||
const MANIFESTS = {
|
||||
office: "https://pivot.claude.ai/manifest.xml", // Excel + Word + PowerPoint (TaskPaneApp)
|
||||
outlook: "https://pivot.claude.ai/manifest-outlook-3p.xml", // Outlook (MailApp — separate schema)
|
||||
};
|
||||
|
||||
// The manifest has two URL slots Office reads from; both must carry the same params.
|
||||
const URL_SLOTS = [/(<SourceLocation\s+DefaultValue=")([^"]+)(")/, /(id="Taskpane\.Url"\s+DefaultValue=")([^"]+)(")/];
|
||||
// Every URL slot Office reads from must carry the same params. Outlook's MailApp
|
||||
// schema repeats Taskpane.Url across V1_0 and V1_1 VersionOverrides, hence /g.
|
||||
const URL_SLOTS = [/(<SourceLocation\s+DefaultValue=")([^"]+)(")/g, /(id="Taskpane\.Url"\s+DefaultValue=")([^"]+)(")/g];
|
||||
|
||||
// Recognized config keys. `pattern` is a shape hint — mismatches warn but don't block
|
||||
// (your infra may look different). `secret` keys warn louder: the manifest is an
|
||||
@@ -34,6 +38,10 @@ const KEYS = {
|
||||
pattern: /^[A-Za-z0-9]{20,}$/,
|
||||
hint: "From Azure Portal → your Foundry resource → Keys and Endpoint → KEY 1",
|
||||
},
|
||||
graph_client_id: {
|
||||
pattern: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
hint: "Entra app (client) ID for Microsoft Graph — Outlook only; omit to use Anthropic's multi-tenant app via the admin consent URL",
|
||||
},
|
||||
gateway_url: { pattern: /^https:\/\//, hint: "HTTPS base URL" },
|
||||
gateway_token: { pattern: /./, hint: "gateway API key", secret: true },
|
||||
gateway_auth_header: { pattern: /^(x-api-key|authorization)$/i, hint: "auth header scheme (default: x-api-key)" },
|
||||
@@ -66,12 +74,20 @@ const KEYS = {
|
||||
const NEEDS_ENTRA = ["aws_role_arn", "graph_client_id", "entra_scope"];
|
||||
|
||||
async function main() {
|
||||
const [out, ...pairs] = process.argv.slice(2);
|
||||
if (!out || pairs.length === 0) {
|
||||
console.error("Usage: node build-manifest.mjs <out.xml> key=value [key=value ...]");
|
||||
const [host, out, ...pairs] = process.argv.slice(2);
|
||||
const manifestUrl = process.env.MANIFEST_URL || MANIFESTS[host];
|
||||
if (!manifestUrl || !out || pairs.length === 0) {
|
||||
console.error("Usage: node build-manifest.mjs <office|outlook> <out.xml> key=value [key=value ...]");
|
||||
console.error(`Keys: ${Object.keys(KEYS).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (host === "outlook" && pairs.some((p) => p.startsWith("aws_"))) {
|
||||
console.error("error: Amazon Bedrock (aws_role_arn/aws_region) is not currently supported for Outlook");
|
||||
process.exit(1);
|
||||
}
|
||||
if (host !== "outlook" && pairs.some((p) => p.startsWith("graph_client_id="))) {
|
||||
console.warn("note: graph_client_id only applies to Outlook; it has no effect in the office manifest");
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
for (const p of pairs) {
|
||||
@@ -102,18 +118,20 @@ async function main() {
|
||||
// URLSearchParams joins with `&`; XML attribute values need it escaped.
|
||||
const qs = params.toString().replaceAll("&", "&");
|
||||
|
||||
const res = await fetch(MANIFEST_URL);
|
||||
if (!res.ok) throw new Error(`fetch ${MANIFEST_URL}: ${res.status} ${res.statusText}`);
|
||||
const res = await fetch(manifestUrl);
|
||||
if (!res.ok) throw new Error(`fetch ${manifestUrl}: ${res.status} ${res.statusText}`);
|
||||
let xml = await res.text();
|
||||
|
||||
for (const slot of URL_SLOTS) {
|
||||
slot.lastIndex = 0;
|
||||
if (!slot.test(xml)) throw new Error(`manifest missing expected URL slot: ${slot.source}`);
|
||||
slot.lastIndex = 0;
|
||||
// The template URL already carries ?m=<tag> — append with & not a second ?
|
||||
xml = xml.replace(slot, (_, pre, url, post) => pre + url + (url.includes("?") ? "&" : "?") + qs + post);
|
||||
}
|
||||
|
||||
writeFileSync(out, xml);
|
||||
console.log(`Wrote ${out} (params: ${params})`);
|
||||
console.log(`Wrote ${out} (${host}, params: ${params})`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
Reference in New Issue
Block a user