@jini-ai/agent-plugins 0.6.17 → 0.6.19
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/CHANGELOG.md +29 -1
- package/NOTICE +14 -0
- package/README.md +35 -14
- package/charts-echarts/plugin.json +39 -0
- package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
- package/charts-recharts/plugin.json +32 -0
- package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
- package/cloudflare/NOTES.md +16 -0
- package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
- package/cloudflare/lib/api.mjs +96 -0
- package/cloudflare/lib/assets.mjs +65 -0
- package/cloudflare/lib/dns.mjs +32 -0
- package/cloudflare/lib/domain-inventory.mjs +61 -0
- package/cloudflare/lib/domain-status.mjs +48 -0
- package/cloudflare/lib/domains.mjs +95 -0
- package/cloudflare/lib/forms.mjs +254 -0
- package/cloudflare/lib/pages-upload.mjs +51 -0
- package/cloudflare/lib/target.mjs +165 -0
- package/cloudflare/lib/workers-upload.mjs +48 -0
- package/cloudflare/lib/zones.mjs +25 -0
- package/cloudflare/mcp.json +4 -0
- package/cloudflare/plugin.json +96 -0
- package/cloudflare/skills/cloudflare/SKILL.md +57 -0
- package/cloudflare/skills/cloudflare/references/domains.md +31 -0
- package/cloudflare/skills/cloudflare/references/pages.md +18 -0
- package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
- package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
- package/cloudflare/skills/cloudflare/references/workers.md +42 -0
- package/cloudflare/targets/cloudflare-pages.mjs +12 -0
- package/cloudflare/targets/cloudflare-workers.mjs +17 -0
- package/cloudflare/tovu-consent.json +6 -0
- package/cloudflare/tovu-deploy-targets.json +630 -0
- package/cloudflare/tovu-form-destinations.json +12 -0
- package/cloudflare/worker/forms-receiver.mjs +249 -0
- package/composio/mcp.json +10 -0
- package/composio/plugin.json +68 -0
- package/composio/skills/composio/SKILL.md +85 -0
- package/dist/core/identity-namespace.d.ts +12 -0
- package/dist/core/identity-namespace.js +20 -0
- package/dist/folder-zip-node.js +29 -7
- package/dist/lifecycle/bundled-digests.js +4 -3
- package/dist/lifecycle/install.d.ts +7 -0
- package/dist/lifecycle/install.js +45 -3
- package/dist/lifecycle/manifest.d.ts +2 -2
- package/dist/lifecycle/manifest.js +5 -3
- package/dist/lifecycle/node.js +2 -0
- package/dist/lifecycle/persistent-state.js +1 -1
- package/dist/lifecycle/ports.d.ts +8 -0
- package/dist/lifecycle/seed-bundled.js +4 -1
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +3 -2
- package/fly/deploy-configs/fly.mjs +115 -0
- package/fly/deploy-ops/fly.mjs +102 -0
- package/fly/mcp.json +4 -0
- package/fly/plugin.json +81 -0
- package/fly/scripts/render-deploy-workflow.mjs +150 -0
- package/fly/skills/fly/SKILL.md +43 -0
- package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
- package/fly/skills/fly/references/fly.template.toml +81 -0
- package/fly/tovu-credential-schemes.json +11 -0
- package/fly/tovu-deploy-configs.json +21 -0
- package/fly/tovu-deploy-ops.json +14 -0
- package/github/deploy-ops/github-actions.mjs +164 -0
- package/github/deployments/github.mjs +415 -0
- package/github/mcp.json +4 -0
- package/github/plugin.json +105 -0
- package/github/skills/github/SKILL.md +249 -0
- package/github/skills/github/references/actions.md +224 -0
- package/github/skills/github/references/auth-and-tokens.md +140 -0
- package/github/skills/github/references/repo-files.md +135 -0
- package/github/skills/github/references/site-backup.md +159 -0
- package/github/skills/github/references/source-control.md +123 -0
- package/github/source-control/backup-push.mjs +242 -0
- package/github/source-control/commit-site.mjs +459 -0
- package/github/source-control/github.mjs +93 -0
- package/github/source-control/write-files.mjs +468 -0
- package/github/tovu-deploy-ops.json +13 -0
- package/github/tovu-source-control.json +117 -0
- package/github-pages/NOTES.md +19 -0
- package/github-pages/mcp.json +4 -0
- package/github-pages/plugin.json +48 -0
- package/github-pages/skills/github-pages/SKILL.md +44 -0
- package/github-pages/targets/github-pages.mjs +543 -0
- package/github-pages/tovu-deploy-targets.json +327 -0
- package/google-login/exports/google-g.svg +1 -0
- package/google-login/exports/sign-in-button.css +68 -0
- package/google-login/exports/sign-in-button.html +4 -0
- package/google-login/mcp.json +4 -0
- package/google-login/plugin.json +59 -0
- package/google-login/skills/google-login/SKILL.md +96 -0
- package/google-login/tovu-login-providers.json +21 -0
- package/google-workspace/README.md +132 -0
- package/google-workspace/form-destinations/README.md +87 -0
- package/google-workspace/form-destinations/dedupe.mjs +43 -0
- package/google-workspace/form-destinations/delivery-result.mjs +69 -0
- package/google-workspace/form-destinations/mapping.mjs +152 -0
- package/google-workspace/form-destinations/sheets.mjs +202 -0
- package/google-workspace/lib/auth.mjs +81 -0
- package/google-workspace/lib/drive-files.mjs +54 -0
- package/google-workspace/lib/form-destination-settings.mjs +72 -0
- package/google-workspace/lib/http.mjs +168 -0
- package/google-workspace/lib/picker-flow.mjs +171 -0
- package/google-workspace/lib/picker-selection.mjs +107 -0
- package/google-workspace/lib/product-state.mjs +93 -0
- package/google-workspace/lib/request-policy.mjs +163 -0
- package/google-workspace/lib/sheets-a1.mjs +79 -0
- package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
- package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
- package/google-workspace/lib/sheets-client.mjs +286 -0
- package/google-workspace/lib/surface-actions.mjs +107 -0
- package/google-workspace/mcp.json +4 -0
- package/google-workspace/plugin.json +77 -0
- package/google-workspace/products.json +12 -0
- package/google-workspace/skills/google-workspace/SKILL.md +82 -0
- package/google-workspace/skills/google-workspace/references/connection.md +102 -0
- package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
- package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
- package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
- package/google-workspace/tools/account-info.mjs +18 -0
- package/google-workspace/tools/sheets/README.md +54 -0
- package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
- package/google-workspace/tools/sheets/copy.mjs +23 -0
- package/google-workspace/tools/sheets/create.mjs +35 -0
- package/google-workspace/tools/sheets/find-replace.mjs +38 -0
- package/google-workspace/tools/sheets/find.mjs +52 -0
- package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
- package/google-workspace/tools/sheets/get.mjs +22 -0
- package/google-workspace/tools/sheets/values-append.mjs +27 -0
- package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
- package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
- package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
- package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
- package/google-workspace/tools/sheets/values-clear.mjs +20 -0
- package/google-workspace/tools/sheets/values-get.mjs +21 -0
- package/google-workspace/tools/sheets/values-update.mjs +26 -0
- package/google-workspace/tools/sheets-admin/README.md +45 -0
- package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
- package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
- package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
- package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
- package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
- package/google-workspace/tools/sheets-admin/format.mjs +70 -0
- package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
- package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
- package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
- package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
- package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
- package/google-workspace/tovu-admin-surfaces.json +63 -0
- package/google-workspace/tovu-agent-tools.json +1178 -0
- package/google-workspace/tovu-connections.json +35 -0
- package/google-workspace/tovu-form-destinations.json +10 -0
- package/google-workspace/ui/README.md +73 -0
- package/google-workspace/ui/connection-panel.handler.mjs +167 -0
- package/google-workspace/ui/connection-panel.html +17 -0
- package/google-workspace/ui/connection-panel.mjs +189 -0
- package/google-workspace/ui/connection-view.mjs +120 -0
- package/google-workspace/ui/destination-panel.handler.mjs +106 -0
- package/google-workspace/ui/destination-panel.html +17 -0
- package/google-workspace/ui/destination-panel.mjs +150 -0
- package/google-workspace/ui/dom.mjs +140 -0
- package/google-workspace/ui/panel.css +170 -0
- package/google-workspace/ui/picker-flow.mjs +87 -0
- package/google-workspace/ui/picker-view.mjs +44 -0
- package/google-workspace/ui/picker.handler.mjs +56 -0
- package/google-workspace/ui/picker.html +17 -0
- package/google-workspace/ui/picker.mjs +88 -0
- package/google-workspace/ui/surface-client.mjs +227 -0
- package/higgsfield-media/mcp.json +9 -0
- package/higgsfield-media/plugin.json +109 -0
- package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
- package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
- package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
- package/jev/mcp.json +4 -0
- package/jev/plugin.json +44 -0
- package/jev/skills/jev/SKILL.md +308 -0
- package/jev/skills/jev/references/cookbooks.md +197 -0
- package/jev/skills/jev/references/jaggedness.md +135 -0
- package/jev/skills/jev/references/javascript-sdk.md +146 -0
- package/jev/skills/jev/references/patterns.md +123 -0
- package/jev/skills/jev/references/primitives-reference.md +187 -0
- package/langfuse/plugin.json +22 -0
- package/meilisearch/mcp.json +4 -0
- package/meilisearch/plugin.json +54 -0
- package/meilisearch/providers/meilisearch.mjs +651 -0
- package/meilisearch/skills/meilisearch/SKILL.md +43 -0
- package/meilisearch/tovu-search-providers.json +10 -0
- package/netlify/NOTES.md +13 -0
- package/netlify/mcp.json +4 -0
- package/netlify/plugin.json +47 -0
- package/netlify/skills/netlify/SKILL.md +39 -0
- package/netlify/targets/netlify.mjs +384 -0
- package/netlify/tovu-consent.json +6 -0
- package/netlify/tovu-deploy-targets.json +163 -0
- package/opentelemetry/mcp.json +4 -0
- package/opentelemetry/plugin.json +52 -0
- package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
- package/package.json +161 -6
- package/posthog/mcp.json +4 -0
- package/posthog/plugin.json +46 -0
- package/posthog/skills/posthog/SKILL.md +36 -0
- package/railway/deploy-configs/railway.mjs +135 -0
- package/railway/deploy-ops/railway.mjs +167 -0
- package/railway/mcp.json +4 -0
- package/railway/plugin.json +45 -0
- package/railway/skills/railway/SKILL.md +66 -0
- package/railway/tovu-deploy-configs.json +16 -0
- package/railway/tovu-deploy-ops.json +11 -0
- package/render/deploy-configs/render.mjs +141 -0
- package/render/deploy-ops/render.mjs +152 -0
- package/render/mcp.json +4 -0
- package/render/plugin.json +47 -0
- package/render/skills/render/SKILL.md +63 -0
- package/render/tovu-deploy-configs.json +16 -0
- package/render/tovu-deploy-ops.json +11 -0
- package/resend/NOTES.md +52 -0
- package/resend/adapters/resend.mjs +161 -0
- package/resend/lib/domains.mjs +137 -0
- package/resend/lib/errors.mjs +25 -0
- package/resend/lib/feedback.mjs +118 -0
- package/resend/mcp.json +13 -0
- package/resend/plugin.json +192 -0
- package/resend/skills/resend/SKILL.md +107 -0
- package/resend/tovu-mail-adapters.json +22 -0
- package/s3-compatible/NOTES.md +11 -0
- package/s3-compatible/mcp.json +4 -0
- package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
- package/s3-compatible/plugin.json +56 -0
- package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
- package/s3-compatible/targets/s3-compatible.mjs +1054 -0
- package/s3-compatible/tovu-deploy-targets.json +432 -0
- package/s3-compatible/tovu-object-stores.json +29 -0
- package/sentry/mcp.json +4 -0
- package/sentry/plugin.json +49 -0
- package/sentry/skills/sentry/SKILL.md +34 -0
- package/shopify/mcp.json +4 -0
- package/shopify/plugin.json +22 -0
- package/shopify/skills/shopify/SKILL.md +103 -0
- package/shopify/skills/shopify/references/catalog-sync.md +74 -0
- package/shopify/skills/shopify/references/domains.md +26 -0
- package/shopify/skills/shopify/references/live-storefront.md +34 -0
- package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
- package/site-compliance/mcp.json +4 -0
- package/site-compliance/plugin.json +34 -0
- package/site-compliance/skills/site-compliance/SKILL.md +220 -0
- package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
- package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
- package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
- package/site-import/mcp.json +4 -0
- package/site-import/plugin.json +41 -0
- package/site-import/skills/site-import/SKILL.md +232 -0
- package/site-import/skills/site-import/references/content-mapping.md +166 -0
- package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
- package/site-import/skills/site-import/references/theme-extraction.md +34 -0
- package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/slide-design/mcp.json +4 -0
- package/slide-design/plugin.json +26 -0
- package/slide-design/skills/slide-design/SKILL.md +103 -0
- package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
- package/slide-design/skills/slide-design/references/html-deck.md +175 -0
- package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
- package/slide-design/skills/slide-design/references/review.md +93 -0
- package/slide-design/skills/slide-design/references/storyline.md +118 -0
- package/stripe/plugin.json +22 -0
- package/supabase/mcp.json +26 -0
- package/supabase/plugin.json +78 -0
- package/supabase/skills/supabase/SKILL.md +169 -0
- package/supabase/skills/supabase/references/failure-modes.md +101 -0
- package/ui-ux-design/CHANGELOG.md +28 -0
- package/ui-ux-design/LICENSE +201 -0
- package/ui-ux-design/NOTICE +14 -0
- package/ui-ux-design/README.md +62 -0
- package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
- package/ui-ux-design/mcp.json +4 -0
- package/ui-ux-design/plugin.json +26 -0
- package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
- package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
- package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
- package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
- package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
- package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
- package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
- package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
- package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
- package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
- package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
- package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
- package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
- package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
- package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
- package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
- package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
- package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
- package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
- package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
- package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
- package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
- package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
- package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
- package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
- package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
- package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
- package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
- package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
- package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
- package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
- package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
- package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
- package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
- package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
- package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
- package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
- package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
- package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
- package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
- package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
- package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
- package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
- package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
- package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
- package/vercel/NOTES.md +23 -0
- package/vercel/mcp.json +4 -0
- package/vercel/object-stores/vercel-blob.mjs +147 -0
- package/vercel/plugin.json +47 -0
- package/vercel/skills/vercel/SKILL.md +48 -0
- package/vercel/targets/vercel.mjs +586 -0
- package/vercel/tovu-consent.json +6 -0
- package/vercel/tovu-deploy-targets.json +219 -0
- package/vercel/tovu-object-stores.json +10 -0
- package/wordpress/NOTES.md +33 -0
- package/wordpress/plugin.json +30 -0
- package/wordpress/skills/wordpress/SKILL.md +43 -0
- package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
- package/wordpress/skills/wordpress/references/subdomain.md +66 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form submissions receiver, uploaded as `receiver.mjs` beside the site's static assets.
|
|
3
|
+
* Runs inside the Cloudflare Workers runtime: no imports, no Node APIs, Web platform only.
|
|
4
|
+
* Forms, their fields and full validation belong to the host; this module only receives and stores.
|
|
5
|
+
* Never log answers, IPs or the management secret: only fixed reason codes.
|
|
6
|
+
*/
|
|
7
|
+
export const LIMITS = Object.freeze({ maxBodyBytes: 64 * 1024, maxFields: 100, maxValueLength: 5000, ratePerWindow: 5, windowSeconds: 60, maxPage: 100 });
|
|
8
|
+
/** The host's reserved names: the honeypot and the double-submit attempt token. Never stored. */
|
|
9
|
+
const HONEYPOT = '_hp';
|
|
10
|
+
const RESERVED = new Set([HONEYPOT, '_attempt']);
|
|
11
|
+
/** The host's existing result query names (`form`, `form_status`), so a returning page reads one vocabulary. */
|
|
12
|
+
const RESULT_KEYS = ['form', 'form_status', 'form_errors', 'form_retry_after'];
|
|
13
|
+
const SUBMIT_PATH = /^\/forms\/([a-z0-9][a-z0-9-]{0,99})\/submit\/?$/;
|
|
14
|
+
|
|
15
|
+
/** Routes form POSTs and the private pull API; every other request is the static site.
|
|
16
|
+
* @complexity O(body bytes + fields) per submission; O(page) per pull. */
|
|
17
|
+
export async function handleRequest({ request, env, manifest }, options = {}) {
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
const submit = SUBMIT_PATH.exec(url.pathname);
|
|
20
|
+
if (submit) return request.method === 'POST' ? handleSubmit({ request, env, manifest, slug: submit[1] }, options) : errorPage({ status: 405, message: 'This address only accepts form submissions.' });
|
|
21
|
+
if (url.pathname === '/_tovu/forms/pending') return request.method === 'GET' ? guarded({ request, env, work: () => listPending({ env, url }) }) : json({ status: 405, body: { error: 'METHOD_NOT_ALLOWED' } });
|
|
22
|
+
if (url.pathname === '/_tovu/forms/ack') return request.method === 'POST' ? guarded({ request, env, work: () => acknowledge({ request, env }, options) }) : json({ status: 405, body: { error: 'METHOD_NOT_ALLOWED' } });
|
|
23
|
+
const slug = sentFormSlug({ request, url, manifest });
|
|
24
|
+
return slug ? withSentNotice({ request, env, slug }, options) : env.ASSETS.fetch(request);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The form named by the host's own success query (`form=<slug>&form_status=success`) on a page GET, if
|
|
28
|
+
* it is one of this site's forms. Any other request is served untouched. */
|
|
29
|
+
export function sentFormSlug({ request, url, manifest }, _options = {}) {
|
|
30
|
+
if (request.method !== 'GET' || url.searchParams.get('form_status') !== 'success') return null;
|
|
31
|
+
const slug = url.searchParams.get('form');
|
|
32
|
+
return manifest.forms.some(form => form.slug === slug) ? slug : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The static page with the same "sent" state the host's server renders: the form hidden and its own
|
|
36
|
+
* success slot shown. A page without a slot gets one plain accessible message after the form.
|
|
37
|
+
* Non-HTML or unsuccessful asset responses pass through unchanged. */
|
|
38
|
+
export async function withSentNotice({ request, env, slug }, { Rewriter = globalThis.HTMLRewriter } = {}) {
|
|
39
|
+
const response = await env.ASSETS.fetch(request);
|
|
40
|
+
if (!response.ok || !(response.headers.get('Content-Type') ?? '').includes('text/html') || !Rewriter) return response;
|
|
41
|
+
const rewriter = new Rewriter();
|
|
42
|
+
for (const [selector, handler] of sentNoticeHandlers({ slug })) rewriter.on(selector, handler);
|
|
43
|
+
return rewriter.transform(response);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SENT_MESSAGE = 'Thanks, your submission was received.';
|
|
47
|
+
/** Element handlers in document order. The host renders the success slot before its form, so the form
|
|
48
|
+
* handler already knows whether a slot was shown. Selectors use the generic success hooks only. */
|
|
49
|
+
export function sentNoticeHandlers({ slug }, _options = {}) {
|
|
50
|
+
const state = { slot: false, noticed: false };
|
|
51
|
+
const showSlot = { element(element) { element.removeAttribute('hidden'); state.slot = true; } };
|
|
52
|
+
const hideForm = { element(element) {
|
|
53
|
+
element.setAttribute('hidden', '');
|
|
54
|
+
if (state.slot || state.noticed) return;
|
|
55
|
+
state.noticed = true;
|
|
56
|
+
element.after(`<p class="tovu-form-success" data-form-slug="${slug}" role="status">${SENT_MESSAGE}</p>`, { html: true });
|
|
57
|
+
} };
|
|
58
|
+
return [
|
|
59
|
+
[`div.tovu-form-success[data-form-slug="${slug}"]`, showSlot],
|
|
60
|
+
[`div[data-tovu-form-success][data-form-slug="${slug}"]`, showSlot],
|
|
61
|
+
[`form[data-form-slug="${slug}"]`, hideForm],
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Honeypot, light manifest checks, D1 rate window, insert, then 303 to the success page. */
|
|
66
|
+
export async function handleSubmit({ request, env, manifest, slug }, { now = () => Date.now(), randomId = () => crypto.randomUUID() } = {}) {
|
|
67
|
+
const back = backLink({ request });
|
|
68
|
+
const form = manifest.forms.find(item => item.slug === slug);
|
|
69
|
+
if (!form) return errorPage({ status: 404, message: 'This form is not available.', back });
|
|
70
|
+
const parsed = await readFields({ request });
|
|
71
|
+
if (parsed.error) return errorPage({ ...parsed.error, back });
|
|
72
|
+
if ((parsed.fields.get(HONEYPOT) ?? []).some(value => value.trim() !== '')) return successRedirect({ request, form });
|
|
73
|
+
const checked = checkFields({ form, fields: parsed.fields });
|
|
74
|
+
if (checked.error) return errorPage({ status: 400, message: checked.error, back });
|
|
75
|
+
const refused = await storeSubmission({ db: env.TOVU_FORMS, slug, answers: checked.answers, ip: request.headers.get('CF-Connecting-IP'), back }, { now, randomId });
|
|
76
|
+
return refused ?? successRedirect({ request, form });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Rate window, then insert. Returns the error page to send, or null once the row is stored. */
|
|
80
|
+
async function storeSubmission({ db, slug, answers, ip, back }, { now, randomId }) {
|
|
81
|
+
try {
|
|
82
|
+
if (!await withinRate({ db, key: `${slug}:${ip ?? 'unknown'}`, nowMs: now() })) return errorPage({ status: 429, message: 'Too many submissions. Wait a minute, then try again.', back });
|
|
83
|
+
await db.prepare('INSERT INTO submissions (id, form_slug, answers_json, submitted_at, ip) VALUES (?, ?, ?, ?, ?)')
|
|
84
|
+
.bind(randomId(), slug, JSON.stringify(answers), new Date(now()).toISOString(), ip).run();
|
|
85
|
+
return null;
|
|
86
|
+
} catch {
|
|
87
|
+
console.error('FORMS_STORE_FAILED');
|
|
88
|
+
return errorPage({ status: 503, message: 'Your submission could not be saved. Try again in a few minutes.', back });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Reads at most LIMITS.maxBodyBytes while streaming, then parses URL-encoded or multipart text fields.
|
|
93
|
+
* Repeated names (checkbox groups) keep every value in order. */
|
|
94
|
+
export async function readFields({ request }, _options = {}) {
|
|
95
|
+
const type = (request.headers.get('Content-Type') ?? '').toLowerCase();
|
|
96
|
+
const multipart = type.startsWith('multipart/form-data');
|
|
97
|
+
if (!multipart && !type.startsWith('application/x-www-form-urlencoded')) return { error: { status: 415, message: 'This form must be sent as a normal web form.' } };
|
|
98
|
+
const bytes = await readBounded({ request });
|
|
99
|
+
if (!bytes) return { error: { status: 413, message: 'The submission is too large.' } };
|
|
100
|
+
const entries = multipart ? await multipartEntries({ bytes, type: request.headers.get('Content-Type') }) : [...new URLSearchParams(new TextDecoder().decode(bytes))];
|
|
101
|
+
const error = entriesError({ entries });
|
|
102
|
+
if (error) return { error };
|
|
103
|
+
const fields = new Map();
|
|
104
|
+
for (const [name, value] of entries) fields.set(name, [...(fields.get(name) ?? []), value]);
|
|
105
|
+
return { fields };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function entriesError({ entries }, _options = {}) {
|
|
109
|
+
if (!entries) return { status: 400, message: 'The submission could not be read.' };
|
|
110
|
+
if (entries.length > LIMITS.maxFields) return { status: 413, message: 'The submission has too many fields.' };
|
|
111
|
+
if (entries.some(([, value]) => typeof value !== 'string')) return { status: 400, message: 'File uploads are not accepted by this form.' };
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function readBounded({ request }, _options = {}) {
|
|
116
|
+
if (Number(request.headers.get('Content-Length')) > LIMITS.maxBodyBytes) return null;
|
|
117
|
+
if (!request.body) return new Uint8Array();
|
|
118
|
+
const chunks = [];
|
|
119
|
+
let size = 0;
|
|
120
|
+
for await (const chunk of request.body) {
|
|
121
|
+
size += chunk.byteLength;
|
|
122
|
+
if (size > LIMITS.maxBodyBytes) return null;
|
|
123
|
+
chunks.push(chunk);
|
|
124
|
+
}
|
|
125
|
+
const bytes = new Uint8Array(size);
|
|
126
|
+
let offset = 0;
|
|
127
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
128
|
+
return bytes;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function multipartEntries({ bytes, type }, _options = {}) {
|
|
132
|
+
try { return [...await new Response(bytes, { headers: { 'Content-Type': type } }).formData()]; }
|
|
133
|
+
catch { return null; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Manifest-driven light validation; the host re-validates every answer on import.
|
|
137
|
+
* @returns {{answers: Record<string, string|string[]>} | {error: string}} */
|
|
138
|
+
export function checkFields({ form, fields }, _options = {}) {
|
|
139
|
+
const known = new Map(form.fields.map(field => [field.name, field]));
|
|
140
|
+
const unknown = [...fields.keys()].find(name => !RESERVED.has(name) && !known.has(name));
|
|
141
|
+
if (unknown !== undefined) return { error: `The field "${unknown}" is not part of this form.` };
|
|
142
|
+
const answers = {};
|
|
143
|
+
for (const field of form.fields) {
|
|
144
|
+
const problem = fieldProblem({ field, values: fields.get(field.name) ?? [] });
|
|
145
|
+
if (problem) return { error: problem };
|
|
146
|
+
const values = fields.get(field.name);
|
|
147
|
+
if (values) answers[field.name] = field.multiple ? values : values[0];
|
|
148
|
+
}
|
|
149
|
+
return { answers };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function fieldProblem({ field, values }, _options = {}) {
|
|
153
|
+
if (field.required && !values.some(value => value.trim() !== '')) return `"${field.name}" is required.`;
|
|
154
|
+
if (!field.multiple && values.length > 1) return `"${field.name}" was sent more than once.`;
|
|
155
|
+
const max = field.maxLength ?? LIMITS.maxValueLength;
|
|
156
|
+
return values.some(value => value.length > max) ? `"${field.name}" is longer than ${max} characters.` : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Fixed-window counter in D1 (shared by every isolate). Counts only valid submissions.
|
|
160
|
+
* Stale windows for any key are pruned in the same batch; no matching rows means no row writes. */
|
|
161
|
+
export async function withinRate({ db, key, nowMs }, _options = {}) {
|
|
162
|
+
const windowStart = Math.floor(nowMs / 1000 / LIMITS.windowSeconds) * LIMITS.windowSeconds;
|
|
163
|
+
const [counted] = await db.batch([
|
|
164
|
+
db.prepare('INSERT INTO rate_windows (key, window_start, count) VALUES (?, ?, 1) ON CONFLICT(key) DO UPDATE SET count = CASE WHEN rate_windows.window_start = excluded.window_start THEN rate_windows.count + 1 ELSE 1 END, window_start = excluded.window_start RETURNING count').bind(key, windowStart),
|
|
165
|
+
db.prepare('DELETE FROM rate_windows WHERE window_start < ?').bind(windowStart - 3600),
|
|
166
|
+
]);
|
|
167
|
+
return Number(counted.results?.[0]?.count ?? Infinity) <= LIMITS.ratePerWindow;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The form's own success page when it has one; otherwise the referring page on this host (or `/`)
|
|
171
|
+
* with the host's result query names, exactly as the live site's 303 sets them. */
|
|
172
|
+
export function successRedirect({ request, form }, _options = {}) {
|
|
173
|
+
const target = form.successPath ? new URL(form.successPath, request.url) : (sameHostReferer({ request }) ?? new URL('/', request.url));
|
|
174
|
+
if (!form.successPath) {
|
|
175
|
+
for (const key of RESULT_KEYS) target.searchParams.delete(key);
|
|
176
|
+
target.searchParams.set('form', form.slug);
|
|
177
|
+
target.searchParams.set('form_status', 'success');
|
|
178
|
+
}
|
|
179
|
+
return new Response(null, { status: 303, headers: { Location: `${target.pathname}${target.search}`, 'Cache-Control': 'no-store' } });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sameHostReferer({ request }, _options = {}) {
|
|
183
|
+
try {
|
|
184
|
+
const referer = new URL(request.headers.get('Referer') ?? '');
|
|
185
|
+
return referer.host === new URL(request.url).host && !referer.pathname.startsWith('//') ? referer : null;
|
|
186
|
+
} catch { return null; }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function backLink({ request }, _options = {}) {
|
|
190
|
+
const referer = sameHostReferer({ request });
|
|
191
|
+
return referer ? `${referer.pathname}${referer.search}` : '/';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const escapeHtml = value => String(value).replace(/[&<>"']/g, char => `&#${char.charCodeAt(0)};`);
|
|
195
|
+
|
|
196
|
+
/** Minimal accessible page; the site's theme is never copied here. */
|
|
197
|
+
export function errorPage({ status, message, back = '/' }, _options = {}) {
|
|
198
|
+
const body = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Form not sent</title></head>`
|
|
199
|
+
+ `<body><main><h1>Form not sent</h1><p role="alert">${escapeHtml(message)}</p><p><a href="${escapeHtml(back)}">Go back</a></p></main></body></html>`;
|
|
200
|
+
return new Response(body, { status, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function json({ status = 200, body }, _options = {}) {
|
|
204
|
+
return Response.json(body, { status, headers: { 'Cache-Control': 'no-store' } });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Bearer check with a constant-time comparison of SHA-256 digests (equal length by construction). */
|
|
208
|
+
export async function authorized({ request, env }, _options = {}) {
|
|
209
|
+
const secret = env.TOVU_FORMS_SECRET;
|
|
210
|
+
const header = request.headers.get('Authorization') ?? '';
|
|
211
|
+
if (typeof secret !== 'string' || secret.length < 32 || !header.startsWith('Bearer ')) return false;
|
|
212
|
+
const encoder = new TextEncoder();
|
|
213
|
+
const [given, expected] = await Promise.all([header.slice(7), secret].map(value => crypto.subtle.digest('SHA-256', encoder.encode(value))));
|
|
214
|
+
const left = new Uint8Array(given);
|
|
215
|
+
const right = new Uint8Array(expected);
|
|
216
|
+
let difference = 0;
|
|
217
|
+
for (let index = 0; index < left.length; index += 1) difference |= left[index] ^ right[index];
|
|
218
|
+
return difference === 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function guarded({ request, env, work }, _options = {}) {
|
|
222
|
+
if (!await authorized({ request, env })) return json({ status: 401, body: { error: 'UNAUTHORIZED' } });
|
|
223
|
+
try { return await work(); }
|
|
224
|
+
catch { console.error('FORMS_PULL_FAILED'); return json({ status: 503, body: { error: 'STORAGE_UNAVAILABLE' } }); }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Oldest unpulled rows after `after`, at most LIMITS.maxPage. */
|
|
228
|
+
export async function listPending({ env, url }, _options = {}) {
|
|
229
|
+
const after = Math.max(0, Number.parseInt(url.searchParams.get('after') ?? '0', 10) || 0);
|
|
230
|
+
const limit = Math.min(LIMITS.maxPage, Math.max(1, Number.parseInt(url.searchParams.get('limit') ?? '50', 10) || 50));
|
|
231
|
+
const { results = [] } = await env.TOVU_FORMS.prepare('SELECT seq, id, form_slug, answers_json, submitted_at, ip FROM submissions WHERE pulled_at IS NULL AND seq > ? ORDER BY seq LIMIT ?').bind(after, limit).all();
|
|
232
|
+
const items = results.map(row => ({ seq: row.seq, id: row.id, formSlug: row.form_slug, answers: JSON.parse(row.answers_json), submittedAt: row.submitted_at, ip: row.ip ?? null }));
|
|
233
|
+
return json({ body: { items, nextAfter: items.length ? items[items.length - 1].seq : after, hasMore: items.length === limit } });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function validIds({ ids }, _options = {}) {
|
|
237
|
+
return Array.isArray(ids) && ids.length > 0 && ids.length <= LIMITS.maxPage && ids.every(id => typeof id === 'string' && id.length > 0 && id.length <= 64);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Marks committed rows as pulled. Unknown or already-pulled ids are ignored. */
|
|
241
|
+
export async function acknowledge({ request, env }, { now = () => Date.now() } = {}) {
|
|
242
|
+
let body;
|
|
243
|
+
try { body = await request.json(); } catch { body = null; }
|
|
244
|
+
const ids = body?.ids;
|
|
245
|
+
if (!validIds({ ids })) return json({ status: 400, body: { error: 'INVALID_IDS' } });
|
|
246
|
+
const result = await env.TOVU_FORMS.prepare(`UPDATE submissions SET pulled_at = ? WHERE pulled_at IS NULL AND id IN (${ids.map(() => '?').join(', ')})`)
|
|
247
|
+
.bind(new Date(now()).toISOString(), ...ids).run();
|
|
248
|
+
return json({ body: { acknowledged: Number(result.meta?.changes ?? 0) } });
|
|
249
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "composio",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Lets the assistant use the user's other apps (Gmail, Google Calendar, Slack, Notion, GitHub, HubSpot, Linear and 1000+ more) through Composio Connect, Composio's hosted MCP server. One browser sign-in to Composio connects it; each app is then connected on first use by a Composio sign-in link, with no API keys anywhere.",
|
|
6
|
+
"author": "Tovu",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"extensions": {
|
|
9
|
+
"tovu": {
|
|
10
|
+
"maturity": "soon",
|
|
11
|
+
"summary": "Lets the assistant use your other apps, like Gmail, Google Calendar, Slack and Notion. You connect each app with one sign-in link.",
|
|
12
|
+
"id": "tovu/composio",
|
|
13
|
+
"provides": [
|
|
14
|
+
"app-connectors"
|
|
15
|
+
],
|
|
16
|
+
"credentialCards": [
|
|
17
|
+
{
|
|
18
|
+
"target": "composio",
|
|
19
|
+
"title": "Connect Composio",
|
|
20
|
+
"label": "Composio account",
|
|
21
|
+
"category": "general",
|
|
22
|
+
"intro": "Connect Composio to use your other apps. Sign in here, then approve each app separately when you need it.",
|
|
23
|
+
"usedFor": [
|
|
24
|
+
"Finds tools for your other apps",
|
|
25
|
+
"Connects each app with your approval and carries out the tasks you request"
|
|
26
|
+
],
|
|
27
|
+
"keyHint": "Browser sign-in only; this connection does not accept a pasted API key.",
|
|
28
|
+
"saveKind": "oauth",
|
|
29
|
+
"permissions": "Connects tool discovery, schemas, account connections and requested app actions. App permissions are approved at each app’s own sign-in. Connection management and action execution need separate write grants; do not grant remote Bash or workbench tools.",
|
|
30
|
+
"baseUrl": "https://connect.composio.dev"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"composio",
|
|
37
|
+
"apps",
|
|
38
|
+
"connect",
|
|
39
|
+
"connect-my-account",
|
|
40
|
+
"integrations",
|
|
41
|
+
"connectors",
|
|
42
|
+
"gmail",
|
|
43
|
+
"email",
|
|
44
|
+
"google-calendar",
|
|
45
|
+
"calendar",
|
|
46
|
+
"google-drive",
|
|
47
|
+
"google-sheets",
|
|
48
|
+
"slack",
|
|
49
|
+
"notion",
|
|
50
|
+
"github",
|
|
51
|
+
"linear",
|
|
52
|
+
"jira",
|
|
53
|
+
"hubspot",
|
|
54
|
+
"crm",
|
|
55
|
+
"discord",
|
|
56
|
+
"stripe",
|
|
57
|
+
"airtable",
|
|
58
|
+
"trello",
|
|
59
|
+
"asana",
|
|
60
|
+
"automation",
|
|
61
|
+
"send-email",
|
|
62
|
+
"external-mcp",
|
|
63
|
+
"mcp",
|
|
64
|
+
"oauth",
|
|
65
|
+
"sign-in",
|
|
66
|
+
"app-connectors"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: composio
|
|
3
|
+
description: Use the user's other apps (Gmail, Calendar, Slack, Notion, GitHub, HubSpot, 1000+ more) through Composio Connect, Composio's hosted MCP server. Covers connecting from nothing (one browser sign-in to Composio, no API key), the grants the connection needs, and the per-app flow - search for a tool, read its schema, connect the app with a Composio sign-in link, then execute.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Composio: the user's other apps
|
|
7
|
+
|
|
8
|
+
## Connection card
|
|
9
|
+
|
|
10
|
+
Choose the card yourself from its `usedFor` list. Reuse a saved key or working sign-in for that
|
|
11
|
+
task. Never offer a replacement unless the person asks.
|
|
12
|
+
|
|
13
|
+
`agent_plugin_connect {pluginId:"composio"}` shows the plugin-declared **Connect Composio** card. Review each app’s sign-in permissions and grant only the requested tools. This hosted connection declares no token fallback; do not request or save a Composio API key.
|
|
14
|
+
|
|
15
|
+
Composio Connect is an **external MCP server** at `https://connect.composio.dev/mcp`. It does not
|
|
16
|
+
expose one tool per app. It exposes a handful of meta-tools, and you reach every app through them.
|
|
17
|
+
Its tools arrive in chat as `mcp__composio__<TOOL>`.
|
|
18
|
+
|
|
19
|
+
| Tool | What it is for |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `COMPOSIO_SEARCH_TOOLS` | Find the app tool that does what the user asked ("send an email", "create a Linear issue") |
|
|
22
|
+
| `COMPOSIO_GET_TOOL_SCHEMAS` | Read the exact inputs of the tools you found |
|
|
23
|
+
| `COMPOSIO_MANAGE_CONNECTIONS` | Check whether an app is connected; get a sign-in link for one that is not |
|
|
24
|
+
| `COMPOSIO_WAIT_FOR_CONNECTIONS` | Wait while the user finishes that app sign-in |
|
|
25
|
+
| `COMPOSIO_MULTI_EXECUTE_TOOL` | Run the app tool(s) |
|
|
26
|
+
|
|
27
|
+
## Connecting from nothing
|
|
28
|
+
|
|
29
|
+
**Never ask the user for a Composio API key, and never accept one pasted into chat.** Composio
|
|
30
|
+
Connect signs the user in by standard MCP OAuth in their browser. The host discovers Composio's
|
|
31
|
+
endpoints and registers its own client, so nobody types a URL, client id or secret.
|
|
32
|
+
|
|
33
|
+
1. **Turn the plugin on.** It ships disabled. Offer to enable it; on yes call `plugins_set_enabled`
|
|
34
|
+
with `family: "agent-plugin"`, `pluginId: "composio"`, `enabled: true` (the user approves a
|
|
35
|
+
dialog), or they use the **Agent Plugins** screen. Enabling creates the `composio` connection row.
|
|
36
|
+
2. **Sign in.** Call `agent_plugin_connect { pluginId: "composio" }`. It provisions the declared
|
|
37
|
+
connection, shows the secure browser sign-in card and waits. On `status: "connected"`, continue;
|
|
38
|
+
on `status: "waiting-for-sign-in"`, wait for the person to return, then call it again once.
|
|
39
|
+
Say: "Sign in to Composio (free account; sign up there if you have none)." If it refuses because
|
|
40
|
+
the site is not set up for sign-in from chat, follow the host guidance for signing in elsewhere;
|
|
41
|
+
do not retry. Never create a second connection or use Bash, curl or a remote workbench to sign in.
|
|
42
|
+
3. **Grant the tools**, see [Grants](#grants). The host guidance says where grants are made and
|
|
43
|
+
whether a saved grant needs an assistant restart before it takes effect.
|
|
44
|
+
|
|
45
|
+
Say plainly that steps 1 and 3 need a click from the user. Do not imply you can finish alone.
|
|
46
|
+
|
|
47
|
+
## Grants
|
|
48
|
+
|
|
49
|
+
In the host's settings for the `composio` connection, grant these tools:
|
|
50
|
+
|
|
51
|
+
- `COMPOSIO_SEARCH_TOOLS`, `COMPOSIO_GET_TOOL_SCHEMAS`, `COMPOSIO_MANAGE_CONNECTIONS`,
|
|
52
|
+
`COMPOSIO_WAIT_FOR_CONNECTIONS`, `COMPOSIO_MULTI_EXECUTE_TOOL`.
|
|
53
|
+
- Where the host asks for a separate write grant (it applies only to a tool that declares it
|
|
54
|
+
writes), grant it for `COMPOSIO_MULTI_EXECUTE_TOOL` and `COMPOSIO_MANAGE_CONNECTIONS`. Without it
|
|
55
|
+
the host refuses the tool and it simply never appears.
|
|
56
|
+
|
|
57
|
+
**Do not grant** `COMPOSIO_REMOTE_WORKBENCH` or `COMPOSIO_REMOTE_BASH_TOOL`. They run code on
|
|
58
|
+
Composio's servers; nothing the user asks for here needs them.
|
|
59
|
+
|
|
60
|
+
If an `mcp__composio__*` tool you expect is missing, do not invent a reason. Report what the
|
|
61
|
+
host's admissions or refusal notice says, or say you do not know and point the user at the host's
|
|
62
|
+
connection settings.
|
|
63
|
+
|
|
64
|
+
## Doing a task
|
|
65
|
+
|
|
66
|
+
1. `COMPOSIO_SEARCH_TOOLS` with the user's goal in plain words.
|
|
67
|
+
2. `COMPOSIO_GET_TOOL_SCHEMAS` for the tool you will use. Never guess argument names.
|
|
68
|
+
3. `COMPOSIO_MANAGE_CONNECTIONS` for that app. If it is not connected, give the user the sign-in
|
|
69
|
+
link it returns ("Connect your Gmail here"), then `COMPOSIO_WAIT_FOR_CONNECTIONS`. Each app is
|
|
70
|
+
connected once; it stays connected across sessions.
|
|
71
|
+
4. `COMPOSIO_MULTI_EXECUTE_TOOL`.
|
|
72
|
+
|
|
73
|
+
**Ask before anything that sends, posts, pays, or deletes** (an email, a Slack message, a payment,
|
|
74
|
+
removing a record): show what will go out and to whom, and run it only after the user says yes.
|
|
75
|
+
Reading (listing emails, events, issues) needs no confirmation.
|
|
76
|
+
|
|
77
|
+
Report what the tool actually returned. If it failed, say so with its error in one plain sentence.
|
|
78
|
+
|
|
79
|
+
## When it stops working
|
|
80
|
+
|
|
81
|
+
- **"is disconnected: its authorization expired or was revoked"**: call `external_mcp_reauth_prompt`
|
|
82
|
+
for `composio`. After the user signs in again, retry the original call once.
|
|
83
|
+
- **An app's own sign-in expired** (the app tool fails with an auth error): run
|
|
84
|
+
`COMPOSIO_MANAGE_CONNECTIONS` for that app again and give the user the new link.
|
|
85
|
+
- **The user wants to disconnect an app**: that is done in their Composio account, not on this site.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Selects the portable identity key without changing explicit host isolation.
|
|
2
|
+
* Legacy manifests remain readable only when no namespace was requested and `jini` is absent.
|
|
3
|
+
* A present malformed extension is left to the identity validator rather than bypassed.
|
|
4
|
+
* @returns The namespace to pass to the shared identity and metadata readers.
|
|
5
|
+
* @complexity O(1) time and space; no I/O or manifest mutation.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveAgentPluginIdentityNamespace({ value }: {
|
|
8
|
+
readonly value: unknown;
|
|
9
|
+
}, { identityNamespace }?: {
|
|
10
|
+
readonly identityNamespace?: string;
|
|
11
|
+
}): string;
|
|
12
|
+
//# sourceMappingURL=identity-namespace.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Selects the portable identity key without changing explicit host isolation.
|
|
2
|
+
* Legacy manifests remain readable only when no namespace was requested and `jini` is absent.
|
|
3
|
+
* A present malformed extension is left to the identity validator rather than bypassed.
|
|
4
|
+
* @returns The namespace to pass to the shared identity and metadata readers.
|
|
5
|
+
* @complexity O(1) time and space; no I/O or manifest mutation.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveAgentPluginIdentityNamespace({ value }, { identityNamespace } = {}) {
|
|
8
|
+
if (identityNamespace !== undefined)
|
|
9
|
+
return identityNamespace;
|
|
10
|
+
if (typeof value !== 'object' || value === null || !('extensions' in value))
|
|
11
|
+
return 'jini';
|
|
12
|
+
const extensions = value.extensions;
|
|
13
|
+
if (typeof extensions !== 'object' || extensions === null || Array.isArray(extensions))
|
|
14
|
+
return 'jini';
|
|
15
|
+
if (Object.hasOwn(extensions, 'jini'))
|
|
16
|
+
return 'jini';
|
|
17
|
+
// Compatibility only: existing published manifests used this product's extension key.
|
|
18
|
+
return Object.hasOwn(extensions, 'tovu') ? 'tovu' : 'jini';
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=identity-namespace.js.map
|
package/dist/folder-zip-node.js
CHANGED
|
@@ -126,19 +126,41 @@ function customizeText({ text, edit }) {
|
|
|
126
126
|
}
|
|
127
127
|
return text.slice(0, index) + edit.newString + text.slice(index + edit.oldString.length);
|
|
128
128
|
}
|
|
129
|
-
/**
|
|
129
|
+
/** Plugin-level history a fork must not inherit: `renamedFrom` lets a package answer for its old
|
|
130
|
+
* ids (export ownership, saved references) and `supersedes` lets it retire other packages. */
|
|
131
|
+
const HISTORICAL_IDENTITY_CLAIMS = ['renamedFrom', 'supersedes'];
|
|
132
|
+
function isPlainObject(value) {
|
|
133
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
134
|
+
}
|
|
135
|
+
/** Every host key, not only the requesting host's: a manifest declares one `extensions` entry per
|
|
136
|
+
* host, so a fork read later by another host must not keep that host's publisher id or claims. */
|
|
137
|
+
function sanitizeHostIdentities({ extensions, localId }) {
|
|
138
|
+
for (const extension of Object.values(extensions)) {
|
|
139
|
+
if (!isPlainObject(extension))
|
|
140
|
+
continue;
|
|
141
|
+
if ('id' in extension)
|
|
142
|
+
extension.id = localId;
|
|
143
|
+
for (const claim of HISTORICAL_IDENTITY_CLAIMS)
|
|
144
|
+
delete extension[claim];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** A fork must not retain its publisher identity or historical aliases and supersession claims.
|
|
148
|
+
* The requesting host always gets a fresh `local/<name>` id, even when the source declared none. */
|
|
130
149
|
function customizeManifest({ text, name, identityNamespace }) {
|
|
131
150
|
const manifest = JSON.parse(text);
|
|
132
151
|
if (!isAgentPluginName({ value: name }) || manifest.name === name) {
|
|
133
152
|
throw new FolderZipError('unreadable', 'Choose a valid new Agent Plugin id different from the source.');
|
|
134
153
|
}
|
|
135
154
|
manifest.name = name;
|
|
136
|
-
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
155
|
+
manifest.extensions ??= {};
|
|
156
|
+
if (!isPlainObject(manifest.extensions))
|
|
157
|
+
throw new FolderZipError('unreadable', 'plugin.json extensions must be an object.');
|
|
158
|
+
const localId = `local/${name}`;
|
|
159
|
+
manifest.extensions[identityNamespace] ??= {};
|
|
160
|
+
sanitizeHostIdentities({ extensions: manifest.extensions, localId });
|
|
161
|
+
const host = manifest.extensions[identityNamespace];
|
|
162
|
+
if (isPlainObject(host))
|
|
163
|
+
host.id = localId;
|
|
142
164
|
return JSON.stringify(manifest, null, 2) + '\n';
|
|
143
165
|
}
|
|
144
166
|
/**
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* ships as — written by `seed-bundled.ts` on every boot, read by the surfaces that have to pick one
|
|
4
4
|
* installed package for a plugin id.
|
|
5
5
|
*
|
|
6
|
-
* Current install policy (frpid, 2026-10-09):
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Current install policy (frpid, 2026-10-09): an explicit install, update or replace prunes the
|
|
7
|
+
* plugin's superseded revisions. Bundled seeding does not (2026-11-02): it cannot tell an operator's
|
|
8
|
+
* edited or uploaded same-name copy from an old bundled one, so the rationale below applies to every
|
|
9
|
+
* reseed. This ledger selects the build's authoritative digest; it never deletes package bytes.
|
|
9
10
|
*
|
|
10
11
|
* ---------------------------------------------------------------------------
|
|
11
12
|
* The gap this closes
|
|
@@ -86,6 +86,13 @@ export interface InstallAgentPluginOptional {
|
|
|
86
86
|
readonly plugin: InstalledAgentPlugin;
|
|
87
87
|
readonly publish: () => Promise<InstalledAgentPlugin>;
|
|
88
88
|
}, optional: Record<string, never>) => Promise<InstalledAgentPlugin>;
|
|
89
|
+
/** Default true: an explicit install, update or replace retires this plugin's other revisions.
|
|
90
|
+
* Bundled seeding passes false. A revision's digest names the bytes it was published with, not
|
|
91
|
+
* what is on disk now, and nothing records whether a same-name copy came from an upload, an
|
|
92
|
+
* operator edit or an older build, so a boot-time seed cannot prove a copy is safe to delete.
|
|
93
|
+
* The bundled-digests ledger selects the build's digest instead (owner rule 2026-10-29: other
|
|
94
|
+
* copies are "kept unselected"). */
|
|
95
|
+
readonly pruneSupersededRevisions?: boolean;
|
|
89
96
|
}
|
|
90
97
|
/** Internal composition; one state/queue instance per injected host context. */
|
|
91
98
|
export declare function createInstallModule(ports: AgentPluginLifecyclePorts): {
|
|
@@ -65,7 +65,7 @@ import { readPluginIdentity, assertPluginIdentityAvailable } from "@jini-ai/core
|
|
|
65
65
|
import { createPersistentStateModule } from "./persistent-state.js";
|
|
66
66
|
import { createAgentPluginUpdateStore, settleAgentPluginUpdateJournal } from "./update-store.js";
|
|
67
67
|
/** Extracted from the host plugin lifecycle. Effects are supplied per host context. */
|
|
68
|
-
import { createHash } from "node:crypto";
|
|
68
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
69
69
|
import { constants } from "node:fs";
|
|
70
70
|
import path from "node:path";
|
|
71
71
|
import { PackagePathViolation } from "./package-paths.js";
|
|
@@ -223,7 +223,7 @@ function buildModule(ports) {
|
|
|
223
223
|
await persistentState.assertOwnedPath({ workspaceRoot: workspaceLayout.root, entryPath: path.relative(workspaceLayout.root, directory) });
|
|
224
224
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
225
225
|
}
|
|
226
|
-
if (await
|
|
226
|
+
if (await reuseOrSetAsideExisting({ packagesDir, finalRoot, extractionRoot, pluginId })) {
|
|
227
227
|
publishedPlugin = await indexInstalledRoot(finalRoot, digest);
|
|
228
228
|
return publishedPlugin;
|
|
229
229
|
}
|
|
@@ -241,7 +241,7 @@ function buildModule(ports) {
|
|
|
241
241
|
// The journal lives under the layout this install was given, the one it publishes into.
|
|
242
242
|
await settleAgentPluginUpdateJournal({ store: createAgentPluginUpdateStore({ ports: { ...ports, layout }, workspaceId, pluginId: indexed.pluginId }) });
|
|
243
243
|
const installed = await (optional.publishGuard ? optional.publishGuard({ plugin: indexed, publish: publishPackage }, {}) : publishPackage());
|
|
244
|
-
if (publishedPlugin)
|
|
244
|
+
if (publishedPlugin && optional.pruneSupersededRevisions !== false)
|
|
245
245
|
await pruneSupersededRevisions({ workspaceRoot: workspaceLayout.root, installed: publishedPlugin });
|
|
246
246
|
return installed;
|
|
247
247
|
} });
|
|
@@ -256,6 +256,48 @@ function buildModule(ports) {
|
|
|
256
256
|
await rm(transactionRoot, { recursive: true, force: true });
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Whether `finalRoot` already holds exactly the verified bytes in `extractionRoot`, so the install
|
|
261
|
+
* can deduplicate. A digest directory's NAME is only a claim: an operator with shell access can
|
|
262
|
+
* edit it in place ({@link freezeTree} is not a security boundary), and the ledger and the
|
|
263
|
+
* contribution trust gate check digest identity only — so reuse without comparing bytes would
|
|
264
|
+
* keep edited executable modules selected as shipped code. A mismatched copy is renamed aside
|
|
265
|
+
* under a non-digest name (kept for the operator, never indexed or selected) and `false` tells the
|
|
266
|
+
* caller to publish the pristine bytes in its place.
|
|
267
|
+
* @complexity O(f + b) in the file count and bytes of the two trees.
|
|
268
|
+
*/
|
|
269
|
+
async function reuseOrSetAsideExisting({ packagesDir, finalRoot, extractionRoot, pluginId }) {
|
|
270
|
+
if (!(await isRealDirectory(finalRoot)))
|
|
271
|
+
return false;
|
|
272
|
+
if (await sameTree(finalRoot, extractionRoot))
|
|
273
|
+
return true;
|
|
274
|
+
const aside = path.join(packagesDir, `.modified-${path.basename(finalRoot).slice(0, 12)}-${randomUUID()}`);
|
|
275
|
+
await createUninstallModule(ports).stageForRemoval(packagesDir, finalRoot, { quarantined: aside });
|
|
276
|
+
ports.onEvent?.({ event: 'agent-plugin-modified-revision-preserved', message: `Plugin '${pluginId}': an installed revision no longer matched its digest; it was kept at '${aside}' and the verified bytes were reinstalled.` });
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
/** Same entry names, kinds and file bytes, recursively. @complexity O(f + b). */
|
|
280
|
+
async function sameTree(left, right) {
|
|
281
|
+
const [leftEntries, rightEntries] = await Promise.all([readdir(left, { withFileTypes: true }), readdir(right, { withFileTypes: true })]);
|
|
282
|
+
if (leftEntries.length !== rightEntries.length)
|
|
283
|
+
return false;
|
|
284
|
+
const rightByName = new Map(rightEntries.map(entry => [entry.name, entry]));
|
|
285
|
+
for (const entry of leftEntries) {
|
|
286
|
+
const other = rightByName.get(entry.name);
|
|
287
|
+
if (!other || entry.isDirectory() !== other.isDirectory() || entry.isFile() !== other.isFile())
|
|
288
|
+
return false;
|
|
289
|
+
const [leftPath, rightPath] = [path.join(left, entry.name), path.join(right, entry.name)];
|
|
290
|
+
if (!(await (entry.isDirectory() ? sameTree(leftPath, rightPath) : sameFile(leftPath, rightPath))))
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
/** Size first, so a grown file is rejected without reading it. */
|
|
296
|
+
async function sameFile(left, right) {
|
|
297
|
+
if ((await stat(left)).size !== (await stat(right)).size)
|
|
298
|
+
return false;
|
|
299
|
+
return Buffer.from(await readFile(left)).equals(Buffer.from(await readFile(right)));
|
|
300
|
+
}
|
|
259
301
|
/** The `(target '...')` suffix on the symlink-rejection message, or nothing when the entry carries
|
|
260
302
|
* no `linkTarget` at all — split out purely to keep that `&&`/ternary pair out of
|
|
261
303
|
* {@link extractOneEntry}'s own branch count. */
|
|
@@ -37,7 +37,7 @@ export type ParseAgentPluginManifestResult = {
|
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
39
|
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
40
|
-
* Hosts inject identityNamespace; the default
|
|
40
|
+
* Hosts inject identityNamespace; the default is `jini`, with a legacy-key fallback.
|
|
41
41
|
*
|
|
42
42
|
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
43
43
|
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
@@ -46,7 +46,7 @@ export type ParseAgentPluginManifestResult = {
|
|
|
46
46
|
*/
|
|
47
47
|
export declare function parseAgentPluginManifest({ value }: {
|
|
48
48
|
readonly value: unknown;
|
|
49
|
-
},
|
|
49
|
+
}, optional?: {
|
|
50
50
|
readonly identityNamespace?: string;
|
|
51
51
|
}): ParseAgentPluginManifestResult;
|
|
52
52
|
/**
|