@vellumai/assistant 0.12.2-staging.1 → 0.12.2-staging.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/desktop-browser-cli.md +33 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +13 -8
- package/node_modules/@vellumai/slack-text/src/label-resolution-entities.test.ts +95 -0
- package/openapi.yaml +50 -4
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +279 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/config-sounds-sync.test.ts +23 -0
- package/src/__tests__/conversation-error.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +33 -3
- package/src/__tests__/conversation-surfaces-point-at-budget.test.ts +1 -0
- package/src/__tests__/conversation-surfaces-point-at-capability.test.ts +1 -0
- package/src/__tests__/credential-routes.test.ts +38 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/cu-unified-flow.test.ts +6 -2
- package/src/__tests__/external-plugin-loader.test.ts +52 -0
- package/src/__tests__/headless-browser-mode.test.ts +48 -0
- package/src/__tests__/host-cu-proxy.test.ts +185 -30
- package/src/__tests__/list-all-apps.test.ts +40 -8
- package/src/__tests__/mcp-auth-routes.test.ts +99 -0
- package/src/__tests__/mcp-client-auth.test.ts +54 -0
- package/src/__tests__/mcp-health-check.test.ts +32 -32
- package/src/__tests__/mcp-list-plugin-servers.test.ts +77 -32
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +4 -2
- package/src/__tests__/oauth-commands-routes.test.ts +55 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +22 -0
- package/src/__tests__/oauth-providers-routes.test.ts +1 -0
- package/src/__tests__/secret-routes-acp-guard.test.ts +59 -1
- package/src/__tests__/skills.test.ts +10 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +51 -0
- package/src/__tests__/ui-channel-variants.test.ts +1 -56
- package/src/__tests__/user-plugin-loader.test.ts +21 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +257 -6
- package/src/acp/__tests__/acp-credentials.test.ts +13 -0
- package/src/acp/__tests__/claude-token-refresh.test.ts +257 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +60 -1
- package/src/acp/acp-claude-oauth.ts +328 -14
- package/src/acp/acp-credentials.ts +19 -0
- package/src/acp/claude-token-refresh.ts +150 -0
- package/src/acp/prepare-agent-env.ts +21 -6
- package/src/apps/app-store.ts +6 -5
- package/src/browser/operations.ts +2 -1
- package/src/browser/types.ts +7 -0
- package/src/browser/virtual-desktop-target.ts +34 -0
- package/src/calls/__tests__/progress-narration.test.ts +23 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +69 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +81 -0
- package/src/calls/progress-narration.ts +15 -2
- package/src/calls/voice-control-protocol.ts +132 -0
- package/src/calls/voice-session-bridge.ts +56 -26
- package/src/calls/voice-triage-escalate.ts +3 -3
- package/src/cli/commands/__tests__/browser.test.ts +84 -3
- package/src/cli/commands/browser.help.ts +43 -0
- package/src/cli/commands/browser.ts +52 -8
- package/src/cli/commands/oauth/status.ts +5 -0
- package/src/cli/commands/plugins.ts +16 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +111 -0
- package/src/cli/lib/__tests__/install-from-platform.test.ts +77 -2
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +45 -3
- package/src/cli/lib/__tests__/plugin-details.test.ts +26 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +122 -5
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/install-from-github.ts +17 -18
- package/src/cli/lib/install-from-platform.ts +14 -9
- package/src/cli/lib/list-installed-plugins.ts +21 -35
- package/src/cli/lib/plugin-details.ts +21 -8
- package/src/cli/lib/uninstall-plugin.ts +59 -1
- package/src/config/__tests__/plugin-resident-skill-discovery.test.ts +21 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +43 -4
- package/src/config/bundled-skills/computer-use/TOOLS.json +3 -3
- package/src/config/bundled-skills/screen-annotation/SKILL.md +2 -2
- package/src/config/feature-flag-registry.json +11 -3
- package/src/config/loader.ts +1 -0
- package/src/config/schemas/__tests__/voice.test.ts +2 -2
- package/src/config/schemas/mcp.ts +9 -7
- package/src/config/schemas/monitoring.ts +9 -0
- package/src/config/schemas/services.ts +10 -0
- package/src/config/schemas/voice.ts +2 -2
- package/src/config/skills.ts +13 -21
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +47 -0
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +1 -0
- package/src/daemon/conversation-client-surface.ts +26 -0
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-surfaces.ts +6 -1
- package/src/daemon/conversation-tool-setup.ts +37 -43
- package/src/daemon/host-cu-proxy.ts +94 -31
- package/src/daemon/host-proxy-preactivation.ts +2 -0
- package/src/daemon/mcp-reload-service.ts +7 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/providers-setup.ts +4 -0
- package/src/desktop/__tests__/fake-desktop.ts +1 -0
- package/src/desktop/desktop-automation-lease.test.ts +208 -0
- package/src/desktop/desktop-automation-lease.ts +270 -0
- package/src/desktop/desktop-browser-client.test.ts +413 -0
- package/src/desktop/desktop-browser-client.ts +430 -0
- package/src/desktop/desktop-browser-cursor.ts +40 -0
- package/src/desktop/desktop-browser-endpoint.test.ts +78 -0
- package/src/desktop/desktop-browser-endpoint.ts +145 -0
- package/src/desktop/desktop-browser-operations.ts +130 -0
- package/src/desktop/desktop-chrome-session.test.ts +48 -0
- package/src/desktop/desktop-chrome-session.ts +58 -8
- package/src/desktop/desktop-dependencies.test.ts +29 -0
- package/src/desktop/desktop-dependencies.ts +26 -5
- package/src/desktop/desktop-display.ts +9 -0
- package/src/desktop/desktop-panel-config.ts +14 -1
- package/src/desktop/desktop-session-manager.test.ts +86 -3
- package/src/desktop/desktop-session-manager.ts +144 -24
- package/src/desktop/desktop-stream-bridge.ts +1 -1
- package/src/desktop/virtual-desktop-feature.ts +18 -0
- package/src/desktop/virtual-desktop-platform.test.ts +61 -0
- package/src/i18n/__tests__/i18n.test.ts +9 -5
- package/src/i18n/messages.ts +13 -4
- package/src/ipc/__tests__/browser-ipc.test.ts +230 -2
- package/src/ipc/__tests__/cancel-on-disconnect.test.ts +54 -0
- package/src/ipc/assistant-server.ts +15 -1
- package/src/ipc/cli-client.ts +13 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +143 -0
- package/src/live-voice/__tests__/live-voice-events.test.ts +9 -7
- package/src/live-voice/__tests__/live-voice-progress.test.ts +79 -0
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +19 -0
- package/src/live-voice/__tests__/protocol.test.ts +47 -0
- package/src/live-voice/__tests__/session-controls.test.ts +113 -0
- package/src/live-voice/live-voice-metrics.ts +9 -0
- package/src/live-voice/live-voice-session.ts +88 -7
- package/src/live-voice/protocol.ts +69 -0
- package/src/live-voice/session-controls.ts +139 -0
- package/src/mcp/__tests__/credential-target.test.ts +59 -0
- package/src/mcp/__tests__/effective-config.test.ts +11 -0
- package/src/mcp/__tests__/manager-state.test.ts +128 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +20 -6
- package/src/mcp/__tests__/mcp-auth-state-target.test.ts +49 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-cleanup.test.ts +107 -0
- package/src/mcp/__tests__/plugin-mcp-oauth-provider-isolation.test.ts +89 -0
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +63 -11
- package/src/mcp/__tests__/reload-signal-emission.test.ts +39 -2
- package/src/mcp/client.ts +64 -16
- package/src/mcp/credential-target.ts +105 -0
- package/src/mcp/effective-config.ts +42 -15
- package/src/mcp/manager.ts +61 -8
- package/src/mcp/mcp-auth-orchestrator.ts +17 -4
- package/src/mcp/mcp-auth-state.ts +38 -6
- package/src/mcp/mcp-oauth-provider.ts +101 -33
- package/src/monitoring/__tests__/mount-watch.test.ts +236 -0
- package/src/monitoring/mount-watch.ts +348 -0
- package/src/monitoring/worker.ts +9 -0
- package/src/oauth/__tests__/seed-providers-managed.test.ts +95 -0
- package/src/oauth/connection-resolver.test.ts +27 -0
- package/src/oauth/connection-resolver.ts +25 -1
- package/src/oauth/provider-serializer.ts +9 -0
- package/src/oauth/seed-providers.ts +121 -2
- package/src/onboarding/checkin-event.ts +2 -1
- package/src/plugins/__tests__/installed-plugin-dirs.test.ts +14 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +38 -8
- package/src/plugins/external-plugin-loader.ts +49 -86
- package/src/plugins/installed-plugin-dirs.ts +6 -4
- package/src/plugins/mcp-servers.ts +9 -15
- package/src/plugins/mtime-cache.ts +2 -1
- package/src/plugins/user-loader.ts +2 -1
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +45 -0
- package/src/providers/inference/endpoint-probe.ts +27 -12
- package/src/providers/openai/responses-provider.ts +9 -2
- package/src/providers/opencode/client.test.ts +167 -0
- package/src/providers/opencode/client.ts +73 -4
- package/src/runtime/AGENTS.md +4 -0
- package/src/runtime/__tests__/desktop-stream-upgrade.test.ts +45 -2
- package/src/runtime/http-server.ts +6 -6
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +16 -5
- package/src/runtime/routes/__tests__/apps-refresh-route.test.ts +74 -4
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +25 -3
- package/src/runtime/routes/acp-claude-auth-routes.ts +11 -3
- package/src/runtime/routes/app-management-routes.ts +17 -2
- package/src/runtime/routes/browser-context.ts +39 -0
- package/src/runtime/routes/browser-routes.ts +16 -47
- package/src/runtime/routes/browser-tabs-routes.ts +72 -29
- package/src/runtime/routes/credential-routes.ts +1 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +16 -1
- package/src/runtime/routes/desktop-setup-routes.ts +7 -5
- package/src/runtime/routes/mcp-auth-routes.ts +106 -94
- package/src/runtime/routes/oauth-commands-routes.ts +14 -0
- package/src/runtime/routes/oauth-providers.ts +7 -0
- package/src/runtime/routes/plugins-routes.ts +14 -1
- package/src/runtime/sync/resource-sync-events.ts +4 -0
- package/src/tools/browser/browser-execution.ts +44 -13
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +69 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +5 -0
- package/src/tools/computer-use/definitions.ts +3 -4
- package/src/tools/ui-surface/channel-variants.ts +10 -59
- package/src/util/abort-reasons.ts +12 -1
- package/src/util/plugin-manifest.ts +203 -0
- package/src/watch/watch-retro.ts +6 -8
- package/src/desktop/desktop-feature.test.ts +0 -18
- package/src/desktop/desktop-feature.ts +0 -19
|
@@ -90,6 +90,13 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
90
90
|
}>;
|
|
91
91
|
appType?: string;
|
|
92
92
|
setupNotes?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* Per-tenant providers only: the host the user supplies at connect time,
|
|
95
|
+
* which the platform substitutes into `{tenant_host}` URL placeholders.
|
|
96
|
+
* `pattern` mirrors the platform registry's validation so clients can
|
|
97
|
+
* reject a malformed host before the request leaves the browser.
|
|
98
|
+
*/
|
|
99
|
+
tenantHost?: { pattern: string; label: string; placeholder: string };
|
|
93
100
|
identityUrl?: string;
|
|
94
101
|
identityMethod?: string;
|
|
95
102
|
identityHeaders?: Record<string, string>;
|
|
@@ -1003,7 +1010,12 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1003
1010
|
// default, because Figma rejects the whole authorization request if the
|
|
1004
1011
|
// app cannot grant a requested scope. `selections:read` is withheld for
|
|
1005
1012
|
// that same reason: it is not offered in the app's OAuth scope list, so
|
|
1006
|
-
// requesting it would fail the entire authorization.
|
|
1013
|
+
// requesting it would fail the entire authorization. `folders:read` is
|
|
1014
|
+
// withheld likewise: Figma documents it, but the app's scope picker only
|
|
1015
|
+
// offers `folder_metadata:read` under Folders. `file_code_connect:write`
|
|
1016
|
+
// is withheld because Figma only offers it to apps owned by an
|
|
1017
|
+
// Organization-plan team, which not every managed app is, and nothing
|
|
1018
|
+
// here uses Code Connect; it stays in availableScopes for BYO apps.
|
|
1007
1019
|
defaultScopes: [
|
|
1008
1020
|
"current_user:read",
|
|
1009
1021
|
"file_content:read",
|
|
@@ -1013,7 +1025,6 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1013
1025
|
"file_comments:write",
|
|
1014
1026
|
"file_dev_resources:read",
|
|
1015
1027
|
"file_dev_resources:write",
|
|
1016
|
-
"folders:read",
|
|
1017
1028
|
"folder_metadata:read",
|
|
1018
1029
|
"library_content:read",
|
|
1019
1030
|
"library_assets:read",
|
|
@@ -1053,6 +1064,10 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1053
1064
|
scope: "file_dev_resources:write",
|
|
1054
1065
|
description: "Write dev resources to files",
|
|
1055
1066
|
},
|
|
1067
|
+
{
|
|
1068
|
+
scope: "file_code_connect:write",
|
|
1069
|
+
description: "Write and change component code (Code Connect)",
|
|
1070
|
+
},
|
|
1056
1071
|
{
|
|
1057
1072
|
scope: "folders:read",
|
|
1058
1073
|
description: "List folders and files in folders",
|
|
@@ -1388,6 +1403,12 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1388
1403
|
"read_discounts",
|
|
1389
1404
|
"write_discounts",
|
|
1390
1405
|
"read_price_rules",
|
|
1406
|
+
// write_themes covers listing, duplicating, and publishing themes.
|
|
1407
|
+
// Writing theme files (settings, JSON templates, Liquid) also requires
|
|
1408
|
+
// Shopify's theme-code exemption on the app itself, which is granted
|
|
1409
|
+
// per app in the Shopify dashboard rather than through a scope.
|
|
1410
|
+
"read_themes",
|
|
1411
|
+
"write_themes",
|
|
1391
1412
|
],
|
|
1392
1413
|
availableScopes: [
|
|
1393
1414
|
{
|
|
@@ -1440,6 +1461,11 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1440
1461
|
scope: "write_price_rules",
|
|
1441
1462
|
description: "Create and update price rules",
|
|
1442
1463
|
},
|
|
1464
|
+
{ scope: "read_themes", description: "Read themes and theme files" },
|
|
1465
|
+
{
|
|
1466
|
+
scope: "write_themes",
|
|
1467
|
+
description: "Create, publish, and edit themes and theme files",
|
|
1468
|
+
},
|
|
1443
1469
|
{ scope: "read_files", description: "Read files uploaded to the store" },
|
|
1444
1470
|
{ scope: "write_files", description: "Upload and update files" },
|
|
1445
1471
|
{ scope: "read_gift_cards", description: "Read gift cards" },
|
|
@@ -1485,6 +1511,12 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1485
1511
|
],
|
|
1486
1512
|
loopbackPort: 17341,
|
|
1487
1513
|
managedServiceConfigKey: "shopify-oauth",
|
|
1514
|
+
// Mirrors `extra_config.tenant_host` in the platform provider registry.
|
|
1515
|
+
tenantHost: {
|
|
1516
|
+
pattern: "^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$",
|
|
1517
|
+
label: "Shop domain",
|
|
1518
|
+
placeholder: "your-store.myshopify.com",
|
|
1519
|
+
},
|
|
1488
1520
|
injectionTemplates: [
|
|
1489
1521
|
{
|
|
1490
1522
|
hostPattern: "*.myshopify.com",
|
|
@@ -1508,6 +1540,93 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
1508
1540
|
// cannot complete.
|
|
1509
1541
|
featureFlag: "shopify-oauth",
|
|
1510
1542
|
},
|
|
1543
|
+
|
|
1544
|
+
quickbooks: {
|
|
1545
|
+
provider: "quickbooks",
|
|
1546
|
+
authorizeUrl: "https://appcenter.intuit.com/connect/oauth2",
|
|
1547
|
+
tokenExchangeUrl:
|
|
1548
|
+
"https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
|
|
1549
|
+
// Access tokens live an hour; refresh tokens 100 days and are rotated on
|
|
1550
|
+
// every refresh, which the platform persists. Both token endpoints
|
|
1551
|
+
// authenticate the client with HTTP Basic.
|
|
1552
|
+
refreshUrl: "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
|
|
1553
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
1554
|
+
// No revokeUrl: Intuit's revocation endpoint wants a JSON body under HTTP
|
|
1555
|
+
// Basic client auth, and the daemon's revoke helper only posts an
|
|
1556
|
+
// unauthenticated form body, so a seeded URL would fail silently on every
|
|
1557
|
+
// your-own disconnect. The platform registry revokes managed tokens itself.
|
|
1558
|
+
// Every Accounting API path is scoped to the company (realm) the user
|
|
1559
|
+
// picked on Intuit's consent screen. The realm arrives only as the
|
|
1560
|
+
// callback's `realmId` query parameter; the platform captures it into the
|
|
1561
|
+
// connection's `provider_params` and fills the `{realm_id}` placeholder
|
|
1562
|
+
// server-side, so managed callers send paths relative to the company
|
|
1563
|
+
// (`/query`, `/customer/123`). Production and sandbox keys use different
|
|
1564
|
+
// hosts; the platform picks the host per environment.
|
|
1565
|
+
baseUrl: "https://quickbooks.api.intuit.com/v3/company/{realm_id}",
|
|
1566
|
+
// No pingUrl: the only company-independent probe would still need the
|
|
1567
|
+
// realm in its path, which the ping route cannot fill in (it sends the
|
|
1568
|
+
// URL's origin as a per-request base override, which would also pin a
|
|
1569
|
+
// managed sandbox connection to the production host).
|
|
1570
|
+
displayLabel: "QuickBooks",
|
|
1571
|
+
description: "Invoices, customers, vendors, and accounting data",
|
|
1572
|
+
dashboardUrl: "https://developer.intuit.com/app/developer/dashboard",
|
|
1573
|
+
clientIdPlaceholder: null,
|
|
1574
|
+
logoUrl: "https://cdn.simpleicons.org/quickbooks",
|
|
1575
|
+
// The Accounting scope covers the QuickBooks Online API. Payments is a
|
|
1576
|
+
// separate product the app must be enabled for, and the OpenID scopes
|
|
1577
|
+
// only add the signing-in user's profile, so none are on by default.
|
|
1578
|
+
defaultScopes: ["com.intuit.quickbooks.accounting"],
|
|
1579
|
+
availableScopes: [
|
|
1580
|
+
{
|
|
1581
|
+
scope: "com.intuit.quickbooks.accounting",
|
|
1582
|
+
description:
|
|
1583
|
+
"Read and write QuickBooks Online accounting data: customers, vendors, invoices, bills, payments, items, accounts, and reports",
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
scope: "com.intuit.quickbooks.payment",
|
|
1587
|
+
description:
|
|
1588
|
+
"QuickBooks Payments: charges, refunds, bank accounts, and cards (requires a Payments-enabled app)",
|
|
1589
|
+
},
|
|
1590
|
+
{ scope: "openid", description: "Sign in with Intuit (OpenID Connect)" },
|
|
1591
|
+
{ scope: "profile", description: "The signing-in user's name" },
|
|
1592
|
+
{ scope: "email", description: "The signing-in user's email address" },
|
|
1593
|
+
{ scope: "phone", description: "The signing-in user's phone number" },
|
|
1594
|
+
{ scope: "address", description: "The signing-in user's address" },
|
|
1595
|
+
],
|
|
1596
|
+
loopbackPort: 17342,
|
|
1597
|
+
managedServiceConfigKey: "quickbooks-oauth",
|
|
1598
|
+
injectionTemplates: [
|
|
1599
|
+
{
|
|
1600
|
+
hostPattern: "quickbooks.api.intuit.com",
|
|
1601
|
+
injectionType: "header",
|
|
1602
|
+
headerName: "Authorization",
|
|
1603
|
+
valuePrefix: "Bearer ",
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
hostPattern: "sandbox-quickbooks.api.intuit.com",
|
|
1607
|
+
injectionType: "header",
|
|
1608
|
+
headerName: "Authorization",
|
|
1609
|
+
valuePrefix: "Bearer ",
|
|
1610
|
+
},
|
|
1611
|
+
],
|
|
1612
|
+
appType: "App",
|
|
1613
|
+
setupNotes: [
|
|
1614
|
+
"QuickBooks scopes every Accounting API call to the company (realm) chosen on Intuit's consent screen. Managed connections carry the realm as provider_params.realm_id and requests are sent relative to /v3/company/{realmId}.",
|
|
1615
|
+
"Intuit development keys only authorize sandbox companies, which live on sandbox-quickbooks.api.intuit.com; production keys use quickbooks.api.intuit.com.",
|
|
1616
|
+
"The Accounting API returns XML unless the request carries Accept: application/json.",
|
|
1617
|
+
],
|
|
1618
|
+
// CompanyInfo does not repeat the realm (its Id is always "1"); the
|
|
1619
|
+
// platform keys the connection on the captured realm and labels it with
|
|
1620
|
+
// the company name.
|
|
1621
|
+
identityUrl:
|
|
1622
|
+
"https://quickbooks.api.intuit.com/v3/company/{realm_id}/companyinfo/{realm_id}",
|
|
1623
|
+
identityHeaders: { Accept: "application/json" },
|
|
1624
|
+
identityResponsePaths: ["CompanyInfo.CompanyName", "CompanyInfo.LegalName"],
|
|
1625
|
+
// Gated like figma/shopify: the platform side lands separately, and until
|
|
1626
|
+
// the client ids are live a visible tile would offer a connect flow that
|
|
1627
|
+
// cannot complete.
|
|
1628
|
+
featureFlag: "quickbooks-oauth",
|
|
1629
|
+
},
|
|
1511
1630
|
};
|
|
1512
1631
|
|
|
1513
1632
|
export const SEEDED_PROVIDER_KEYS = new Set(Object.keys(PROVIDER_SEED_DATA));
|
|
@@ -69,7 +69,8 @@ export function buildCheckinTitle({
|
|
|
69
69
|
* Google Calendar strips nearly all styling, so the body relies only on the
|
|
70
70
|
* tags that survive (`<p>`, `<strong>`, `<a>`, emoji) and makes the CTA a bold
|
|
71
71
|
* link rather than a styled button. The deep link opens a fresh conversation
|
|
72
|
-
* (`uuid`)
|
|
72
|
+
* (`uuid`) with the first-week prompt staged in the composer; the user sends
|
|
73
|
+
* it. A clicked link never auto-sends (`clients/web/src/utils/auto-send-prompt.ts`).
|
|
73
74
|
*/
|
|
74
75
|
export function buildCheckinDescription(uuid: string): string {
|
|
75
76
|
// `vref` is an app-owned attribution param, NOT a marketing `utm_*`: the
|
|
@@ -45,10 +45,23 @@ beforeEach(() => {
|
|
|
45
45
|
describe("listInstalledPluginDirs", () => {
|
|
46
46
|
test("lists real plugin directories and skips dot entries", () => {
|
|
47
47
|
makePluginDir(pluginsDir, "alpha");
|
|
48
|
+
const standard = join(pluginsDir, "standard");
|
|
49
|
+
mkdirSync(standard, { recursive: true });
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(standard, "plugin.json"),
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
54
|
+
name: "standard",
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
48
57
|
makePluginDir(pluginsDir, ".hidden");
|
|
49
58
|
mkdirSync(join(pluginsDir, "no-manifest"), { recursive: true });
|
|
50
59
|
|
|
51
|
-
expect(
|
|
60
|
+
expect(
|
|
61
|
+
listInstalledPluginDirs()
|
|
62
|
+
.map((p) => p.name)
|
|
63
|
+
.sort(),
|
|
64
|
+
).toEqual(["alpha", "standard"]);
|
|
52
65
|
});
|
|
53
66
|
|
|
54
67
|
test("skips a symlinked plugin root that points outside the plugins dir", () => {
|
|
@@ -24,17 +24,27 @@ import {
|
|
|
24
24
|
function makePluginsDir(
|
|
25
25
|
plugins: Record<
|
|
26
26
|
string,
|
|
27
|
-
{
|
|
27
|
+
{
|
|
28
|
+
mcpJson?: string;
|
|
29
|
+
packageJson?: string | null;
|
|
30
|
+
pluginJson?: string;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
}
|
|
28
33
|
>,
|
|
29
34
|
): string {
|
|
30
35
|
const root = mkdtempSync(join(tmpdir(), "vellum-plugin-mcp-"));
|
|
31
36
|
for (const [name, spec] of Object.entries(plugins)) {
|
|
32
37
|
const dir = join(root, name);
|
|
33
38
|
mkdirSync(dir, { recursive: true });
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
if (spec.packageJson !== null) {
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(dir, "package.json"),
|
|
42
|
+
spec.packageJson ?? JSON.stringify({ name, version: "1.0.0" }),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (spec.pluginJson !== undefined) {
|
|
46
|
+
writeFileSync(join(dir, "plugin.json"), spec.pluginJson);
|
|
47
|
+
}
|
|
38
48
|
if (spec.mcpJson !== undefined) {
|
|
39
49
|
writeFileSync(join(dir, "mcp.json"), spec.mcpJson);
|
|
40
50
|
}
|
|
@@ -137,6 +147,26 @@ describe("readPluginMcpServers", () => {
|
|
|
137
147
|
});
|
|
138
148
|
});
|
|
139
149
|
|
|
150
|
+
test("reads MCP servers from a standard-only plugin", () => {
|
|
151
|
+
const dir = makePluginsDir({
|
|
152
|
+
unabyss: {
|
|
153
|
+
packageJson: null,
|
|
154
|
+
pluginJson: JSON.stringify({
|
|
155
|
+
$schema: "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
156
|
+
name: "unabyss",
|
|
157
|
+
}),
|
|
158
|
+
mcpJson: VALID_MANIFEST,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const { servers, issues } = readPluginMcpServers({
|
|
163
|
+
workspacePluginsDir: dir,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(issues).toEqual([]);
|
|
167
|
+
expect(servers.map((server) => server.id)).toEqual(["unabyss"]);
|
|
168
|
+
});
|
|
169
|
+
|
|
140
170
|
test("defaults an omitted type to streamable-http", () => {
|
|
141
171
|
const dir = makePluginsDir({
|
|
142
172
|
openseo: {
|
|
@@ -205,7 +235,7 @@ describe("readPluginMcpServers", () => {
|
|
|
205
235
|
workspacePluginsDir: root,
|
|
206
236
|
});
|
|
207
237
|
expect(servers).toEqual([]);
|
|
208
|
-
expect(issues[0].message).toContain("
|
|
238
|
+
expect(issues[0].message).toContain("selected plugin manifest");
|
|
209
239
|
});
|
|
210
240
|
|
|
211
241
|
test("ignores a directory whose package.json is unparseable", () => {
|
|
@@ -216,7 +246,7 @@ describe("readPluginMcpServers", () => {
|
|
|
216
246
|
workspacePluginsDir: dir,
|
|
217
247
|
});
|
|
218
248
|
expect(servers).toEqual([]);
|
|
219
|
-
expect(issues[0].message).toContain("
|
|
249
|
+
expect(issues[0].message).toContain("selected plugin manifest");
|
|
220
250
|
});
|
|
221
251
|
|
|
222
252
|
test("ignores a directory whose package.json has no name", () => {
|
|
@@ -230,7 +260,7 @@ describe("readPluginMcpServers", () => {
|
|
|
230
260
|
workspacePluginsDir: dir,
|
|
231
261
|
});
|
|
232
262
|
expect(servers).toEqual([]);
|
|
233
|
-
expect(issues[0].message).toContain("
|
|
263
|
+
expect(issues[0].message).toContain("selected plugin manifest");
|
|
234
264
|
});
|
|
235
265
|
|
|
236
266
|
test("skips plugins with no mcp.json without complaining", () => {
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* need to be renamed when the convention shifts.
|
|
11
11
|
*
|
|
12
12
|
* <pluginDir>/
|
|
13
|
-
* package.json ← manifest
|
|
14
|
-
* (npm scope stripped);
|
|
13
|
+
* package.json ← legacy Vellum manifest, selected first;
|
|
15
14
|
* peerDependencies["@vellumai/plugin-api"]
|
|
16
15
|
* semver range is checked against the
|
|
17
16
|
* running assistant version and rejects
|
|
18
17
|
* the plugin if unsatisfied
|
|
18
|
+
* plugin.json ← standard fallback when package.json is absent
|
|
19
19
|
* hooks/
|
|
20
20
|
* <name>.ts ← default export → plugin.hooks[<name>]
|
|
21
21
|
* (today the runtime invokes "init" at
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
43
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
44
|
-
import { readFile } from "node:fs/promises";
|
|
45
44
|
import { basename, join } from "node:path";
|
|
46
45
|
import { pathToFileURL } from "node:url";
|
|
47
46
|
|
|
@@ -53,6 +52,10 @@ import { PLUGIN_SECRET_PATTERN_LIMITS } from "../security/plugin-secret-patterns
|
|
|
53
52
|
import { finalizeTool } from "../tools/tool-defaults.js";
|
|
54
53
|
import type { Tool, ToolDefinition } from "../tools/types.js";
|
|
55
54
|
import { getLogger } from "../util/logger.js";
|
|
55
|
+
import {
|
|
56
|
+
LEGACY_PLUGIN_MANIFEST,
|
|
57
|
+
readPluginManifest,
|
|
58
|
+
} from "../util/plugin-manifest.js";
|
|
56
59
|
import { registerPlugin } from "./registry.js";
|
|
57
60
|
import type {
|
|
58
61
|
HookFunction,
|
|
@@ -69,39 +72,12 @@ const log = getLogger("external-plugin-loader");
|
|
|
69
72
|
/** Default upper bound on how long a single plugin load may take. */
|
|
70
73
|
const DEFAULT_IMPORT_TIMEOUT_MS = 10_000;
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* against the running assistant version and rejects the plugin on
|
|
79
|
-
* mismatch. If absent, the plugin loads without a host-compat claim
|
|
80
|
-
* (with a warning).
|
|
81
|
-
* - `credentialKeyPatterns` is typed `unknown` here and shape-validated
|
|
82
|
-
* separately ({@link parseCredentialKeyPatterns}) so a malformed
|
|
83
|
-
* declaration degrades to `undefined` instead of failing the whole
|
|
84
|
-
* `safeParse` and blocking plugin load.
|
|
85
|
-
* - Unknown fields pass through (`passthrough`) so the loader does not
|
|
86
|
-
* destructively reshape the file when the rest of the npm ecosystem
|
|
87
|
-
* writes to it.
|
|
88
|
-
*/
|
|
89
|
-
const PluginPackageJsonSchema = z
|
|
90
|
-
.object({
|
|
91
|
-
name: z.string().min(1, "package.json `name` must be a non-empty string"),
|
|
92
|
-
version: z.string().optional(),
|
|
93
|
-
peerDependencies: z.record(z.string(), z.string()).optional(),
|
|
94
|
-
credentialKeyPatterns: z.unknown().optional(),
|
|
95
|
-
/** Human title, when the package name is not one ("@vellumai/imessage"). */
|
|
96
|
-
displayName: z.string().min(1).optional(),
|
|
97
|
-
/** Standard npm field, reused as the one-line description clients show. */
|
|
98
|
-
description: z.string().min(1).optional(),
|
|
99
|
-
/** Lucide icon name without the `lucide-` prefix, matching `ChannelInfo`. */
|
|
100
|
-
icon: z.string().min(1).optional(),
|
|
101
|
-
})
|
|
102
|
-
.passthrough();
|
|
103
|
-
|
|
104
|
-
type PluginPackageJson = z.infer<typeof PluginPackageJsonSchema>;
|
|
75
|
+
interface LegacyPluginFields extends Record<string, unknown> {
|
|
76
|
+
readonly peerDependencies?: Record<string, string>;
|
|
77
|
+
readonly credentialKeyPatterns?: unknown;
|
|
78
|
+
readonly displayName?: string;
|
|
79
|
+
readonly icon?: string;
|
|
80
|
+
}
|
|
105
81
|
|
|
106
82
|
/**
|
|
107
83
|
* Shape-level caps for the `credentialKeyPatterns` manifest field. The
|
|
@@ -270,23 +246,8 @@ async function loadHooks(
|
|
|
270
246
|
* timeout/try-catch/register triple.
|
|
271
247
|
*/
|
|
272
248
|
async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
rawPkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
277
|
-
} catch (err) {
|
|
278
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
279
|
-
throw new Error(
|
|
280
|
-
`package.json at ${pluginDir} could not be read or parsed: ${reason}`,
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
const parsed = PluginPackageJsonSchema.safeParse(rawPkg);
|
|
284
|
-
if (!parsed.success) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
`package.json at ${pluginDir} failed schema validation: ${parsed.error.message}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
const pkg: PluginPackageJson = parsed.data;
|
|
249
|
+
const resolvedManifest = readPluginManifest(pluginDir);
|
|
250
|
+
const legacyFields = resolvedManifest.raw as LegacyPluginFields;
|
|
290
251
|
// A plugin's identity is its install directory name — the slug the plugin
|
|
291
252
|
// was installed under (a marketplace slug or a GitHub path leaf). This is
|
|
292
253
|
// the identity every other surface uses: `plugins list`, enable/disable,
|
|
@@ -296,7 +257,10 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
296
257
|
// registry from the identity the user toggles and the scope filters match.
|
|
297
258
|
// The manifest is still required (schema-validated above) as the load gate.
|
|
298
259
|
const name = basename(pluginDir);
|
|
299
|
-
const version =
|
|
260
|
+
const version =
|
|
261
|
+
resolvedManifest.version && resolvedManifest.version.length > 0
|
|
262
|
+
? resolvedManifest.version
|
|
263
|
+
: "0.0.0";
|
|
300
264
|
|
|
301
265
|
// Host-compat negotiation: plugins declare their plugin-api version
|
|
302
266
|
// range via standard `peerDependencies["@vellumai/plugin-api"]`. We
|
|
@@ -310,7 +274,7 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
310
274
|
//
|
|
311
275
|
// If the peerDep is absent, the plugin loads without a host-compat
|
|
312
276
|
// claim; we log a warning so the omission is visible at boot.
|
|
313
|
-
const range =
|
|
277
|
+
const range = legacyFields.peerDependencies?.[PLUGIN_API_PEER_DEP];
|
|
314
278
|
if (range !== undefined) {
|
|
315
279
|
if (!semver.validRange(range)) {
|
|
316
280
|
log.error(
|
|
@@ -333,7 +297,7 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
333
297
|
`external plugin ${name}: peerDependencies["${PLUGIN_API_PEER_DEP}"] requires "${range}" but assistant is ${assistantPkg.version} — loading anyway`,
|
|
334
298
|
);
|
|
335
299
|
}
|
|
336
|
-
} else {
|
|
300
|
+
} else if (resolvedManifest.source === LEGACY_PLUGIN_MANIFEST) {
|
|
337
301
|
log.warn(
|
|
338
302
|
{ pluginDir, plugin: name, peerDep: PLUGIN_API_PEER_DEP },
|
|
339
303
|
"external plugin missing plugin-api peerDependency — loading without host-compat claim",
|
|
@@ -342,7 +306,9 @@ async function buildPluginFromDir(pluginDir: string): Promise<Plugin> {
|
|
|
342
306
|
|
|
343
307
|
const manifest: PluginManifest = { name, version };
|
|
344
308
|
const credentialKeyPatterns = parseCredentialKeyPatterns(
|
|
345
|
-
|
|
309
|
+
resolvedManifest.source === LEGACY_PLUGIN_MANIFEST
|
|
310
|
+
? legacyFields.credentialKeyPatterns
|
|
311
|
+
: undefined,
|
|
346
312
|
pluginDir,
|
|
347
313
|
);
|
|
348
314
|
if (credentialKeyPatterns !== undefined) {
|
|
@@ -459,9 +425,9 @@ export async function loadExternalPlugin(
|
|
|
459
425
|
}
|
|
460
426
|
|
|
461
427
|
/**
|
|
462
|
-
* Parse a plugin's
|
|
428
|
+
* Parse a plugin's selected manifest from disk. Returns the plugin
|
|
463
429
|
* identity (its install directory name), version, and any declared
|
|
464
|
-
* `credentialKeyPatterns`, or `undefined` when the
|
|
430
|
+
* legacy `credentialKeyPatterns`, or `undefined` when the manifest is
|
|
465
431
|
* missing, unparseable, or fails schema validation.
|
|
466
432
|
*
|
|
467
433
|
* Exported so the mtime cache can discover plugin identity without going
|
|
@@ -497,20 +463,23 @@ export interface PluginPresentation {
|
|
|
497
463
|
export async function parsePluginPresentation(
|
|
498
464
|
pluginDir: string,
|
|
499
465
|
): Promise<PluginPresentation | undefined> {
|
|
500
|
-
let rawPkg: unknown;
|
|
501
466
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
467
|
+
const manifest = readPluginManifest(pluginDir);
|
|
468
|
+
const legacyFields = manifest.raw as LegacyPluginFields;
|
|
469
|
+
return {
|
|
470
|
+
displayName:
|
|
471
|
+
manifest.source === LEGACY_PLUGIN_MANIFEST
|
|
472
|
+
? legacyFields.displayName
|
|
473
|
+
: manifest.name,
|
|
474
|
+
description: manifest.description,
|
|
475
|
+
icon:
|
|
476
|
+
manifest.source === LEGACY_PLUGIN_MANIFEST
|
|
477
|
+
? legacyFields.icon
|
|
478
|
+
: undefined,
|
|
479
|
+
};
|
|
505
480
|
} catch {
|
|
506
481
|
return undefined;
|
|
507
482
|
}
|
|
508
|
-
const parsed = PluginPackageJsonSchema.safeParse(rawPkg);
|
|
509
|
-
if (!parsed.success) {
|
|
510
|
-
return undefined;
|
|
511
|
-
}
|
|
512
|
-
const { displayName, description, icon } = parsed.data;
|
|
513
|
-
return { displayName, description, icon };
|
|
514
483
|
}
|
|
515
484
|
|
|
516
485
|
export async function parsePluginManifest(
|
|
@@ -519,35 +488,29 @@ export async function parsePluginManifest(
|
|
|
519
488
|
): Promise<
|
|
520
489
|
Pick<PluginManifest, "name" | "version" | "credentialKeyPatterns"> | undefined
|
|
521
490
|
> {
|
|
522
|
-
|
|
523
|
-
let rawPkg: unknown;
|
|
491
|
+
let resolvedManifest: ReturnType<typeof readPluginManifest>;
|
|
524
492
|
try {
|
|
525
|
-
|
|
493
|
+
resolvedManifest = readPluginManifest(pluginDir);
|
|
526
494
|
} catch (err) {
|
|
527
495
|
const reason = err instanceof Error ? err.message : String(err);
|
|
528
496
|
if (!opts.quiet) {
|
|
529
497
|
log.error(
|
|
530
498
|
{ err, pluginDir },
|
|
531
|
-
`
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
return undefined;
|
|
535
|
-
}
|
|
536
|
-
const parsed = PluginPackageJsonSchema.safeParse(rawPkg);
|
|
537
|
-
if (!parsed.success) {
|
|
538
|
-
if (!opts.quiet) {
|
|
539
|
-
log.error(
|
|
540
|
-
{ err: parsed.error, pluginDir },
|
|
541
|
-
`package.json at ${pluginDir} failed schema validation: ${parsed.error.message}`,
|
|
499
|
+
`Plugin manifest at ${pluginDir} could not be read or validated: ${reason}`,
|
|
542
500
|
);
|
|
543
501
|
}
|
|
544
502
|
return undefined;
|
|
545
503
|
}
|
|
546
|
-
const
|
|
504
|
+
const legacyFields = resolvedManifest.raw as LegacyPluginFields;
|
|
547
505
|
const name = basename(pluginDir);
|
|
548
|
-
const version =
|
|
506
|
+
const version =
|
|
507
|
+
resolvedManifest.version && resolvedManifest.version.length > 0
|
|
508
|
+
? resolvedManifest.version
|
|
509
|
+
: "0.0.0";
|
|
549
510
|
const credentialKeyPatterns = parseCredentialKeyPatterns(
|
|
550
|
-
|
|
511
|
+
resolvedManifest.source === LEGACY_PLUGIN_MANIFEST
|
|
512
|
+
? legacyFields.credentialKeyPatterns
|
|
513
|
+
: undefined,
|
|
551
514
|
pluginDir,
|
|
552
515
|
);
|
|
553
516
|
if (credentialKeyPatterns !== undefined) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Enumerate installed plugin directories under the workspace plugins dir.
|
|
3
3
|
*
|
|
4
|
-
* An installed plugin is a non-hidden directory carrying a `package.json
|
|
5
|
-
* whose realpath stays under the realpath of the plugins
|
|
4
|
+
* An installed plugin is a non-hidden directory carrying a `package.json` or
|
|
5
|
+
* root `plugin.json` whose realpath stays under the realpath of the plugins
|
|
6
|
+
* directory. This walk
|
|
6
7
|
* is shared by the plugin source-version collector
|
|
7
8
|
* (`./collect-source-versions.ts`) and the schedule reconciler
|
|
8
9
|
* (`../schedule/plugin-schedule-reconciler.ts`) so both agree on what counts
|
|
@@ -17,10 +18,11 @@
|
|
|
17
18
|
* declaration is available to run.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import { readdirSync, realpathSync, statSync } from "node:fs";
|
|
21
22
|
import { join, sep } from "node:path";
|
|
22
23
|
|
|
23
24
|
import { getWorkspacePluginsDir } from "../util/platform.js";
|
|
25
|
+
import { hasPluginManifest } from "../util/plugin-manifest.js";
|
|
24
26
|
|
|
25
27
|
export interface InstalledPluginDir {
|
|
26
28
|
/** Directory basename: the plugin's install identity. */
|
|
@@ -92,7 +94,7 @@ export function listInstalledPluginDirs(): InstalledPluginDir[] {
|
|
|
92
94
|
if (!isInsidePluginRoot(dir, pluginsDir)) {
|
|
93
95
|
continue;
|
|
94
96
|
}
|
|
95
|
-
if (!
|
|
97
|
+
if (!hasPluginManifest(dir)) {
|
|
96
98
|
continue;
|
|
97
99
|
}
|
|
98
100
|
out.push({ name: entry, dir });
|
|
@@ -21,19 +21,11 @@
|
|
|
21
21
|
* disables only that entry. Neither ever throws, because a malformed file
|
|
22
22
|
* in one plugin must not remove another plugin's servers from a listing.
|
|
23
23
|
*
|
|
24
|
-
* Authentication is
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Consumers must not resolve a plugin server's id against the
|
|
31
|
-
* `mcp:<serverId>:*` credential namespace. Those keys belong to
|
|
32
|
-
* workspace-configured servers, and a plugin controls both its server key
|
|
33
|
-
* and its URL, so honoring them for a plugin-declared server would send a
|
|
34
|
-
* workspace credential to an endpoint the plugin chose. Every config built
|
|
35
|
-
* here carries `source: "plugin"`, which is what `McpClient` reads to
|
|
36
|
-
* decide, so the rule travels with the server rather than with the caller.
|
|
24
|
+
* Authentication is client-managed. Agent Plugins 1.0.0 defines no portable
|
|
25
|
+
* OAuth or credential-reference fields, and any `headers` in the file are
|
|
26
|
+
* literal package data. Runtime configs carry the declaring plugin directory
|
|
27
|
+
* name and original server key so OAuth credentials can be isolated from the
|
|
28
|
+
* public server id and from workspace credentials.
|
|
37
29
|
*/
|
|
38
30
|
|
|
39
31
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -141,7 +133,7 @@ export function interpolatePluginPaths(
|
|
|
141
133
|
*
|
|
142
134
|
* `listAllPlugins` is an inventory of directories and reports a malformed
|
|
143
135
|
* entry rather than dropping it, so it happily returns a directory with no
|
|
144
|
-
* usable
|
|
136
|
+
* usable selected manifest. The runtime loader rejects those in
|
|
145
137
|
* `parsePluginManifest`: the manifest must parse and carry a non-empty
|
|
146
138
|
* `name`. Applying the same gate here keeps `mcp.json` from being honored
|
|
147
139
|
* for a directory that will never load as a plugin.
|
|
@@ -192,7 +184,7 @@ export function readPluginMcpServers(
|
|
|
192
184
|
if (!hasLoadableManifest(plugin)) {
|
|
193
185
|
issues.push({
|
|
194
186
|
pluginName: plugin.name,
|
|
195
|
-
message: `${PLUGIN_MCP_MANIFEST} ignored:
|
|
187
|
+
message: `${PLUGIN_MCP_MANIFEST} ignored: the selected plugin manifest is missing or invalid, so the runtime will not load this directory as a plugin`,
|
|
196
188
|
});
|
|
197
189
|
continue;
|
|
198
190
|
}
|
|
@@ -257,6 +249,8 @@ export function readPluginMcpServers(
|
|
|
257
249
|
config: {
|
|
258
250
|
transport: projectTransport(entry.data, plugin.target),
|
|
259
251
|
source: "plugin",
|
|
252
|
+
pluginName: plugin.name,
|
|
253
|
+
serverKey,
|
|
260
254
|
},
|
|
261
255
|
});
|
|
262
256
|
}
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getWorkspaceHooksDir,
|
|
60
60
|
getWorkspacePluginsDir,
|
|
61
61
|
} from "../util/platform.js";
|
|
62
|
+
import { hasPluginManifest } from "../util/plugin-manifest.js";
|
|
62
63
|
import { collectSourceVersions } from "./collect-source-versions.js";
|
|
63
64
|
import {
|
|
64
65
|
deriveToolName,
|
|
@@ -768,7 +769,7 @@ async function scanPlugins(): Promise<void> {
|
|
|
768
769
|
);
|
|
769
770
|
continue;
|
|
770
771
|
}
|
|
771
|
-
if (!
|
|
772
|
+
if (!hasPluginManifest(pluginDir)) {
|
|
772
773
|
continue;
|
|
773
774
|
}
|
|
774
775
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* User plugin loader — discovers plugins under `<workspaceDir>/plugins/*` and
|
|
3
3
|
* populates the mtime cache.
|
|
4
4
|
*
|
|
5
|
-
* A plugin directory is recognized by a `package.json`
|
|
5
|
+
* A plugin directory is recognized by a legacy `package.json` or standard
|
|
6
|
+
* `plugin.json` manifest. The loader
|
|
6
7
|
* delegates to the mtime cache's `populateCacheAtBoot`, which builds each
|
|
7
8
|
* plugin via `buildExternalPlugin` and stores it keyed by mtime. The cache
|
|
8
9
|
* is the single source of truth for user plugin state — the registry is no
|