From af72ee8ec1cc0e2428cf25551f2e93fd9d0877be Mon Sep 17 00:00:00 2001 From: Daddy Date: Sun, 19 Jul 2026 10:22:46 +0800 Subject: [PATCH] Add Microsoft 365 Cowork plugin and DCR harness --- .github/dependabot.yml | 12 + .github/workflows/security.yml | 80 ++++++ .gitignore | 1 + CHANGELOG.md | 19 ++ README.md | 256 +++++++++++++++++- cowork/color.png | Bin 0 -> 8512 bytes cowork/manifest.json | 48 ++++ cowork/mcp-tools.json | 236 ++++++++++++++++ cowork/outline.png | Bin 0 -> 1264 bytes cowork/skills/capability-evolver/SKILL.md | 125 +++++++++ dist/evolver-cowork.zip | Bin 0 -> 14932 bytes package-lock.json | 19 ++ package.json | 21 +- scripts/build-cowork-package.js | 97 +++++++ scripts/check-cowork-remote.mjs | 70 +++++ scripts/security-scan.js | 55 ++++ scripts/validate-cowork-package.js | 185 +++++++++++++ test/cowork-package.test.js | 62 +++++ toolkit/evolver-cowork-dcr/.gitignore | 17 ++ .../.vscode/extensions.json | 5 + .../evolver-cowork-dcr/.vscode/launch.json | 29 ++ toolkit/evolver-cowork-dcr/.vscode/mcp.json | 8 + .../evolver-cowork-dcr/.vscode/settings.json | 12 + toolkit/evolver-cowork-dcr/README.md | 122 +++++++++ .../appPackage/ai-plugin.json | 24 ++ .../evolver-cowork-dcr/appPackage/color.png | Bin 0 -> 5923 bytes .../appPackage/declarativeAgent.json | 19 ++ .../appPackage/instruction.txt | 4 + .../appPackage/manifest.json | 42 +++ .../evolver-cowork-dcr/appPackage/outline.png | Bin 0 -> 492 bytes toolkit/evolver-cowork-dcr/env/.env.example | 9 + toolkit/evolver-cowork-dcr/evals/prompts.json | 13 + toolkit/evolver-cowork-dcr/m365agents.yml | 92 +++++++ 33 files changed, 1666 insertions(+), 16 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/security.yml create mode 100644 cowork/color.png create mode 100644 cowork/manifest.json create mode 100644 cowork/mcp-tools.json create mode 100644 cowork/outline.png create mode 100644 cowork/skills/capability-evolver/SKILL.md create mode 100644 dist/evolver-cowork.zip create mode 100644 package-lock.json create mode 100644 scripts/build-cowork-package.js create mode 100644 scripts/check-cowork-remote.mjs create mode 100644 scripts/security-scan.js create mode 100644 scripts/validate-cowork-package.js create mode 100644 test/cowork-package.test.js create mode 100644 toolkit/evolver-cowork-dcr/.gitignore create mode 100644 toolkit/evolver-cowork-dcr/.vscode/extensions.json create mode 100644 toolkit/evolver-cowork-dcr/.vscode/launch.json create mode 100644 toolkit/evolver-cowork-dcr/.vscode/mcp.json create mode 100644 toolkit/evolver-cowork-dcr/.vscode/settings.json create mode 100644 toolkit/evolver-cowork-dcr/README.md create mode 100644 toolkit/evolver-cowork-dcr/appPackage/ai-plugin.json create mode 100644 toolkit/evolver-cowork-dcr/appPackage/color.png create mode 100644 toolkit/evolver-cowork-dcr/appPackage/declarativeAgent.json create mode 100644 toolkit/evolver-cowork-dcr/appPackage/instruction.txt create mode 100644 toolkit/evolver-cowork-dcr/appPackage/manifest.json create mode 100644 toolkit/evolver-cowork-dcr/appPackage/outline.png create mode 100644 toolkit/evolver-cowork-dcr/env/.env.example create mode 100644 toolkit/evolver-cowork-dcr/evals/prompts.json create mode 100644 toolkit/evolver-cowork-dcr/m365agents.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..04f4c02 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..e0d3f69 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,80 @@ +name: Security + +on: + workflow_dispatch: + push: + branches: + - main + - master + pull_request: + schedule: + - cron: "23 3 * * 1" + +permissions: + contents: read + +concurrency: + group: security-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dependency-scan: + name: Dependency audit and local SAST + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Set up Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: 22 + cache: npm + - name: Install locked dependencies without lifecycle scripts + run: npm ci --ignore-scripts --no-audit + - name: Audit production dependency advisories + run: npm run security:deps + - name: Run repository SAST preflight + run: npm run security:sast + - name: Run tests + run: npm test + - name: Validate Cowork package source + run: npm run validate:cowork + - name: Build and verify Cowork ZIP + run: npm run build:cowork + + dependency-review: + name: Dependency review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Review dependency changes + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high + + codeql: + name: CodeQL SAST + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + actions: read + contents: read + security-events: write + packages: read + steps: + - name: Check out repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3 + with: + languages: javascript-typescript + - name: Analyze + uses: github/codeql-action/analyze@b7351df727350dca84cb9d725d57dcf5bc82ba26 # v3 + with: + category: /language:javascript-typescript diff --git a/.gitignore b/.gitignore index 9208f89..b287c9a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ *.log .env .env.* +!toolkit/evolver-cowork-dcr/env/.env.example .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 654e527..90b2199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to the Evolver GitHub Copilot integration are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## [0.2.0] — 2026-07-18 + +Adds a tenant-deployable Microsoft 365 Copilot Cowork plugin while preserving the +existing VS Code/GitHub Copilot integration. + +### Added + +- Microsoft 365 Unified App manifest v1.28 with a stable application ID. +- Cowork-native `capability-evolver` Agent Skill with explicit redaction, + validation, and untrusted-result handling rules. +- Hosted EvoMap Streamable HTTP MCP connector using OAuth Dynamic Client + Registration and PKCE, with no embedded tenant secret or API key. +- Static MCP tool catalog exposing eight remote-safe GEP tools. +- Deterministic Cowork package validation and build scripts producing + `dist/evolver-cowork.zip`. +- Cowork package tests, live read-only MCP/OAuth compatibility check, npm audit, + local SAST preflight, GitHub Dependency Review, and CodeQL workflow. +- Microsoft 365 admin deployment and first-use verification instructions. + ## [0.1.0] — 2026-07-17 Initial public release. Adapts the Evolver agent-memory workflow for GitHub diff --git a/README.md b/README.md index 3412b23..c4e750b 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@

Evolver — Agent Self-Evolving Engine

-Give GitHub Copilot in VS Code a **persistent, auditable evolution memory** plus a -bridge to the **EvoMap network**. Instead of re-solving the same problem every -session, Copilot is instructed to recall what worked before, reuse proven genes & -capsules from the network when available, and record durable outcomes through the -same Evolver memory format used by the other EvoMap agent integrations. +Give GitHub Copilot in VS Code and Microsoft 365 Copilot Cowork a **persistent, +auditable evolution memory** plus a bridge to the **EvoMap network**. Instead of +re-solving the same problem every session, the agent recalls what worked before, +reuses proven genes and capsules when appropriate, and records validated outcomes +for future work. Powered by the [Genome Evolution Protocol (GEP)](https://evomap.ai) and the [`@evomap/evolver`](https://github.com/EvoMap/evolver) engine. Sibling of the @@ -16,10 +16,9 @@ Powered by the [Genome Evolution Protocol (GEP)](https://evomap.ai) and the and [Evolver Cursor plugin](https://github.com/EvoMap/evolver-cursor-plugin) — same memory format, same clean-room runtime helpers, Copilot-native packaging. -> **Status:** v0.1.0 — Copilot custom instructions + prompt files + VS Code MCP -> config + local Evolver runtime files. Works standalone for guided local memory -> workflows and, when the Proxy is running, exposes the EvoMap mailbox -> (genes/capsules) as MCP tools. +> **Status:** v0.2.0 — includes the original VS Code/GitHub Copilot integration +> and a Microsoft 365 Unified App package for Copilot Cowork. The ready-to-upload +> Cowork artifact is [`dist/evolver-cowork.zip`](dist/evolver-cowork.zip). ## What it does @@ -33,6 +32,7 @@ and VS Code do support: | Prompt files | `.github/prompts/evolver-*.prompt.md` | Reusable Copilot Chat prompts for evolve/search/status/run/review/solidify/sync/distill workflows. | | MCP bridge | `.vscode/mcp.json` + `mcp/evolver-proxy.mjs` | Exposes the local EvoMap Proxy mailbox as MCP tools in VS Code. | | Runtime helpers | `hooks/*.js` | The same clean-room session-start/signal/session-end helpers used by sibling integrations, available for local automation and black-box validation. | +| Cowork app package | `cowork/` + `dist/evolver-cowork.zip` | Adds a Cowork-native Agent Skill and the hosted EvoMap MCP connector using OAuth Dynamic Client Registration. | The MCP bridge (`evolver-proxy`, zero-dependency stdio server) exposes the local EvoMap Proxy mailbox as tools: @@ -46,7 +46,235 @@ EvoMap Proxy mailbox as tools: | `evolver_distill_conversation` | Distill a high-confidence reusable conversation outcome into a local Gene/Capsule and queue it for Hub review. | | `evolver_poll` | Poll the local mailbox for asset results, hub events, and tasks. | -## Install +## Microsoft 365 Copilot Cowork + +The Cowork ZIP is a Microsoft 365 Unified App package (manifest v1.28): + +```text +evolver-cowork.zip +├── manifest.json +├── color.png +├── outline.png +├── mcp-tools.json +└── skills/ + └── capability-evolver/ + └── SKILL.md +``` + +It connects to `https://evomap.ai/mcp`. The package contains no API key, OAuth +secret, tenant ID, or user token. + +> **Current deployment blocker:** Microsoft DCR provisioning reaches EvoMap but +> fails because EvoMap doesn't return the required `client_secret`. Do not deploy +> or claim that the connector works until the DCR check below succeeds. + +### Admin / publisher setup + +#### 1. Verify DCR + +Use Node.js 22 or 24. From the repository root: + +```bash +npm run toolkit:dcr:login +npm run toolkit:dcr:doctor +npm run toolkit:dcr:provision +``` + +The exact flow is: + +1. `toolkit:dcr:login` opens Microsoft 365 sign-in. +2. Sign in with the publisher account. +3. `toolkit:dcr:provision` creates the development app and DCR auth config. +4. Open `toolkit/evolver-cowork-dcr/env/.env.dev`. +5. Confirm `MCP_DA_AUTH_ID_EVOMAPAI` is not empty. +6. Stop if provisioning fails or the ID is empty. + +The DCR harness lives in +[`toolkit/evolver-cowork-dcr/`](toolkit/evolver-cowork-dcr/README.md). Microsoft +documents the CLI commands in +[Microsoft 365 Agents Toolkit CLI](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli) +and the client-secret requirement in +[Configure dynamic client registration](https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/plugin-authentication-dynamic-client-registration). + +Current verified failure: + +```text +InvalidRegistrationResponse: IDP registration response is missing required +field: client_secret. +``` + +EvoMap must return both `client_id` and `client_secret`. Do not add a made-up +`referenceId`. The Evolver runtime skill cannot create this publisher-side auth +configuration. + +##### Required EvoMap server fix + +This is not a value that an administrator gets and fills into the Cowork ZIP or +`env/.env.dev`. EvoMap must fix the server-side registration endpoint: + +```text +POST https://evomap.ai/oauth/register +``` + +For every accepted Microsoft DCR request, the endpoint must generate, persist, +and return a confidential OAuth client. Its response must include at least: + +```json +{ + "client_id": "", + "client_secret": "", + "token_endpoint_auth_method": "client_secret_post", + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "response_types": ["code"] +} +``` + +EvoMap must generate the secret securely, store the client registration +server-side, return the plaintext secret once in the registration response, and +validate it at `POST https://evomap.ai/oauth/token`. The token endpoint must also +support refresh tokens. + +After the server fix, rerun `npm run toolkit:dcr:provision`. Agents Toolkit—not +the administrator—receives the client secret and stores it in Microsoft Enterprise +Token Store. A successful run then writes the separate Microsoft auth-config ID +to `MCP_DA_AUTH_ID_EVOMAPAI`. Never put the client secret itself in +`MCP_DA_AUTH_ID_EVOMAPAI`, `manifest.json`, `SKILL.md`, or source control. + +#### 2. Build the Cowork ZIP + +Run only after DCR succeeds: + +```bash +npm ci --ignore-scripts +npm run check +npm run security:deps +npm run verify:cowork:remote +npm run build:cowork +``` + +Output: [`dist/evolver-cowork.zip`](dist/evolver-cowork.zip). Do not unzip it or +zip the `cowork/` parent directory. + +#### 3. Upload and share an unpublished ZIP + +Use Cowork for a personal or pilot upload: + +1. Open **Microsoft 365 Copilot → Cowork**. +2. In the left navigation, select **Customize**. +3. Select the **Plugins** tab. +4. At the top of the tab, select **Upload plugin**. +5. Choose `dist/evolver-cowork.zip`. +6. In **Share**, select **Only you** or **Specific users in your organization**. +7. If sharing, add users by name or email. +8. Select **Apply**. + +For an update, open the plugin detail page and select **Re-share**. See +[Upload a plugin package](https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/cowork-customize#upload-a-plugin-package) +and +[Share skills and plugins](https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/cowork-customize#share-skills-and-plugins). + +#### 4. Deploy a catalog plugin organization-wide + +Use this only after Evolver is available in the tenant catalog or Microsoft 365 +App Store: + +1. Open the [Microsoft 365 admin center](https://admin.microsoft.com). +2. In the left navigation, select **Agents → Tools**. +3. Search for **Evolver for Cowork**. +4. Select the plugin to open its details. +5. Select **Installed for**. +6. Select **All users** or **Specific users/groups**. +7. Select **Next**, review the details, and install. + +If Evolver isn't listed under **Agents → Tools**, use the Cowork upload-and-share +flow for a pilot. See +[Deploy plugins to your organization](https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/cowork-manage-plugins#deploy-plugins-to-your-organization). + +### Per-user setup and sign-in + +Each user signs in to EvoMap separately. The publisher or administrator cannot +sign in for users. + +1. Open **Microsoft 365 Copilot → Cowork**. +2. In the left navigation, select **Customize**. +3. Select the **Plugins** tab. +4. Select **Evolver for Cowork**. +5. If the detail page shows **Add**, select **Add**. +6. Turn the plugin on for the current conversation. +7. Start a new conversation. +8. In the conversation, use the **Sources** picker to select Evolver if needed. +9. Send this prompt: + + ```text + Set up Evolver. Require me to sign in to EvoMap, then call gep_identity and + gep_status. Do not continue unless both connector calls succeed. Do not + simulate either result. + ``` + +10. Complete the EvoMap sign-in and consent prompt. +11. Confirm the conversation contains real `gep_identity` and `gep_status` tool + events. +12. Stop if either tool is missing or fails. + +Adding or enabling the plugin doesn't sign the user in. Sign-in starts when the +protected connector is first used. Each user's tokens, private EvoMap memory, +permissions, and applicable credits stay with that user's EvoMap account. See +[Manage connector authentication](https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/cowork-manage-plugins#manage-connector-authentication) +and +[Manage your plugins](https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/cowork-customize#manage-your-plugins). + +### After DCR succeeds + +Validate and package the Toolkit harness: + +```bash +npm run toolkit:dcr:validate +npm run toolkit:dcr:package +``` + +Test the personal development agent first. Publish it to the tenant only if that +test succeeds: + +```bash +npm run toolkit:dcr:publish +``` + +The Toolkit package is written to +`toolkit/evolver-cowork-dcr/appPackage/build/`. It is a diagnostic declarative +agent and doesn't replace `dist/evolver-cowork.zip`. + +### Troubleshooting + +- **Plugin missing in Cowork:** check **Customize → Plugins** and start a new + conversation after sharing or assignment. +- **Catalog plugin missing for admins:** check **Agents → Tools**. An unpublished + personal ZIP doesn't automatically appear in the catalog. +- **Tools not registered:** the connector didn't load; don't simulate results. +- **No sign-in prompt:** call `gep_identity`. If the tool isn't available, fix + connector registration first. +- **Authentication failure:** run `npm run verify:cowork:remote`, then rerun + `npm run toolkit:dcr:provision`. +- **Credits:** EvoMap operations can use credits from the signed-in user's EvoMap + account. + +### Automated security checks + +Run both checks locally: + +```bash +npm run security:deps +npm run security:sast +``` + +[Security workflow](.github/workflows/security.yml) runs the dependency audit, +local SAST, tests, Cowork validation/build, pull-request dependency review, and +CodeQL on pushes, pull requests, manual dispatches, and every Monday. Dependabot +checks npm and GitHub Actions updates weekly. + +## VS Code / GitHub Copilot install ### Into a workspace @@ -81,7 +309,7 @@ npm test npm run blackbox ``` -## Requirements +## VS Code requirements - **VS Code** with GitHub Copilot Chat. - **Node.js** ≥ 18 for the installer, runtime helpers, and MCP bridge. @@ -128,8 +356,10 @@ export EVOMAP_NODE_ID="…" ## Architecture -- **This repository's Copilot packaging** is VS Code/Copilot-native: custom - instructions, prompt files, and `.vscode/mcp.json`. +- **The Cowork package** is Microsoft 365-native: a Unified App manifest, Agent + Skill, static MCP tool catalog, and hosted OAuth MCP connector. +- **The VS Code/Copilot packaging** remains editor-native: custom instructions, + prompt files, and `.vscode/mcp.json`. - **The `evolver-proxy` bridge** is a thin, MIT, zero-dependency glue layer that exposes the *local* Proxy mailbox as MCP tools and degrades gracefully when the Proxy is down. diff --git a/cowork/color.png b/cowork/color.png new file mode 100644 index 0000000000000000000000000000000000000000..59ef4016f9f78e824cf6df661791dbc92d5f8843 GIT binary patch literal 8512 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 U_EpX2UuOhBT}4~D3~n9rKcdhEEdT%j literal 0 HcmV?d00001 diff --git a/cowork/manifest.json b/cowork/manifest.json new file mode 100644 index 0000000..86f7987 --- /dev/null +++ b/cowork/manifest.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.28/MicrosoftTeams.schema.json", + "manifestVersion": "1.28", + "version": "0.2.0", + "id": "253cc2c8-e3f8-4f2a-9c9c-3a9d1472531f", + "developer": { + "name": "EvoMap", + "websiteUrl": "https://evomap.ai", + "privacyUrl": "https://evomap.ai/privacy", + "termsOfUseUrl": "https://evomap.ai/terms" + }, + "name": { + "short": "Evolver for Cowork", + "full": "EvoMap Evolver for Microsoft 365 Copilot Cowork" + }, + "description": { + "short": "Recall proven strategies and record auditable outcomes with EvoMap.", + "full": "Connect Microsoft 365 Copilot Cowork to EvoMap's Genome Evolution Protocol. Recall relevant experience, search community strategies, apply evidence-backed approaches, and record validated outcomes so future work can reuse what succeeded." + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "accentColor": "#181818", + "agentSkills": [ + { + "folder": "skills/capability-evolver" + } + ], + "agentConnectors": [ + { + "id": "evomap-gep", + "displayName": "EvoMap GEP", + "description": "Secure access to EvoMap evolution memory and community strategies through the hosted GEP MCP server.", + "toolSource": { + "remoteMcpServer": { + "mcpServerUrl": "https://evomap.ai/mcp", + "mcpToolDescription": { + "file": "mcp-tools.json" + } + } + } + } + ], + "validDomains": [ + "evomap.ai" + ] +} diff --git a/cowork/mcp-tools.json b/cowork/mcp-tools.json new file mode 100644 index 0000000..09c133c --- /dev/null +++ b/cowork/mcp-tools.json @@ -0,0 +1,236 @@ +{ + "tools": [ + { + "name": "gep_status", + "description": "Get the current remote evolution status, including gene and capsule counts, recent events, and memory graph size.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + }, + { + "name": "gep_recall", + "description": "Query private EvoMap evolution memory for relevant past experience. Use this before substantive work to find prior signal-gene-outcome mappings.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "A concise, non-sensitive description of the task or problem to recall experience for." + }, + "signals": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional signal labels such as test_failure, deployment_issue, or capability_gap." + }, + "limit": { + "type": "number", + "minimum": 1, + "maximum": 50, + "description": "Maximum results to return." + }, + "budget_tokens": { + "type": "integer", + "minimum": 0, + "description": "Optional token budget used as a ranking hint." + }, + "budget_usd": { + "type": "number", + "minimum": 0, + "description": "Optional USD budget used as a ranking hint." + }, + "cost_tier": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "Optional qualitative cost tier used as a ranking hint." + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + }, + { + "name": "gep_search_community", + "description": "Search the EvoMap community for relevant Genes and Capsules published by other agents.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Natural-language search query. Do not include secrets or confidential tenant data." + }, + "type": { + "type": "string", + "enum": [ + "Gene", + "Capsule" + ], + "description": "Optional asset type filter." + }, + "outcome": { + "type": "string", + "enum": [ + "success", + "failed" + ], + "description": "Optional outcome filter." + }, + "limit": { + "type": "number", + "minimum": 1, + "maximum": 50, + "description": "Maximum results to return." + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + }, + { + "name": "gep_evolve", + "description": "Build an evolution plan from the supplied context by detecting signals and selecting an appropriate strategy.", + "inputSchema": { + "type": "object", + "properties": { + "context": { + "type": "string", + "description": "A redacted description of the task, error, or capability that should evolve." + }, + "intent": { + "type": "string", + "enum": [ + "repair", + "optimize", + "innovate" + ], + "description": "Optional evolution intent." + } + }, + "required": [ + "context" + ], + "additionalProperties": false + } + }, + { + "name": "gep_record_outcome", + "description": "Record a validated task outcome in private EvoMap evolution memory for future recall.", + "inputSchema": { + "type": "object", + "properties": { + "geneId": { + "type": "string", + "description": "The reused Gene identifier, or ad_hoc when no Gene was used." + }, + "signals": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Signal labels that describe the task." + }, + "status": { + "type": "string", + "enum": [ + "success", + "failed" + ], + "description": "Whether the validated outcome succeeded or failed." + }, + "score": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Outcome confidence or quality score from 0 to 1." + }, + "summary": { + "type": "string", + "description": "A concise, redacted description of the problem, approach, and validation result." + }, + "cost_tokens": { + "type": "integer", + "minimum": 0, + "description": "Optional approximate token cost." + }, + "cost_usd": { + "type": "number", + "minimum": 0, + "description": "Optional approximate USD cost." + }, + "used_asset_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional EvoMap asset identifiers that were genuinely reused." + } + }, + "required": [ + "geneId", + "signals", + "status", + "score", + "summary" + ], + "additionalProperties": false + } + }, + { + "name": "gep_list_genes", + "description": "List available evolution Genes, optionally filtered by category.", + "inputSchema": { + "type": "object", + "properties": { + "category": { + "type": "string", + "enum": [ + "repair", + "optimize", + "innovate" + ], + "description": "Optional category filter." + } + }, + "additionalProperties": false + } + }, + { + "name": "gep_identity", + "description": "Fetch an EvoMap node identity profile and optionally its reputation attestation.", + "inputSchema": { + "type": "object", + "properties": { + "nodeId": { + "type": "string", + "description": "Optional node identifier; defaults to the authenticated node." + }, + "includeAttestation": { + "type": "boolean", + "description": "Whether to include a reputation attestation." + } + }, + "additionalProperties": false + } + }, + { + "name": "gep_protocol_info", + "description": "Return the GEP schema and protocol versions exposed by the server.", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + } + ] +} diff --git a/cowork/outline.png b/cowork/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..459f133da584964e528d3548c450a912b3818ea6 GIT binary patch literal 1264 zcmVeShANpjA)XRovOT6>>x490OBdmHBW`lW0E7*%^1kBLfvfO=J2mF>`3Qvnb$h%D@*DsuMw`iGGS204`Q2_e z3keC~e_saTjR62}L2-s0lolJfHM`?a;T_moPdpF8y~Dj(MYz#-(`u&^+$y}kYG z^z`)i4-O6v8yXsnI-O2NbN?p*PN7gJkB*L9nVFd%l5>ptR#sLPrEVk%0Og#*4h?%9 z4u{Kbx3l>8_;+Z|>2!K57R&RqdLmj5q5<$#OQDeO@9%px8jU(XKR*{(G>C$O=0QMZ zOiWC|#>U29AR?uJopFKG;R{AgQ2+$zM7Z4C+`C;}UEjP1qq)-3(ju$XYD-E=N-!7< zpU%w8{GwK?J){bH$il+0(2KA)bRw)FM&eTz96t%oY2jYgvZdysM@0pQsVdAV@Aa}u@6z;!%uq$ZAOAou843{WU{idvQJJ6d@Xa6M(|P zLPJkaj~R~nYPY$$$qEVz?!cjre%oxeKj@BuwJM~-O_2=N8~y`;76|}Q+aHi~8mA@$ znZ5fHtgNi;Axn|r{Ie6!iyM{C#D*R6ANa~67soU==M!nykZu@qrs zXeCe}DgznwlCT3Qz^+>jdY&!{Ge0si~)dM(Xlr{>x6z4$!1OY|lWH~uGrEt!_2loF-QWL}WYXcB0EiEkr zAF}1;<=++;7pd`xV*9lLP)nt*hQ?BjP;0@3r%;BUaxV7vb1eV@F{!ITg7>JlQ+W+m z;odj^K^-Jf)O)^WNV)xr25{MJiky-n#H^JJK +- Reused: +- Validation: +- Recorded: +``` + +If authentication or the connector failed, say so explicitly instead of emitting a +successful trace. diff --git a/dist/evolver-cowork.zip b/dist/evolver-cowork.zip new file mode 100644 index 0000000000000000000000000000000000000000..bc2141c96856e011ea39fa50239a37065a03f162 GIT binary patch literal 14932 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

eSKE+3f-^Y+qPdAgo-r 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 R{{{{Y(DWA=!T#^={{!^!Hh=&C literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c56899a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,19 @@ +{ + "name": "evolver-copilot-plugin", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "evolver-copilot-plugin", + "version": "0.2.0", + "license": "MIT", + "bin": { + "evolver-copilot-plugin": "scripts/install.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index 0da797a..bfea7d3 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,21 @@ { "name": "evolver-copilot-plugin", - "version": "0.1.0", - "description": "Official EvoMap Evolver customization pack for GitHub Copilot: persistent, auditable agent evolution memory powered by GEP.", + "version": "0.2.0", + "description": "Official EvoMap Evolver integration for GitHub Copilot and Microsoft 365 Copilot Cowork.", "type": "commonjs", "bin": { "evolver-copilot-plugin": "scripts/install.js" }, "files": [ + "cowork/", + "dist/evolver-cowork.zip", "hooks/", "mcp/", "skills/", ".github/copilot-instructions.md", ".github/prompts/", ".vscode/mcp.json", - "scripts/install.js", + "scripts/", "README.md", "CHANGELOG.md", "LICENSE", @@ -22,6 +24,19 @@ "scripts": { "test": "node --test test/*.test.js", "verify": "node scripts/install.js --verify --config-root .", + "validate:cowork": "node scripts/validate-cowork-package.js", + "verify:cowork:remote": "node scripts/check-cowork-remote.mjs", + "build:cowork": "node scripts/build-cowork-package.js", + "toolkit:dcr:login": "npx -y @microsoft/m365agentstoolkit-cli@latest auth login m365 --telemetry false", + "toolkit:dcr:doctor": "npx -y @microsoft/m365agentstoolkit-cli@latest doctor --telemetry false", + "toolkit:dcr:provision": "npx -y @microsoft/m365agentstoolkit-cli@latest provision --env dev --folder toolkit/evolver-cowork-dcr --telemetry false", + "toolkit:dcr:validate": "npx -y @microsoft/m365agentstoolkit-cli@latest validate --env dev --folder toolkit/evolver-cowork-dcr --telemetry false", + "toolkit:dcr:package": "npx -y @microsoft/m365agentstoolkit-cli@latest package --env dev --folder toolkit/evolver-cowork-dcr --telemetry false", + "toolkit:dcr:publish": "npx -y @microsoft/m365agentstoolkit-cli@latest publish --env dev --folder toolkit/evolver-cowork-dcr --telemetry false", + "security:sast": "node scripts/security-scan.js", + "security:deps": "npm audit --omit=dev --audit-level=high", + "security:all": "npm run security:deps && npm run security:sast", + "check": "npm run security:sast && npm test && npm run validate:cowork", "pack:dry": "npm pack --dry-run" }, "keywords": [ diff --git a/scripts/build-cowork-package.js b/scripts/build-cowork-package.js new file mode 100644 index 0000000..94125b9 --- /dev/null +++ b/scripts/build-cowork-package.js @@ -0,0 +1,97 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: MIT + +'use strict'; + +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const { validatePackage } = require('./validate-cowork-package'); + +const root = path.resolve(__dirname, '..'); +const source = path.join(root, 'cowork'); +const dist = path.join(root, 'dist'); +const output = path.join(dist, 'evolver-cowork.zip'); +const ZIP_EPOCH = new Date('1980-01-01T00:00:00.000Z'); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { encoding: 'utf8', ...options }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`${command} failed (${result.status}): ${(result.stderr || result.stdout).trim()}`); + } + return result.stdout; +} + +function normalizeTimestamps(directory) { + const entries = fs.readdirSync(directory, { withFileTypes: true }); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) normalizeTimestamps(entryPath); + fs.utimesSync(entryPath, ZIP_EPOCH, ZIP_EPOCH); + } + fs.utimesSync(directory, ZIP_EPOCH, ZIP_EPOCH); +} + +function build() { + const sourceReport = validatePackage(source); + fs.mkdirSync(dist, { recursive: true }); + if (fs.existsSync(output)) fs.unlinkSync(output); + + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'evolver-cowork-package-')); + try { + fs.cpSync(source, temporary, { recursive: true, dereference: true }); + validatePackage(temporary); + normalizeTimestamps(temporary); + run('zip', [ + '-X', + '-q', + '-r', + output, + 'manifest.json', + 'color.png', + 'outline.png', + 'mcp-tools.json', + 'skills', + ], { cwd: temporary, env: { ...process.env, TZ: 'UTC' } }); + } finally { + fs.rmSync(temporary, { recursive: true, force: true }); + } + + const listing = run('unzip', ['-Z1', output]) + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + for (const required of ['manifest.json', 'color.png', 'outline.png', 'mcp-tools.json', 'skills/capability-evolver/SKILL.md']) { + if (!listing.includes(required)) throw new Error(`built ZIP is missing root entry: ${required}`); + } + if (listing.some((entry) => entry.startsWith('/') || entry.includes('../') || entry.startsWith('__MACOSX/'))) { + throw new Error('built ZIP contains an unsafe or platform-specific path'); + } + + const archive = fs.readFileSync(output); + const sha256 = crypto.createHash('sha256').update(archive).digest('hex'); + const report = { + ok: true, + output, + version: sourceReport.version, + bytes: archive.length, + sha256, + entries: listing.length, + }; + process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); + return report; +} + +if (require.main === module) { + try { + build(); + } catch (error) { + process.stderr.write(`Cowork package build failed: ${error.message}\n`); + process.exitCode = 1; + } +} + +module.exports = { build }; diff --git a/scripts/check-cowork-remote.mjs b/scripts/check-cowork-remote.mjs new file mode 100644 index 0000000..6395169 --- /dev/null +++ b/scripts/check-cowork-remote.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: MIT + +const MCP_URL = 'https://evomap.ai/mcp'; +const RESOURCE_METADATA_URL = 'https://evomap.ai/.well-known/oauth-protected-resource'; +const AUTHORIZATION_METADATA_URL = 'https://evomap.ai/.well-known/oauth-authorization-server'; + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +async function json(url) { + const response = await fetch(url, { + headers: { Accept: 'application/json' }, + signal: AbortSignal.timeout(10000), + }); + assert(response.ok, `${url} returned HTTP ${response.status}`); + return response.json(); +} + +async function main() { + const resource = await json(RESOURCE_METADATA_URL); + assert(resource.resource === 'https://evomap.ai', 'OAuth protected-resource metadata has an unexpected resource'); + assert(Array.isArray(resource.authorization_servers) && resource.authorization_servers.includes('https://evomap.ai'), 'OAuth protected-resource metadata has no EvoMap authorization server'); + + const authorization = await json(AUTHORIZATION_METADATA_URL); + assert(authorization.issuer === 'https://evomap.ai', 'OAuth issuer is not EvoMap'); + assert(authorization.authorization_endpoint === 'https://evomap.ai/oauth/authorize', 'OAuth authorization endpoint changed'); + assert(authorization.token_endpoint === 'https://evomap.ai/oauth/token', 'OAuth token endpoint changed'); + assert(typeof authorization.registration_endpoint === 'string' && authorization.registration_endpoint.startsWith('https://'), 'OAuth Dynamic Client Registration endpoint is missing'); + assert(Array.isArray(authorization.code_challenge_methods_supported) && authorization.code_challenge_methods_supported.includes('S256'), 'OAuth PKCE S256 support is missing'); + + const initialize = await fetch(MCP_URL, { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json', + 'MCP-Protocol-Version': '2025-06-18', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { name: 'evolver-cowork-package-check', version: '1.0.0' }, + }, + }), + signal: AbortSignal.timeout(10000), + }); + assert(initialize.status === 401, `unauthenticated MCP initialize should return 401, got ${initialize.status}`); + const challenge = initialize.headers.get('www-authenticate') || ''; + assert(challenge.includes('Bearer'), 'MCP endpoint did not return a Bearer challenge'); + assert(challenge.includes(RESOURCE_METADATA_URL), 'MCP Bearer challenge did not advertise OAuth resource metadata'); + + process.stdout.write(`${JSON.stringify({ + ok: true, + mcpUrl: MCP_URL, + oauthIssuer: authorization.issuer, + registrationEndpoint: authorization.registration_endpoint, + pkce: 'S256', + unauthenticatedInitialize: initialize.status, + }, null, 2)}\n`); +} + +main().catch((error) => { + process.stderr.write(`Cowork remote check failed: ${error.message}\n`); + process.exitCode = 1; +}); diff --git a/scripts/security-scan.js b/scripts/security-scan.js new file mode 100644 index 0000000..9130431 --- /dev/null +++ b/scripts/security-scan.js @@ -0,0 +1,55 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: MIT + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const root = path.resolve(__dirname, '..'); +const scanRoots = ['.github', 'hooks', 'mcp', 'scripts', 'cowork', 'test', 'toolkit']; +const rootFiles = ['package.json', 'package-lock.json']; +const extensions = new Set(['.js', '.mjs', '.json', '.md', '.txt', '.yml', '.yaml']); +const findings = []; + +const rules = [ + ['dynamic code execution', /\beval\s*\(|\bnew\s+Function\s*\(/g], + ['shell-enabled child process', /\bshell\s*:\s*true\b/g], + ['string-based child_process exec', /\b(?:exec|execSync)\s*\(/g], + ['embedded private key', /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g], + ['embedded credential', /\b(?:api[_-]?key|client[_-]?secret|access[_-]?token)\s*[:=]\s*["'][^"']{12,}["']/gi], + ['unsafe TLS bypass', /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*["']?0/g], +]; + +function walk(directory) { + if (!fs.existsSync(directory)) return []; + const files = []; + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const filePath = path.join(directory, entry.name); + if (entry.isSymbolicLink()) findings.push({ file: path.relative(root, filePath), rule: 'symbolic link in scanned source' }); + else if (entry.isDirectory()) files.push(...walk(filePath)); + else if (entry.isFile() && extensions.has(path.extname(entry.name))) files.push(filePath); + } + return files; +} + +const files = [ + ...rootFiles.map((file) => path.join(root, file)).filter((file) => fs.existsSync(file)), + ...scanRoots.flatMap((directory) => walk(path.join(root, directory))), +]; +for (const file of files) { + if (file === __filename) continue; + const content = fs.readFileSync(file, 'utf8'); + for (const [name, pattern] of rules) { + pattern.lastIndex = 0; + if (pattern.test(content)) findings.push({ file: path.relative(root, file), rule: name }); + } +} + +if (findings.length > 0) { + process.stderr.write(`SAST preflight found ${findings.length} issue(s):\n`); + for (const finding of findings) process.stderr.write(`- ${finding.file}: ${finding.rule}\n`); + process.exitCode = 1; +} else { + process.stdout.write(`SAST preflight passed (${files.length} files, ${rules.length} rules).\n`); +} diff --git a/scripts/validate-cowork-package.js b/scripts/validate-cowork-package.js new file mode 100644 index 0000000..3931236 --- /dev/null +++ b/scripts/validate-cowork-package.js @@ -0,0 +1,185 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: MIT + +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const REQUIRED_MANIFEST_FIELDS = [ + 'manifestVersion', + 'version', + 'id', + 'developer', + 'name', + 'description', + 'icons', + 'accentColor', +]; + +function fail(message) { + throw new Error(`Cowork package validation failed: ${message}`); +} + +function assert(condition, message) { + if (!condition) fail(message); +} + +function readJson(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (error) { + fail(`${path.basename(filePath)} is not valid JSON: ${error.message}`); + } +} + +function readPngInfo(filePath) { + const data = fs.readFileSync(filePath); + const pngSignature = '89504e470d0a1a0a'; + assert(data.length >= 26, `${path.basename(filePath)} is too small to be a PNG`); + assert(data.subarray(0, 8).toString('hex') === pngSignature, `${path.basename(filePath)} is not a PNG`); + assert(data.subarray(12, 16).toString('ascii') === 'IHDR', `${path.basename(filePath)} has no PNG IHDR`); + return { + width: data.readUInt32BE(16), + height: data.readUInt32BE(20), + colorType: data[25], + }; +} + +function frontmatterValue(markdown, key) { + const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---/); + assert(match, 'SKILL.md must start with YAML frontmatter between --- delimiters'); + const line = match[1].split(/\r?\n/).find((entry) => entry.startsWith(`${key}:`)); + assert(line, `SKILL.md frontmatter is missing ${key}`); + return line.slice(key.length + 1).trim(); +} + +function walkFiles(root) { + const files = []; + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + const absolute = path.join(root, entry.name); + assert(!entry.isSymbolicLink(), `symbolic links are not allowed: ${absolute}`); + if (entry.isDirectory()) files.push(...walkFiles(absolute)); + else if (entry.isFile()) files.push(absolute); + } + return files; +} + +function validatePackage(packageRoot) { + const root = path.resolve(packageRoot); + assert(fs.existsSync(root), `package source does not exist: ${root}`); + + const manifestPath = path.join(root, 'manifest.json'); + const manifest = readJson(manifestPath); + for (const field of REQUIRED_MANIFEST_FIELDS) { + assert(manifest[field] !== undefined, `manifest.json is missing ${field}`); + } + assert(manifest.manifestVersion === '1.28', 'manifestVersion must be 1.28'); + assert(/^\d+\.\d+\.\d+$/.test(manifest.version), 'manifest version must be numeric semver'); + assert(/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(manifest.id), 'manifest id must be a UUID'); + assert(/^#[0-9a-f]{6}$/i.test(manifest.accentColor), 'accentColor must be a six-digit hex color'); + + for (const urlField of ['websiteUrl', 'privacyUrl', 'termsOfUseUrl']) { + const value = manifest.developer[urlField]; + assert(typeof value === 'string' && value.startsWith('https://'), `developer.${urlField} must use HTTPS`); + } + + const iconExpectations = [ + ['color', 192], + ['outline', 32], + ]; + for (const [kind, size] of iconExpectations) { + const relative = manifest.icons[kind]; + assert(typeof relative === 'string' && !relative.includes('..'), `icons.${kind} must be a safe relative path`); + const info = readPngInfo(path.join(root, relative)); + assert(info.width === size && info.height === size, `${relative} must be ${size}x${size}, got ${info.width}x${info.height}`); + assert([4, 6].includes(info.colorType), `${relative} must have an alpha channel`); + } + + assert(Array.isArray(manifest.agentSkills) && manifest.agentSkills.length > 0, 'manifest must declare at least one agentSkill'); + assert(manifest.agentSkills.length <= 20, 'manifest can declare at most 20 agentSkills'); + const skillFolders = new Set(); + for (const skill of manifest.agentSkills) { + assert(typeof skill.folder === 'string' && skill.folder.length <= 256, 'each agentSkill needs a valid folder'); + assert(!skill.folder.startsWith('/') && !skill.folder.includes('..') && !skill.folder.includes('\\'), `unsafe skill folder: ${skill.folder}`); + assert(!skillFolders.has(skill.folder), `duplicate skill folder: ${skill.folder}`); + skillFolders.add(skill.folder); + const skillFile = path.join(root, skill.folder, 'SKILL.md'); + assert(fs.existsSync(skillFile), `missing ${skill.folder}/SKILL.md`); + const markdown = fs.readFileSync(skillFile, 'utf8'); + const folderName = path.basename(skill.folder); + const name = frontmatterValue(markdown, 'name'); + frontmatterValue(markdown, 'description'); + assert(name === folderName, `skill name ${name} must match folder ${folderName}`); + assert(/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name), `skill name must be kebab-case: ${name}`); + for (const forbidden of ['127.0.0.1', 'localhost', '~/.evolver', 'EVOMAP_API_KEY', 'process.env']) { + assert(!markdown.includes(forbidden), `Cowork skill contains unsupported local/runtime reference: ${forbidden}`); + } + } + + assert(Array.isArray(manifest.agentConnectors) && manifest.agentConnectors.length === 1, 'manifest must declare the EvoMap connector exactly once'); + const connector = manifest.agentConnectors[0]; + const remote = connector.toolSource && connector.toolSource.remoteMcpServer; + assert(connector.id === 'evomap-gep', 'connector id must remain stable as evomap-gep'); + assert(remote && remote.mcpServerUrl === 'https://evomap.ai/mcp', 'connector must use the hosted EvoMap MCP endpoint'); + assert(remote.authorization === undefined, 'authorization must be omitted so Cowork can use EvoMap Dynamic Client Registration'); + assert(remote.mcpToolDescription && typeof remote.mcpToolDescription.file === 'string', 'connector must reference a static MCP tool description'); + + const toolCatalog = readJson(path.join(root, remote.mcpToolDescription.file)); + assert(Array.isArray(toolCatalog.tools) && toolCatalog.tools.length > 0, 'MCP tool description must contain tools'); + const toolNames = new Set(); + for (const tool of toolCatalog.tools) { + assert(typeof tool.name === 'string' && /^[a-z0-9_]+$/.test(tool.name), `invalid MCP tool name: ${tool.name}`); + assert(!toolNames.has(tool.name), `duplicate MCP tool: ${tool.name}`); + toolNames.add(tool.name); + assert(typeof tool.description === 'string' && tool.description.length > 10, `${tool.name} needs a useful description`); + assert(tool.inputSchema && tool.inputSchema.type === 'object', `${tool.name} needs an object inputSchema`); + } + for (const requiredTool of ['gep_status', 'gep_recall', 'gep_search_community', 'gep_evolve', 'gep_record_outcome']) { + assert(toolNames.has(requiredTool), `MCP tool description is missing ${requiredTool}`); + } + + const files = walkFiles(root); + const relativeFiles = files.map((file) => path.relative(root, file).split(path.sep).join('/')); + for (const relative of relativeFiles) { + assert(!relative.split('/').some((part) => part.startsWith('.')), `hidden files are not allowed: ${relative}`); + assert(!relative.includes('..'), `path traversal is not allowed: ${relative}`); + assert(fs.statSync(path.join(root, relative)).size <= 5 * 1024 * 1024, `file exceeds 5 MB: ${relative}`); + } + const allowedTopLevel = new Set(['manifest.json', 'color.png', 'outline.png', 'mcp-tools.json', 'skills']); + for (const relative of relativeFiles) { + assert(allowedTopLevel.has(relative.split('/')[0]), `unexpected top-level package content: ${relative}`); + } + + const text = files + .filter((file) => /\.(?:json|md)$/i.test(file)) + .map((file) => fs.readFileSync(file, 'utf8')) + .join('\n'); + const secretPatterns = [ + /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, + /\b(?:api[_-]?key|client[_-]?secret|access[_-]?token)\s*[:=]\s*["'][^"']{12,}["']/i, + /\bek_[a-z0-9_-]{16,}\b/i, + ]; + for (const pattern of secretPatterns) assert(!pattern.test(text), `potential embedded secret matched ${pattern}`); + + return { + root, + version: manifest.version, + appId: manifest.id, + skills: manifest.agentSkills.length, + tools: toolCatalog.tools.length, + files: relativeFiles.sort(), + }; +} + +if (require.main === module) { + try { + const report = validatePackage(process.argv[2] || path.resolve(__dirname, '..', 'cowork')); + process.stdout.write(`${JSON.stringify({ ok: true, ...report }, null, 2)}\n`); + } catch (error) { + process.stderr.write(`${error.message}\n`); + process.exitCode = 1; + } +} + +module.exports = { validatePackage, readPngInfo }; diff --git a/test/cowork-package.test.js b/test/cowork-package.test.js new file mode 100644 index 0000000..fc660f9 --- /dev/null +++ b/test/cowork-package.test.js @@ -0,0 +1,62 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const ROOT = path.resolve(__dirname, '..'); +const COWORK = path.join(ROOT, 'cowork'); +const { validatePackage, readPngInfo } = require('../scripts/validate-cowork-package'); + +describe('Copilot Cowork package', () => { + it('passes package validation with the required skill and hosted MCP tools', () => { + const report = validatePackage(COWORK); + assert.equal(report.version, '0.2.0'); + assert.equal(report.skills, 1); + assert.ok(report.tools >= 5); + assert.ok(report.files.includes('skills/capability-evolver/SKILL.md')); + }); + + it('uses exact Microsoft icon dimensions with alpha channels', () => { + const color = readPngInfo(path.join(COWORK, 'color.png')); + const outline = readPngInfo(path.join(COWORK, 'outline.png')); + assert.deepEqual([color.width, color.height], [192, 192]); + assert.deepEqual([outline.width, outline.height], [32, 32]); + assert.ok([4, 6].includes(color.colorType)); + assert.ok([4, 6].includes(outline.colorType)); + }); + + it('builds an uploadable ZIP with package files at the archive root', () => { + const build = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'build-cowork-package.js')], { + cwd: ROOT, + encoding: 'utf8', + timeout: 15000, + }); + assert.equal(build.status, 0, build.stderr || build.stdout); + const zipPath = path.join(ROOT, 'dist', 'evolver-cowork.zip'); + assert.ok(fs.existsSync(zipPath)); + const list = spawnSync('unzip', ['-Z1', zipPath], { encoding: 'utf8' }); + assert.equal(list.status, 0, list.stderr); + const entries = list.stdout.trim().split(/\r?\n/); + assert.ok(entries.includes('manifest.json')); + assert.ok(entries.includes('mcp-tools.json')); + assert.ok(entries.includes('skills/capability-evolver/SKILL.md')); + assert.ok(!entries.some((entry) => entry.startsWith('cowork/') || entry.startsWith('__MACOSX/'))); + + const firstArchive = fs.readFileSync(zipPath); + const rebuild = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'build-cowork-package.js')], { + cwd: ROOT, + encoding: 'utf8', + timeout: 15000, + }); + assert.equal(rebuild.status, 0, rebuild.stderr || rebuild.stdout); + assert.deepEqual(fs.readFileSync(zipPath), firstArchive, 'Cowork ZIP should be reproducible'); + }); + + it('has no runtime npm dependencies in the upload package or repository', () => { + const packageJson = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + assert.deepEqual(packageJson.dependencies || {}, {}); + const manifest = JSON.parse(fs.readFileSync(path.join(COWORK, 'manifest.json'), 'utf8')); + assert.equal(JSON.stringify(manifest).includes('node_modules'), false); + }); +}); diff --git a/toolkit/evolver-cowork-dcr/.gitignore b/toolkit/evolver-cowork-dcr/.gitignore new file mode 100644 index 0000000..d3ce28d --- /dev/null +++ b/toolkit/evolver-cowork-dcr/.gitignore @@ -0,0 +1,17 @@ +# TeamsFx files +env/.env.* +!env/.env.example +env/.env.local +.localConfigs +appPackage/build + +# dependencies +node_modules/ + +# misc +.env +.deployment +.DS_Store + +# generated files +appPackage/.generated diff --git a/toolkit/evolver-cowork-dcr/.vscode/extensions.json b/toolkit/evolver-cowork-dcr/.vscode/extensions.json new file mode 100644 index 0000000..aac0a6e --- /dev/null +++ b/toolkit/evolver-cowork-dcr/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "TeamsDevApp.ms-teams-vscode-extension" + ] +} diff --git a/toolkit/evolver-cowork-dcr/.vscode/launch.json b/toolkit/evolver-cowork-dcr/.vscode/launch.json new file mode 100644 index 0000000..6bf71a9 --- /dev/null +++ b/toolkit/evolver-cowork-dcr/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Preview in Copilot (Edge)", + "type": "msedge", + "request": "launch", + "url": "https://m365.cloud.microsoft/chat/entity1-d870f6cd-4aa5-4d42-9626-ab690c041429/${agent-hint}?auth=2&developerMode=Basic", + "presentation": { + "group": "remote", + "order": 1 + }, + "internalConsoleOptions": "neverOpen", + "runtimeArgs": ["--remote-debugging-port=9222", "--no-first-run"] + }, + { + "name": "Preview in Copilot (Chrome)", + "type": "chrome", + "request": "launch", + "url": "https://m365.cloud.microsoft/chat/entity1-d870f6cd-4aa5-4d42-9626-ab690c041429/${agent-hint}?auth=2&developerMode=Basic", + "presentation": { + "group": "remote", + "order": 2 + }, + "internalConsoleOptions": "neverOpen", + "runtimeArgs": ["--remote-debugging-port=9223", "--no-first-run"] + } + ] +} diff --git a/toolkit/evolver-cowork-dcr/.vscode/mcp.json b/toolkit/evolver-cowork-dcr/.vscode/mcp.json new file mode 100644 index 0000000..b4be3c2 --- /dev/null +++ b/toolkit/evolver-cowork-dcr/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "evomapai": { + "type": "http", + "url": "https://evomap.ai/mcp" + } + } +} \ No newline at end of file diff --git a/toolkit/evolver-cowork-dcr/.vscode/settings.json b/toolkit/evolver-cowork-dcr/.vscode/settings.json new file mode 100644 index 0000000..187eca2 --- /dev/null +++ b/toolkit/evolver-cowork-dcr/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "debug.onTaskErrors": "abort", + "json.schemas": [ + { + "fileMatch": ["/aad.*.json"], + "schema": {} + } + ], + "files.readonlyInclude": { + "appPackage/.generated/**/*": true + } +} diff --git a/toolkit/evolver-cowork-dcr/README.md b/toolkit/evolver-cowork-dcr/README.md new file mode 100644 index 0000000..1b25e1a --- /dev/null +++ b/toolkit/evolver-cowork-dcr/README.md @@ -0,0 +1,122 @@ +# Evolver Cowork DCR provisioning harness + +This isolated Microsoft 365 Agents Toolkit project verifies that Microsoft's +Enterprise Token Store can dynamically register an OAuth client with +`https://evomap.ai/mcp`. It is a publisher-side provisioning and diagnostic +harness, not the end-user Cowork plugin ZIP in `../../dist/`. + +## What provisioning creates + +The `provision` lifecycle in `m365agents.yml`: + +1. Creates a development Microsoft 365/Teams app registration. +2. Runs `dcr/register` against EvoMap's OAuth authorization-server metadata. +3. Writes the resulting auth configuration ID to + `MCP_DA_AUTH_ID_EVOMAPAI` in the ignored `env/.env.dev` file. +4. Builds and validates a development declarative-agent package. +5. Installs the development agent for personal testing. + +The DCR configuration is scoped to the signed-in home tenant and is applicable +to any app in that tenant. Per-user access and refresh tokens remain separate; +the generated ID identifies the plugin's OAuth configuration, not an EvoMap +user. + +## Publisher steps + +Use Node.js 22 or 24 and a Microsoft 365 account that can upload custom apps: + +1. From the repository root, run: + +```bash +npm run toolkit:dcr:login +npm run toolkit:dcr:doctor +npm run toolkit:dcr:provision +``` + +2. Complete the Microsoft 365 sign-in opened by `toolkit:dcr:login`. +3. Wait for every `toolkit:dcr:provision` lifecycle step to pass. +4. Open `env/.env.dev`. +5. Confirm `MCP_DA_AUTH_ID_EVOMAPAI` is not empty. +6. Stop if provisioning fails or the ID is empty. + +Provisioning changes the signed-in tenant. Do not commit `env/.env.dev`; it +contains tenant-specific resource IDs. See +[Microsoft 365 Agents Toolkit CLI](https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/microsoft-365-agents-toolkit-cli). + +## Current compatibility gate + +The 2026-07-18 verification run reached Microsoft Teams Graph but failed during +`dcr/register` with: + +```text +InvalidRegistrationResponse: IDP registration response is missing required +field: client_secret. +``` + +Microsoft 365 DCR currently requires the authorization server to return both +`client_id` and `client_secret`. EvoMap advertises DCR but currently returns a +public-client registration without `client_secret`, so Microsoft did not create +`MCP_DA_AUTH_ID_EVOMAPAI`. EvoMap must update its RFC 7591 registration response +to issue a confidential client secret before this flow can succeed. See +[Configure dynamic client registration](https://learn.microsoft.com/en-us/microsoft-365/copilot/extensibility/plugin-authentication-dynamic-client-registration). + +The fix belongs in EvoMap's server, not in this Toolkit environment file: + +```text +POST https://evomap.ai/oauth/register +``` + +The successful registration response must contain: + +```json +{ + "client_id": "", + "client_secret": "", + "token_endpoint_auth_method": "client_secret_post", + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "response_types": ["code"] +} +``` + +Do not manually place that secret in `env/.env.dev`. Agents Toolkit passes the +registration response to Microsoft Enterprise Token Store and writes a different +value—the resulting auth-config ID—to `MCP_DA_AUTH_ID_EVOMAPAI`. + +After the server fix, rerun `npm run toolkit:dcr:provision`. The existing +development app ID in the ignored local environment file can be reused. + +## Build and deploy after successful DCR + +1. Validate and package the harness: + +```bash +npm run toolkit:dcr:validate +npm run toolkit:dcr:package +``` + +2. Open the personal development agent created by provisioning. +3. Ask it to call `gep_identity`, then `gep_status`. +4. Complete the EvoMap sign-in prompt. +5. Stop if either real tool call is missing or fails. +6. Only after the test succeeds, publish the harness to the tenant catalog: + + +```bash +npm run toolkit:dcr:publish +``` + +The generated package is written beneath `appPackage/build/`. Publishing this +harness is separate from building the Cowork plugin: + +```bash +npm run build:cowork +``` + +The Cowork artifact remains `../../dist/evolver-cowork.zip` and is uploaded from +Cowork **left navigation → Customize → Plugins → Upload plugin**. See +[Upload a plugin package](https://learn.microsoft.com/en-us/microsoft-365/copilot/cowork/cowork-customize#upload-a-plugin-package). +Do not claim the Cowork connector is deployable until the DCR provisioning +harness and the per-user identity preflight both succeed. diff --git a/toolkit/evolver-cowork-dcr/appPackage/ai-plugin.json b/toolkit/evolver-cowork-dcr/appPackage/ai-plugin.json new file mode 100644 index 0000000..f3e07f6 --- /dev/null +++ b/toolkit/evolver-cowork-dcr/appPackage/ai-plugin.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/copilot/plugin/v2.4/schema.json", + "schema_version": "v2.4", + "name_for_human": "Evolver DCR verification", + "description_for_human": "Provision and verify per-user EvoMap OAuth for Evolver through Microsoft 365 Agents Toolkit.", + "contact_email": "team@evomap.ai", + "namespace": "evomapai", + "functions": [], + "runtimes": [ + { + "type": "RemoteMCPServer", + "spec": { + "url": "https://evomap.ai/mcp" + }, + "run_for_functions": [ + "*" + ], + "auth": { + "type": "OAuthPluginVault", + "reference_id": "${{MCP_DA_AUTH_ID_EVOMAPAI}}" + } + } + ] +} diff --git a/toolkit/evolver-cowork-dcr/appPackage/color.png b/toolkit/evolver-cowork-dcr/appPackage/color.png new file mode 100644 index 0000000000000000000000000000000000000000..11e255fa0b831ca86ff380e109882ffdca5dc3d2 GIT binary patch literal 5923 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 TvMtV90~2)p5d literal 0 HcmV?d00001 diff --git a/toolkit/evolver-cowork-dcr/appPackage/declarativeAgent.json b/toolkit/evolver-cowork-dcr/appPackage/declarativeAgent.json new file mode 100644 index 0000000..58aae3b --- /dev/null +++ b/toolkit/evolver-cowork-dcr/appPackage/declarativeAgent.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.7/schema.json", + "version": "v1.7", + "name": "Evolver DCR Verification${{APP_NAME_SUFFIX}}", + "description": "Verifies EvoMap OAuth DCR and per-user GEP connector access before Cowork deployment.", + "instructions": "$[file('instruction.txt')]", + "conversation_starters": [ + { + "title": "Verify EvoMap identity", + "text": "Connect to EvoMap, call gep_identity, and then call gep_status. Stop if either call fails." + } + ], + "actions": [ + { + "id": "action_1", + "file": "ai-plugin.json" + } + ] +} diff --git a/toolkit/evolver-cowork-dcr/appPackage/instruction.txt b/toolkit/evolver-cowork-dcr/appPackage/instruction.txt new file mode 100644 index 0000000..454c5b2 --- /dev/null +++ b/toolkit/evolver-cowork-dcr/appPackage/instruction.txt @@ -0,0 +1,4 @@ +Use the EvoMap MCP connector only after authentication succeeds. Call +gep_identity without a nodeId, require the user to complete EvoMap sign-in if +prompted, and stop if identity verification fails. Only after gep_identity +succeeds, call gep_status. Never simulate connector results. diff --git a/toolkit/evolver-cowork-dcr/appPackage/manifest.json b/toolkit/evolver-cowork-dcr/appPackage/manifest.json new file mode 100644 index 0000000..7285818 --- /dev/null +++ b/toolkit/evolver-cowork-dcr/appPackage/manifest.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.29/MicrosoftTeams.schema.json", + "manifestVersion": "1.29", + "version": "1.0.0", + "id": "${{TEAMS_APP_ID}}", + "developer": { + "name": "EvoMap", + "websiteUrl": "https://evomap.ai", + "privacyUrl": "https://evomap.ai/privacy", + "termsOfUseUrl": "https://evomap.ai/terms" + }, + "icons": { + "color": "color.png", + "outline": "outline.png" + }, + "name": { + "short": "Evolver DCR${{APP_NAME_SUFFIX}}", + "full": "Evolver EvoMap DCR Verification for Microsoft 365" + }, + "description": { + "short": "Verify EvoMap OAuth DCR and per-user connector sign-in.", + "full": "Publisher-side verification harness for EvoMap Dynamic Client Registration and per-user GEP MCP authentication before deploying Evolver to Copilot Cowork." + }, + "accentColor": "#FFFFFF", + "supportsChannelFeatures": "tier1", + "composeExtensions": [], + "copilotAgents": { + "declarativeAgents": [ + { + "id": "declarativeAgent", + "file": "declarativeAgent.json" + } + ] + }, + "permissions": [ + "identity", + "messageTeamMembers" + ], + "validDomains": [ + "evomap.ai" + ] +} diff --git a/toolkit/evolver-cowork-dcr/appPackage/outline.png b/toolkit/evolver-cowork-dcr/appPackage/outline.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a4c864475f219c8ff252e15ee250cd2308c9f5 GIT binary patch literal 492 zcmVfQ-;iK$xI(f`$oT17L!(LFfcz168`nA*Cc%I0atv-RTUm zZ2wkd832qx#F%V@dJ3`^u!1Jbu|MA-*zqXsjx6)|^3FfFwG`kef*{y-Ind7Q&tc211>U&A`hY=1aJl9Iuetm z$}wv*0hFK%+BrvIsvN?C7pA3{MC8=uea7593GXf-z|+;_E5i;~j+ukPpM7$AJ