@jini-ai/agent-plugins 0.6.16 → 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 +35 -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
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* caller that reads the file and hands the parsed value here.
|
|
56
56
|
*/
|
|
57
57
|
import { isAgentPluginName, MAX_NAME_LENGTH } from '../core/plugin-name.js';
|
|
58
|
+
import { resolveAgentPluginIdentityNamespace } from '../core/identity-namespace.js';
|
|
58
59
|
export { isAgentPluginName } from '../core/plugin-name.js';
|
|
59
60
|
import { readAgentPluginCompatibilityMetadata } from "./compatibility-metadata.js";
|
|
60
61
|
import { parseAgentPluginExports } from './exports.js';
|
|
@@ -174,17 +175,18 @@ function collectManifestProblems({ raw, identityNamespace }, { nameError } = {})
|
|
|
174
175
|
}
|
|
175
176
|
/**
|
|
176
177
|
* Validates an already-`JSON.parse`d `plugin.json` value against the 1.0.0/1.1.0 grammar.
|
|
177
|
-
* Hosts inject identityNamespace; the default
|
|
178
|
+
* Hosts inject identityNamespace; the default is `jini`, with a legacy-key fallback.
|
|
178
179
|
*
|
|
179
180
|
* @throws Nothing — every failure is reported via the returned `errors`, matching this codebase's
|
|
180
181
|
* own convention for expected validation outcomes (`plugin-runtime/manifest.ts`,
|
|
181
182
|
* `plugin-runtime/discovery.ts`).
|
|
182
183
|
* @complexity O(k + n) time and space in the input keys and keyword entries.
|
|
183
184
|
*/
|
|
184
|
-
export function parseAgentPluginManifest({ value },
|
|
185
|
+
export function parseAgentPluginManifest({ value }, optional = {}) {
|
|
185
186
|
if (!isJsonObject(value))
|
|
186
187
|
return { ok: false, errors: ["plugin.json must be a JSON object"] };
|
|
187
188
|
const raw = value;
|
|
189
|
+
const identityNamespace = resolveAgentPluginIdentityNamespace({ value }, optional);
|
|
188
190
|
const nameResult = resolveManifestName(raw);
|
|
189
191
|
const { errors, warnings } = collectManifestProblems({ raw, identityNamespace }, { nameError: nameResult.error });
|
|
190
192
|
if (errors.length > 0)
|
|
@@ -406,7 +408,7 @@ function collectMcpConfigErrors({ raw }, { pluginManifest } = {}) {
|
|
|
406
408
|
}
|
|
407
409
|
/** The per-server metadata `plugin.json` declares under `extensionNamespace`, or `{}` when it declares none. */
|
|
408
410
|
function readExtensionServers({ pluginManifest, extensionNamespace }) {
|
|
409
|
-
const manifest = parseAgentPluginManifest({ value: pluginManifest });
|
|
411
|
+
const manifest = parseAgentPluginManifest({ value: pluginManifest }, { identityNamespace: extensionNamespace });
|
|
410
412
|
if (!manifest.ok)
|
|
411
413
|
return {};
|
|
412
414
|
return readAgentPluginExtension({ manifest: manifest.manifest, namespace: extensionNamespace, read: ({ value }) => isJsonObject(value.mcpServers) ? value.mcpServers : {} }) ?? {};
|
package/dist/lifecycle/node.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomInt, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { performance } from 'node:perf_hooks';
|
|
5
5
|
import { setTimeout } from 'node:timers/promises';
|
|
6
|
+
import { readBootIdentity } from '@jini-ai/platform/fs/file-lock';
|
|
6
7
|
/** Granularity of the uniform [0, 1) value: crypto.randomInt's largest exclusive bound (2^48 - 1). */
|
|
7
8
|
const RANDOM_RESOLUTION = 2 ** 48 - 1;
|
|
8
9
|
/** Opt-in native effects. Layout, fetch, guard, namespace and provisioning remain host-owned. */
|
|
@@ -19,6 +20,7 @@ export function createNodeAgentPluginEffects(_required, optional = {}) {
|
|
|
19
20
|
pid: process.pid,
|
|
20
21
|
platform: process.platform,
|
|
21
22
|
hostname: () => os.hostname(),
|
|
23
|
+
bootIdentity: () => readBootIdentity({}),
|
|
22
24
|
isAlive: ({ pid }) => {
|
|
23
25
|
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
24
26
|
return false;
|
|
@@ -23,7 +23,7 @@ export function createPersistentStateModule(ports, _optional = {}) {
|
|
|
23
23
|
assertOwnedPath: (required, optional = {}) => assertPluginStatePath({ ...effects, ...required }, optional),
|
|
24
24
|
migrate: (required, optional = {}) => migratePluginLayout({ ...effects, ...required,
|
|
25
25
|
parsePluginId: ({ value }) => {
|
|
26
|
-
const parsed = parseAgentPluginManifest({ value });
|
|
26
|
+
const parsed = parseAgentPluginManifest({ value }, { identityNamespace: ports.extensionNamespace });
|
|
27
27
|
if (!parsed.ok)
|
|
28
28
|
throw new Error(parsed.errors.join('; '));
|
|
29
29
|
return parsed.manifest.name;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Clock, IdGenerator } from "@jini-ai/core/primitives";
|
|
2
|
+
import type { BootIdentity } from "@jini-ai/platform/fs/file-lock";
|
|
2
3
|
import type { AgentPluginServerMetadataReader } from './manifest.js';
|
|
3
4
|
import type { AgentPluginLayoutPort } from './layout.js';
|
|
4
5
|
import type { AgentPluginDeliveryMode, InstalledAgentPlugin } from './types.js';
|
|
@@ -24,6 +25,10 @@ export interface AgentPluginProcessPort {
|
|
|
24
25
|
isAlive(required: {
|
|
25
26
|
readonly pid: number;
|
|
26
27
|
}): boolean;
|
|
28
|
+
/** The kernel boot this process runs in, recorded in every lock it takes. Without it a lock left by
|
|
29
|
+
* a killed process is never provably this machine's once the hostname's spelling changes (macOS
|
|
30
|
+
* `name.lan`/`name.local`), and a never-ageing plugin state lock then wedges every boot. */
|
|
31
|
+
bootIdentity?(): BootIdentity | undefined;
|
|
27
32
|
}
|
|
28
33
|
/** The host implements federation in its own composition layer. No MCP dependency is required. */
|
|
29
34
|
export interface AgentPluginMcpProvisioningPort {
|
|
@@ -85,6 +90,9 @@ export interface AgentPluginLifecyclePorts {
|
|
|
85
90
|
readonly process: AgentPluginProcessPort;
|
|
86
91
|
readonly layout: AgentPluginLayoutPort;
|
|
87
92
|
readonly productName: string;
|
|
93
|
+
/** This host's own `extensions` key in plugin.json (README "Extension namespaces"). A plugin
|
|
94
|
+
* declares one entry per host it supports; the lifecycle reads only this key and never assumes
|
|
95
|
+
* a particular host's. Publisher ids are kept as declared: no alias, no rewrite between keys. */
|
|
88
96
|
readonly extensionNamespace: string;
|
|
89
97
|
/** Host boot policy may migrate every workspace before seeding the requested workspace.
|
|
90
98
|
* A failure refuses all publication; absent uses the owner's single-workspace migration. */
|
|
@@ -186,13 +186,16 @@ function buildModule(ports) {
|
|
|
186
186
|
const packing = typeof optional.packing === 'function'
|
|
187
187
|
? await optional.packing({ sourceDir: args.sourceDir, dirName: args.dirName }, {}) : optional.packing;
|
|
188
188
|
const packed = await packAgentPluginDirectory(args.sourceDir, packing);
|
|
189
|
+
// No revision pruning here, not even of the digest the ledger recorded last boot: that digest
|
|
190
|
+
// names the bytes this build once published, not what is on disk now, so an operator edit
|
|
191
|
+
// under it would be deleted. Every same-name copy stays; the ledger selects this build's.
|
|
189
192
|
const installed = await installAgentPlugin({
|
|
190
193
|
archive: packed.bytes,
|
|
191
194
|
expectedSha256: packed.sha256,
|
|
192
195
|
archiveReader: packed.archiveReader ?? createBundledSourceArchiveReader(),
|
|
193
196
|
layout: args.layout,
|
|
194
197
|
workspaceId: args.workspaceId,
|
|
195
|
-
});
|
|
198
|
+
}, { pruneSupersededRevisions: false });
|
|
196
199
|
const { recorded } = await recordBundledAgentPluginIfAbsent({ workspaceRoot: args.layout.forWorkspace({ workspaceId: args.workspaceId }).root, pluginId: installed.pluginId }, { enabled: ports.seededEnabledPluginIds.has(installed.pluginId) });
|
|
197
200
|
return {
|
|
198
201
|
pluginId: installed.pluginId,
|
package/dist/manifest.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export interface McpManifest {
|
|
|
59
59
|
}
|
|
60
60
|
export declare function isMcpManifest(value: unknown): value is McpManifest;
|
|
61
61
|
/** Object-argument facade for the legacy structural validator.
|
|
62
|
-
* Hosts inject identityNamespace; the default
|
|
62
|
+
* Hosts inject identityNamespace; the default is `jini`, with a legacy-key fallback. */
|
|
63
63
|
export declare function validatePluginManifest(required: {
|
|
64
64
|
readonly value: unknown;
|
|
65
65
|
}, optional?: {
|
package/dist/manifest.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validatePluginIdentity } from "@jini-ai/core/plugin-directory";
|
|
2
|
+
import { resolveAgentPluginIdentityNamespace } from './core/identity-namespace.js';
|
|
2
3
|
/**
|
|
3
4
|
* Types for the Agent Plugins 1.0.0 open standard (published by a Technical Steering Committee
|
|
4
5
|
* with maintainers from Amazon, Cursor, Google, Microsoft, OpenAI, and Vercel). A plugin is a
|
|
@@ -29,9 +30,9 @@ export function isMcpManifest(value) {
|
|
|
29
30
|
return typeof servers === 'object' && servers !== null && !Array.isArray(servers);
|
|
30
31
|
}
|
|
31
32
|
/** Object-argument facade for the legacy structural validator.
|
|
32
|
-
* Hosts inject identityNamespace; the default
|
|
33
|
+
* Hosts inject identityNamespace; the default is `jini`, with a legacy-key fallback. */
|
|
33
34
|
export function validatePluginManifest(required, optional = {}) {
|
|
34
|
-
return isPluginManifest(required.value) && validatePluginIdentity({ manifest: required.value, namespace: optional
|
|
35
|
+
return isPluginManifest(required.value) && validatePluginIdentity({ manifest: required.value, namespace: resolveAgentPluginIdentityNamespace(required, optional) }).errors.length === 0;
|
|
35
36
|
}
|
|
36
37
|
/** Object-argument facade for the legacy structural validator. */
|
|
37
38
|
export function validateMcpManifest(required) {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The Fly deploy-config generator: renders `fly.toml` from the host's `DeploymentDescriptor`
|
|
3
|
+
* (`[build]`, `[env]`, `[[mounts]]`, `[http_service]` with a check on the descriptor's health path).
|
|
4
|
+
* A host's CI can import this dependency-free module directly (the workflow template's generate
|
|
5
|
+
* step); there is no checked-in `fly.toml` to read. Host-specific runtime switches arrive in
|
|
6
|
+
* `options.env` and host notes are appended by the host after render: this module defaults none.
|
|
7
|
+
*
|
|
8
|
+
* No enumerated region allow-list here, unlike the Render/Railway renderers: Fly's region catalog is
|
|
9
|
+
* large (~30 codes) and had no authoritative source to enumerate against without risking rejecting a
|
|
10
|
+
* real one. Only presence is checked.
|
|
11
|
+
*
|
|
12
|
+
* Every quoted sink (`app = "${...}"`, `source = "${...}"`, `primary_region = "${...}"`) goes through
|
|
13
|
+
* the host kit's `assertNoConfigInjection` first: `--region 'iad"\nprimary_region_evil="x'` once closed
|
|
14
|
+
* the quoted value early and emitted a real extra `primary_region_evil` key into `fly.toml` (moved
|
|
15
|
+
* here from the host's kit doc; regression pinned in `__tests__/fly.test.mjs`).
|
|
16
|
+
*
|
|
17
|
+
* Plain JS by owner decision (2026-09-29). The host passes the kit: `assertNoConfigInjection`,
|
|
18
|
+
* `validationError(message)`, `migrationsNote`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {{ readonly name: string, readonly requirement: "boot-blocking" | "recommended" }} DeploymentSecret
|
|
23
|
+
* @typedef {{ readonly appName: string, readonly port: number, readonly dockerfilePath: string, readonly volumeMountPath: string, readonly volumeName: string, readonly healthCheckPath: string, readonly secrets: readonly DeploymentSecret[] }} DeploymentDescriptor
|
|
24
|
+
* @typedef {{ readonly region: string, readonly env?: Readonly<Record<string, string>>, readonly buildArgs?: Readonly<Record<string, string>> }} RenderDeployConfigOptions
|
|
25
|
+
* @typedef {{ readonly assertNoConfigInjection: (fieldLabel: string, value: string) => void, readonly validationError: (message: string) => Error, readonly migrationsNote: string }} DeployConfigKit
|
|
26
|
+
* @typedef {{ readonly filename: string, readonly contents: string, readonly notes: readonly string[] }} RenderedDeployConfig
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Serializes explicitly supplied non-secret variables as TOML assignments. JSON string quoting
|
|
31
|
+
* also escapes TOML basic strings, including URLs, backslashes, quotes and control characters.
|
|
32
|
+
* @param {{ values: Readonly<Record<string, string>>, secrets: readonly DeploymentSecret[], label: string }} input
|
|
33
|
+
* @param {DeployConfigKit} kit Error factory supplied by the host.
|
|
34
|
+
* @returns {string} Indented assignments, or an empty string for an empty map.
|
|
35
|
+
* @throws {Error} For invalid variable names, non-string values or declared secret names.
|
|
36
|
+
* @complexity O(n + s), where n is the total variable text and s the secret count.
|
|
37
|
+
*/
|
|
38
|
+
function renderVariables({ values, secrets, label }, kit) {
|
|
39
|
+
const secretNames = new Set(secrets.map(secret => secret.name));
|
|
40
|
+
return Object.entries(values).map(([name, value]) => {
|
|
41
|
+
if (!/^[A-Za-z_]\w*$/.test(name) || typeof value !== "string" || secretNames.has(name)) {
|
|
42
|
+
throw kit.validationError(`${label} must contain only non-secret string variables with valid names`);
|
|
43
|
+
}
|
|
44
|
+
// JSON permits a literal DEL character; TOML requires it to be escaped too.
|
|
45
|
+
return ` ${name} = ${JSON.stringify(value).replace(/\u007f/g, "\\u007f")}\n`;
|
|
46
|
+
}).join("");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Renders the descriptor and optional non-secret env/build arguments without I/O. The established
|
|
51
|
+
* three-argument generator port is retained for compatibility with every hosting plugin.
|
|
52
|
+
* @param {DeploymentDescriptor} descriptor
|
|
53
|
+
* @param {RenderDeployConfigOptions} options
|
|
54
|
+
* @param {DeployConfigKit} kit
|
|
55
|
+
* @returns {RenderedDeployConfig}
|
|
56
|
+
* @throws {Error} The host's validation error for missing/unsafe identifiers or invalid variables.
|
|
57
|
+
* @complexity O(n + s), where n is variable text and s the declared secret count.
|
|
58
|
+
*/
|
|
59
|
+
function render(descriptor, options, kit) {
|
|
60
|
+
const region = options.region.trim();
|
|
61
|
+
if (!region) {
|
|
62
|
+
throw kit.validationError("--region is required for --target fly (no default region is assumed)");
|
|
63
|
+
}
|
|
64
|
+
kit.assertNoConfigInjection("--region", region);
|
|
65
|
+
kit.assertNoConfigInjection("the derived Fly app name (fly.toml's app)", descriptor.appName);
|
|
66
|
+
kit.assertNoConfigInjection("the derived Fly volume name (fly.toml's [[mounts]].source)", descriptor.volumeName);
|
|
67
|
+
|
|
68
|
+
const buildArgs = renderVariables({ values: options.buildArgs ?? {}, secrets: descriptor.secrets, label: "buildArgs" }, kit);
|
|
69
|
+
const env = renderVariables({
|
|
70
|
+
values: { PORT: String(descriptor.port), ...options.env },
|
|
71
|
+
secrets: descriptor.secrets,
|
|
72
|
+
label: "env",
|
|
73
|
+
}, kit);
|
|
74
|
+
|
|
75
|
+
const contents = `app = "${descriptor.appName}"
|
|
76
|
+
primary_region = "${region}"
|
|
77
|
+
|
|
78
|
+
[build]
|
|
79
|
+
dockerfile = "${descriptor.dockerfilePath}"
|
|
80
|
+
|
|
81
|
+
${buildArgs ? `[build.args]\n${buildArgs}\n` : ""}[env]
|
|
82
|
+
${env.trimEnd()}
|
|
83
|
+
|
|
84
|
+
[[mounts]]
|
|
85
|
+
source = "${descriptor.volumeName}"
|
|
86
|
+
destination = "${descriptor.volumeMountPath}"
|
|
87
|
+
|
|
88
|
+
[http_service]
|
|
89
|
+
internal_port = ${descriptor.port}
|
|
90
|
+
force_https = true
|
|
91
|
+
auto_stop_machines = false
|
|
92
|
+
auto_start_machines = true
|
|
93
|
+
min_machines_running = 1
|
|
94
|
+
|
|
95
|
+
[[http_service.checks]]
|
|
96
|
+
grace_period = "60s"
|
|
97
|
+
interval = "30s"
|
|
98
|
+
method = "GET"
|
|
99
|
+
timeout = "5s"
|
|
100
|
+
path = "${descriptor.healthCheckPath}"
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const notes = [
|
|
104
|
+
`Create the volume before the first deploy — this file only MOUNTS it, it does not create it: ` +
|
|
105
|
+
`fly volumes create ${descriptor.volumeName} --region ${region} -a ${descriptor.appName}`,
|
|
106
|
+
kit.migrationsNote,
|
|
107
|
+
...descriptor.secrets.map(
|
|
108
|
+
(secret) => `Set ${secret.name} (${secret.requirement}) with: fly secrets set ${secret.name}=<value> -a ${descriptor.appName}`
|
|
109
|
+
),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
return { filename: "fly.toml", contents, notes };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default { render };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Fly observer and app-secrets adapter. Auth, HTTP policy, bounded responses and audit belong to the host. */
|
|
2
|
+
const text = value => String(value ?? "").slice(0, 2000);
|
|
3
|
+
/** An app is a single path segment; encode only after validation to prevent path injection. */
|
|
4
|
+
function app(ctx, target) {
|
|
5
|
+
if (typeof target !== "string" || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(target)) ctx.fail("Target must be a Fly app name.");
|
|
6
|
+
return encodeURIComponent(target);
|
|
7
|
+
}
|
|
8
|
+
/** Reduce mixed machine states without treating unchecked warnings or unknown states as health. */
|
|
9
|
+
function overall(machines) {
|
|
10
|
+
if (machines.some(m => m.checks?.some(c => c.status === "critical"))) return "failing";
|
|
11
|
+
if (machines.some(m => ["replacing", "starting", "updating"].includes(m.state))) return "deploying";
|
|
12
|
+
if (machines.some(m => !["started", "stopped", "suspended", "created", "destroyed", "destroying", "stopping"].includes(m.state))) return "unknown";
|
|
13
|
+
const started = machines.filter(m => m.state === "started");
|
|
14
|
+
if (!started.length) return "stopped";
|
|
15
|
+
return started.every(m => (m.checks ?? []).every(c => c.status === "passing")) ? "healthy" : "unknown";
|
|
16
|
+
}
|
|
17
|
+
/** Prefer the resolved image over a mutable config tag when the Machines API supplies it. */
|
|
18
|
+
function imageRef(machine) {
|
|
19
|
+
const ref = machine.image_ref;
|
|
20
|
+
if (typeof ref === "string") return ref;
|
|
21
|
+
if (ref?.repository) {
|
|
22
|
+
const registry = ref.registry ? `${ref.registry}/` : "";
|
|
23
|
+
const tag = ref.tag ? `:${ref.tag}` : "";
|
|
24
|
+
const digest = ref.digest ? `@${ref.digest}` : "";
|
|
25
|
+
return `${registry}${ref.repository}${tag}${digest}`;
|
|
26
|
+
}
|
|
27
|
+
return machine.config?.image;
|
|
28
|
+
}
|
|
29
|
+
/** Machine checks are optional, but every supplied check must carry a status. @complexity O(checks). */
|
|
30
|
+
function invalidChecks({ checks }, _options = {}) {
|
|
31
|
+
return checks !== undefined && (!Array.isArray(checks) || checks.some(c => !c || typeof c.status !== "string"));
|
|
32
|
+
}
|
|
33
|
+
/** Return only machine identity/state and a bounded detail with region, image, checks and update time. */
|
|
34
|
+
async function status(ctx, { target }) {
|
|
35
|
+
const response = await ctx.get(`https://api.machines.dev/v1/apps/${app(ctx, target)}/machines`);
|
|
36
|
+
if (!Array.isArray(response.json)) ctx.fail("Machine status response must be an array.");
|
|
37
|
+
if (response.json.some(m => !m || typeof m.id !== "string" || typeof m.state !== "string" || invalidChecks({ checks: m.checks }))) ctx.fail("Machine status response must be an array.");
|
|
38
|
+
const machines = response.json; const state = overall(machines);
|
|
39
|
+
return { platform: "fly", target, state, summary: `${machines.length} machines: ${state}`, items: machines.slice(0, 500).map(m => ({ id: text(m.id), state: text(m.state), detail: machineDetail({ machine: m }) })), checkedAt: ctx.nowIso() };
|
|
40
|
+
}
|
|
41
|
+
/** Bounded display detail; check names and statuses use the same per-field cap. @complexity O(checks). */
|
|
42
|
+
function machineDetail({ machine }, _options = {}) {
|
|
43
|
+
const checks = (machine.checks ?? []).map(c => `${text(c.name)}:${text(c.status)}`).join(",");
|
|
44
|
+
return text(`region=${text(machine.region)}; image=${text(imageRef(machine))}; checks=${checks}; updated_at=${text(machine.updated_at)}`);
|
|
45
|
+
}
|
|
46
|
+
function flyLogOrigin({ entry }, _options = {}) {
|
|
47
|
+
return { ...(entry.timestamp || entry.at ? { at: text(entry.timestamp ?? entry.at) } : {}), ...(entry.instance || entry.source ? { source: text(entry.instance ?? entry.source) } : {}) };
|
|
48
|
+
}
|
|
49
|
+
function flyLogLine({ entry }, _options = {}) {
|
|
50
|
+
if (typeof entry === "string") return { message: text(entry) };
|
|
51
|
+
return { ...flyLogOrigin({ entry }), ...(entry.level ? { level: text(entry.level) } : {}), message: text(entry.message ?? entry.msg ?? "") };
|
|
52
|
+
}
|
|
53
|
+
/** HTTP logs endpoint not verified live. Accept array, logs wrapper, JSON lines or plain log text. */
|
|
54
|
+
async function logs(ctx, { target, limit }) {
|
|
55
|
+
const response = await ctx.get(`https://api.fly.io/api/v1/apps/${app(ctx, target)}/logs`);
|
|
56
|
+
let entries = Array.isArray(response.json) ? response.json : response.json?.logs;
|
|
57
|
+
if (!Array.isArray(entries)) entries = response.text.split(/\r?\n/).filter(Boolean).map(line => { try { return JSON.parse(line); } catch { return { message: line }; } });
|
|
58
|
+
if (entries.some(entry => entry === null || (typeof entry !== "string" && (typeof entry !== "object" || Array.isArray(entry))))) ctx.fail("Log response contains an invalid entry.");
|
|
59
|
+
const lines = entries.slice(0, limit).map(entry => flyLogLine({ entry }));
|
|
60
|
+
return { platform: "fly", target, lines, truncated: response.truncated === true || entries.length > limit || entries.some(e => String(typeof e === "string" ? e : e.message ?? e.msg ?? "").length > 2000) };
|
|
61
|
+
}
|
|
62
|
+
/** List apps in personal (default) or the explicitly supplied organization. No credential is exposed. */
|
|
63
|
+
async function listTargets(ctx, { org = "personal" } = {}) {
|
|
64
|
+
const response = await ctx.get(`https://api.machines.dev/v1/apps?org_slug=${encodeURIComponent(org)}`);
|
|
65
|
+
const apps = Array.isArray(response.json) ? response.json : response.json?.apps;
|
|
66
|
+
if (!Array.isArray(apps)) ctx.fail("App list response must contain an apps array.");
|
|
67
|
+
if (apps.some(a => !a || typeof a.name !== "string" || !a.name)) ctx.fail("App list response contains an invalid app.");
|
|
68
|
+
return { platform: "fly", targets: apps.slice(0, 500).map(a => ({ id: text(a.name), name: text(a.name), ...(a.status ? { state: text(a.status) } : {}) })) };
|
|
69
|
+
}
|
|
70
|
+
/** App secrets live under one Machines API collection; names are env-var shaped before encoding. */
|
|
71
|
+
const secretsUrl = (ctx, target) => `https://api.machines.dev/v1/apps/${app(ctx, target)}/secrets`;
|
|
72
|
+
function secretName(ctx, name) {
|
|
73
|
+
if (typeof name !== "string" || !/^[A-Za-z_]\w{0,127}$/.test(name)) ctx.fail("Secret name must be an environment variable name.");
|
|
74
|
+
return encodeURIComponent(name);
|
|
75
|
+
}
|
|
76
|
+
/** Names, Fly digests and update times only. Values are never requested by a listing. */
|
|
77
|
+
async function listSecrets(ctx, { target }) {
|
|
78
|
+
const response = await ctx.get(secretsUrl(ctx, target));
|
|
79
|
+
const secrets = Array.isArray(response.json) ? response.json : response.json?.secrets;
|
|
80
|
+
if (!Array.isArray(secrets) || secrets.some(s => !s || typeof s.name !== "string" || !s.name)) ctx.fail("Secret list response must contain a secrets array.");
|
|
81
|
+
return { secrets: secrets.slice(0, 500).map(s => ({ name: text(s.name), ...(typeof s.digest === "string" ? { digest: text(s.digest) } : {}), ...(typeof s.updated_at === "string" ? { updatedAt: text(s.updated_at) } : {}) })), truncated: secrets.length > 500 };
|
|
82
|
+
}
|
|
83
|
+
/** Host-only value read for compare/copy. A name absent from the listing is reported, not fetched (Fly 404s it). */
|
|
84
|
+
async function readSecret(ctx, { target, name }) {
|
|
85
|
+
const encoded = secretName(ctx, name);
|
|
86
|
+
if (!(await listSecrets(ctx, { target })).secrets.some(s => s.name === name)) return { absent: true };
|
|
87
|
+
const response = await ctx.get(`${secretsUrl(ctx, target)}/${encoded}?show_secrets=true`);
|
|
88
|
+
if (typeof response.json?.value !== "string") ctx.fail("Fly did not return the secret's value; the token may lack permission to read secrets.");
|
|
89
|
+
return { value: response.json.value };
|
|
90
|
+
}
|
|
91
|
+
/** POST stages the value: running machines keep the old one until the next deploy or machine update. */
|
|
92
|
+
async function setSecret(ctx, { target, name, value }) {
|
|
93
|
+
const response = await ctx.send({ method: "POST", url: `${secretsUrl(ctx, target)}/${secretName(ctx, name)}`, body: { value } });
|
|
94
|
+
return { ...(Number.isInteger(response.json?.version) ? { version: response.json.version } : {}) };
|
|
95
|
+
}
|
|
96
|
+
/** DELETE is staged the same way; the variable stays in running machines until the next deploy. */
|
|
97
|
+
async function unsetSecret(ctx, { target, name }) {
|
|
98
|
+
const response = await ctx.send({ method: "DELETE", url: `${secretsUrl(ctx, target)}/${secretName(ctx, name)}` });
|
|
99
|
+
return { ...(Number.isInteger(response.json?.version) ? { version: response.json.version } : {}) };
|
|
100
|
+
}
|
|
101
|
+
const secretCapabilities = { appliesOn: "next-deploy", supportsStaging: true };
|
|
102
|
+
export default { status, logs, listTargets, secretCapabilities, listSecrets, readSecret, setSecret, unsetSecret };
|
package/fly/mcp.json
ADDED
package/fly/plugin.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "fly",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Deploys the host's server to fly.io: one machine only for volume-backed state, secrets never in fly.toml, deploying ships code, not content. Contributes Fly operations, server config generation and the FlyV1 credential scheme.",
|
|
6
|
+
"author": "Tovu",
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"extensions": {
|
|
9
|
+
"tovu": {
|
|
10
|
+
"vendorAliases": ["FLY_", "fly.io", "flyctl", "fly.toml", "machines.dev", "flyv1"],
|
|
11
|
+
"displayName": "Fly.io",
|
|
12
|
+
"summary": "Puts the Tovu server itself online on Fly.io.",
|
|
13
|
+
"hostSignals": [
|
|
14
|
+
{
|
|
15
|
+
"env": "FLY_APP_NAME",
|
|
16
|
+
"facts": { "hosted": true, "trustProxyHops": 1, "nativeToolsRestricted": true },
|
|
17
|
+
"label": "Fly.io (FLY_APP_NAME)"
|
|
18
|
+
},
|
|
19
|
+
{ "env": "FLY_MACHINE_ID", "facts": { "nativeToolsRestricted": true } },
|
|
20
|
+
{ "env": "FLY_ALLOC_ID", "facts": { "nativeToolsRestricted": true } }
|
|
21
|
+
],
|
|
22
|
+
"deploymentAdvice": "Set TOVU_RUNTIME_MODE=production (fly.toml and docker-compose.yml already do).",
|
|
23
|
+
"id": "tovu/fly",
|
|
24
|
+
"provides": [
|
|
25
|
+
"deploy-ops:fly",
|
|
26
|
+
"deploy-config:fly",
|
|
27
|
+
"credential-scheme:fly-io"
|
|
28
|
+
],
|
|
29
|
+
"credentialCards": [
|
|
30
|
+
{
|
|
31
|
+
"target": "fly",
|
|
32
|
+
"title": "Connect Fly.io",
|
|
33
|
+
"label": "fly.io",
|
|
34
|
+
"ownedApiLabel": "fly.io",
|
|
35
|
+
"category": "hosting",
|
|
36
|
+
"intro": "Connect Fly.io to inspect and deploy the Tovu server and manage its Machines.",
|
|
37
|
+
"usedFor": [
|
|
38
|
+
"Deploys your Tovu server and checks its status and logs",
|
|
39
|
+
"Manages the app's running servers",
|
|
40
|
+
"Creates and manages apps when you grant access to the organization"
|
|
41
|
+
],
|
|
42
|
+
"keyHint": "Use a deploy token scoped to this app. Use an org-scoped token only when creating or managing apps across that organization. In the Fly dashboard, select the app or organization, then Tokens. Avoid the short-lived login auth token.",
|
|
43
|
+
"saveKind": "api",
|
|
44
|
+
"additionalHosts": [
|
|
45
|
+
"https://api.machines.dev"
|
|
46
|
+
],
|
|
47
|
+
"baseUrl": "https://api.fly.io"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"toolKeywords": {
|
|
51
|
+
"credential_save": "fly flyio fly.io fly api token hosting account",
|
|
52
|
+
"custom_credential_list": "flyio fly.io",
|
|
53
|
+
"custom_credential_make_request": "fly.io",
|
|
54
|
+
"custom_credential_verify": "fly.io",
|
|
55
|
+
"deployment_ops_deploy": "fly",
|
|
56
|
+
"deployment_ops_list_secrets": "fly",
|
|
57
|
+
"deployment_ops_list_targets": "fly",
|
|
58
|
+
"deployment_ops_logs": "fly",
|
|
59
|
+
"deployment_ops_set_secret": "fly",
|
|
60
|
+
"deployment_ops_status": "fly fly.io",
|
|
61
|
+
"deployment_ops_unset_secret": "fly",
|
|
62
|
+
"search_agent_plugin_local": "fly.io"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"deploy",
|
|
68
|
+
"hosting",
|
|
69
|
+
"fly",
|
|
70
|
+
"fly.io",
|
|
71
|
+
"flyctl",
|
|
72
|
+
"machines",
|
|
73
|
+
"server",
|
|
74
|
+
"volume",
|
|
75
|
+
"secrets",
|
|
76
|
+
"readyz",
|
|
77
|
+
"deploy-ops:fly",
|
|
78
|
+
"deploy-config:fly",
|
|
79
|
+
"credential-scheme:fly-io"
|
|
80
|
+
]
|
|
81
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file Renders `skills/fly/references/fly-deploy.template.yml` into a GitHub Actions deploy workflow.
|
|
4
|
+
*
|
|
5
|
+
* One template serves two readers. An operator's assistant copies it by hand and fills its
|
|
6
|
+
* `<<PLACEHOLDER: ...>>` values; a host's own production deploy is rendered from it with that host's
|
|
7
|
+
* values file, so the shared parts of the workflow live here in the Fly plugin and only the host's
|
|
8
|
+
* own parts live with the host. The host's committed workflow is OUTPUT: the host's drift test
|
|
9
|
+
* fails while it differs from a fresh render.
|
|
10
|
+
*
|
|
11
|
+
* Template regions, one marker per line: `#>> <name>` ... `#<< <name>`. A region holds the operator
|
|
12
|
+
* default. When the values file names a block for `<name>`, the block's file replaces the whole
|
|
13
|
+
* region; otherwise the default stays. Regions do not nest. Every placeholder, in the template and
|
|
14
|
+
* in the blocks, must have a value; a missing one throws rather than shipping a workflow that
|
|
15
|
+
* deploys `<<PLACEHOLDER: ...>>`.
|
|
16
|
+
*
|
|
17
|
+
* Values file (JSON): `{ output, placeholders: { "<<PLACEHOLDER: ...>>": value }, blocks: { name:
|
|
18
|
+
* "relative/file.yml" } }`. Block paths resolve against the values file's own folder; `output` is
|
|
19
|
+
* relative to `--repo-root`. Nothing about any host is defaulted here.
|
|
20
|
+
*
|
|
21
|
+
* Usage: `node render-deploy-workflow.mjs --values <file> --repo-root <dir> [--check]` writes the
|
|
22
|
+
* values file's `output`, or with `--check` exits 1 when the committed file differs. A missing
|
|
23
|
+
* `--values` or `--repo-root` exits 2.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
|
|
29
|
+
const OPEN = /^#>> ([a-z0-9-]+)$/;
|
|
30
|
+
const CLOSE = /^#<< ([a-z0-9-]+)$/;
|
|
31
|
+
const PLACEHOLDER = /<<PLACEHOLDER:[^>]+>>/g;
|
|
32
|
+
const USAGE = "usage: render-deploy-workflow.mjs --values <file> --repo-root <dir> [--check]";
|
|
33
|
+
|
|
34
|
+
export const PLUGIN_ROOT = path.resolve(import.meta.dirname, "..");
|
|
35
|
+
export const TEMPLATE_PATH = path.join(PLUGIN_ROOT, "skills", "fly", "references", "fly-deploy.template.yml");
|
|
36
|
+
|
|
37
|
+
/** Opens a region; a second open before the first closes is a template error. */
|
|
38
|
+
function openRegion({ current, name, lineNumber }) {
|
|
39
|
+
if (current) throw new Error(`Line ${lineNumber}: region '${name}' opened inside '${current}'`);
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function closeRegion({ current, name, lineNumber }) {
|
|
44
|
+
if (current !== name) throw new Error(`Line ${lineNumber}: '#<< ${name}' does not close an open region`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function substitute({ line, placeholders }) {
|
|
49
|
+
return line.replace(PLACEHOLDER, (token) => {
|
|
50
|
+
if (!Object.hasOwn(placeholders, token)) throw new Error(`No value for ${token}`);
|
|
51
|
+
return placeholders[token];
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A block's text as lines; the file's own trailing newline is not an extra empty line. */
|
|
56
|
+
function blockLines(text) {
|
|
57
|
+
return text === "" ? [] : text.replace(/\r\n/g, "\n").replace(/\n$/, "").split("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Lines one template line contributes: the block on the opening marker of a replaced region,
|
|
62
|
+
* nothing for any other marker or a replaced default, the line itself otherwise.
|
|
63
|
+
*/
|
|
64
|
+
function emitted({ line, region, open, blocks }) {
|
|
65
|
+
if (open) return Object.hasOwn(blocks, open) ? blockLines(blocks[open]) : [];
|
|
66
|
+
if (region !== null && Object.hasOwn(blocks, region)) return [];
|
|
67
|
+
return [line];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Renders the template text. `placeholders` maps each full `<<PLACEHOLDER: ...>>` token to its value;
|
|
72
|
+
* `blocks` maps region names to replacement text. A block for a region the template lacks throws,
|
|
73
|
+
* so a stale values file cannot silently drop a host step.
|
|
74
|
+
* @complexity O(lines).
|
|
75
|
+
*/
|
|
76
|
+
export function renderDeployWorkflow({ template, placeholders }, { blocks = {} } = {}) {
|
|
77
|
+
const out = [];
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
let region = null;
|
|
80
|
+
template.replace(/\r\n/g, "\n").split("\n").forEach((line, index) => {
|
|
81
|
+
const lineNumber = index + 1;
|
|
82
|
+
const open = OPEN.exec(line)?.[1] ?? null;
|
|
83
|
+
const close = open ? null : CLOSE.exec(line)?.[1] ?? null;
|
|
84
|
+
if (close) {
|
|
85
|
+
region = closeRegion({ current: region, name: close, lineNumber });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (open) {
|
|
89
|
+
region = openRegion({ current: region, name: open, lineNumber });
|
|
90
|
+
seen.add(open);
|
|
91
|
+
}
|
|
92
|
+
out.push(...emitted({ line, region, open, blocks }));
|
|
93
|
+
});
|
|
94
|
+
if (region) throw new Error(`Region '${region}' is never closed`);
|
|
95
|
+
const unknown = Object.keys(blocks).filter((name) => !seen.has(name));
|
|
96
|
+
if (unknown.length) throw new Error(`No template region for block(s): ${unknown.join(", ")}`);
|
|
97
|
+
return out.map((line) => substitute({ line, placeholders })).join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Reads every block file the values name, relative to the values file's folder. */
|
|
101
|
+
function readBlocks({ readFile, valuesPath, blocks = {} }) {
|
|
102
|
+
const folder = path.dirname(valuesPath);
|
|
103
|
+
return Object.fromEntries(Object.entries(blocks).map(([name, file]) => [name, readFile(path.resolve(folder, file))]));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Renders a host's workflow from the template and that host's values file.
|
|
108
|
+
* @complexity O(template lines + block bytes).
|
|
109
|
+
*/
|
|
110
|
+
export function renderFromValues({ readFile, valuesPath }, { templatePath = TEMPLATE_PATH } = {}) {
|
|
111
|
+
const values = JSON.parse(readFile(valuesPath));
|
|
112
|
+
const blocks = readBlocks({ readFile, valuesPath, blocks: values.blocks });
|
|
113
|
+
const contents = renderDeployWorkflow({ template: readFile(templatePath), placeholders: values.placeholders ?? {} }, { blocks });
|
|
114
|
+
return { output: values.output, contents };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The value after `name` in argv, or undefined. */
|
|
118
|
+
function flag(argv, name) {
|
|
119
|
+
const index = argv.indexOf(name);
|
|
120
|
+
return index === -1 ? undefined : argv[index + 1];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** CLI: write (default) or `--check` the rendered workflow. Returns the exit code. */
|
|
124
|
+
export function main({ argv, readFile, writeFile, log }, { templatePath = TEMPLATE_PATH } = {}) {
|
|
125
|
+
const valuesPath = flag(argv, "--values");
|
|
126
|
+
const repoRoot = flag(argv, "--repo-root");
|
|
127
|
+
if (!valuesPath || !repoRoot) {
|
|
128
|
+
log(USAGE);
|
|
129
|
+
return 2;
|
|
130
|
+
}
|
|
131
|
+
const { output, contents } = renderFromValues({ readFile, valuesPath }, { templatePath });
|
|
132
|
+
const target = path.join(repoRoot, output);
|
|
133
|
+
if (!argv.includes("--check")) {
|
|
134
|
+
writeFile(target, contents);
|
|
135
|
+
log(`Rendered ${output}`);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if (readFile(target) === contents) return 0;
|
|
139
|
+
log(`${output} differs from its render; re-run without --check`);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
144
|
+
process.exitCode = main({
|
|
145
|
+
argv: process.argv.slice(2),
|
|
146
|
+
readFile: (file) => readFileSync(file, "utf8"),
|
|
147
|
+
writeFile: (file, text) => writeFileSync(file, text),
|
|
148
|
+
log: (message) => console.log(message),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fly
|
|
3
|
+
description: Deploy the host's server to fly.io with no CLI installed (flyctl runs on GitHub's runner) - one machine only for volume-backed state, secrets never in fly.toml, and deploying ships code, not content. Publishing a static export to a host is not this plugin's job - each host is its own plugin.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Deploy the server to fly.io
|
|
7
|
+
|
|
8
|
+
Follow the host guidance first: it names the server's own rules (secrets, mount path, port,
|
|
9
|
+
production switch, first-boot steps) and its full procedure. This skill holds what is true for
|
|
10
|
+
any server on Fly.
|
|
11
|
+
|
|
12
|
+
## Connection card
|
|
13
|
+
|
|
14
|
+
Choose the card yourself from its `usedFor` list. Reuse a saved key or working sign-in for that
|
|
15
|
+
task. Never offer a replacement unless the person asks.
|
|
16
|
+
|
|
17
|
+
Open **Connect Fly.io** with `credential_save {kind:"plugin-provider",target:"fly"}` for a new key. The card explains app-scoped deploy tokens and saves the `fly.io` credential with both API origins. In the Fly dashboard select the real app or organization, then **Tokens**; there is no universal app token-creation URL to guess. Only if the person explicitly asks to replace the saved key, rotate the existing row through `credential_save {kind:"api",target:"fly.io"}`.
|
|
18
|
+
|
|
19
|
+
## Scope
|
|
20
|
+
|
|
21
|
+
This plugin deploys **the server itself** (app, admin, database), not a static export. Static hosts
|
|
22
|
+
are their own plugins.
|
|
23
|
+
|
|
24
|
+
## Deploying to fly.io
|
|
25
|
+
|
|
26
|
+
The server goes on one fly.io machine through a GitHub Actions workflow, so nobody installs flyctl
|
|
27
|
+
or Docker. Say first: deploying ships code, not content. Rules that generic fly.io advice gets wrong:
|
|
28
|
+
|
|
29
|
+
- **One machine only** when state lives on a Fly volume (SQLite): a volume attaches to one machine,
|
|
30
|
+
so a second machine gets its own divergent database. Never scale above 1 or autoscale.
|
|
31
|
+
- **The volume hides the image's files at its mount path.** Nothing the server reads at boot may
|
|
32
|
+
live there in the image.
|
|
33
|
+
- **Secrets go through `flyctl secrets set`**, never `fly.toml`, which is committed and public.
|
|
34
|
+
- **The volume exists before the first deploy.** The workflow mounts it; it does not create it.
|
|
35
|
+
|
|
36
|
+
The two templates the procedure writes are `references/fly.template.toml` and
|
|
37
|
+
`references/fly-deploy.template.yml` (read them with this plugin's own tool,
|
|
38
|
+
`file: "references/<name>"`). Every `<<PLACEHOLDER: ...>>` must be replaced; re-read the written
|
|
39
|
+
files and confirm none survives. Every GitHub step belongs to the host's GitHub plugin.
|
|
40
|
+
|
|
41
|
+
## When it fails
|
|
42
|
+
|
|
43
|
+
Explain the cause in one plain sentence and the one next step.
|