@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.
Files changed (335) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/NOTICE +14 -0
  3. package/README.md +35 -14
  4. package/charts-echarts/plugin.json +39 -0
  5. package/charts-echarts/skills/charts-echarts/SKILL.md +110 -0
  6. package/charts-recharts/plugin.json +32 -0
  7. package/charts-recharts/skills/charts-recharts/SKILL.md +108 -0
  8. package/cloudflare/NOTES.md +16 -0
  9. package/cloudflare/form-destinations/cloudflare-d1.mjs +9 -0
  10. package/cloudflare/lib/api.mjs +96 -0
  11. package/cloudflare/lib/assets.mjs +65 -0
  12. package/cloudflare/lib/dns.mjs +32 -0
  13. package/cloudflare/lib/domain-inventory.mjs +61 -0
  14. package/cloudflare/lib/domain-status.mjs +48 -0
  15. package/cloudflare/lib/domains.mjs +95 -0
  16. package/cloudflare/lib/forms.mjs +254 -0
  17. package/cloudflare/lib/pages-upload.mjs +51 -0
  18. package/cloudflare/lib/target.mjs +165 -0
  19. package/cloudflare/lib/workers-upload.mjs +48 -0
  20. package/cloudflare/lib/zones.mjs +25 -0
  21. package/cloudflare/mcp.json +4 -0
  22. package/cloudflare/plugin.json +96 -0
  23. package/cloudflare/skills/cloudflare/SKILL.md +57 -0
  24. package/cloudflare/skills/cloudflare/references/domains.md +31 -0
  25. package/cloudflare/skills/cloudflare/references/pages.md +18 -0
  26. package/cloudflare/skills/cloudflare/references/permissions.md +29 -0
  27. package/cloudflare/skills/cloudflare/references/troubleshooting.md +20 -0
  28. package/cloudflare/skills/cloudflare/references/workers.md +42 -0
  29. package/cloudflare/targets/cloudflare-pages.mjs +12 -0
  30. package/cloudflare/targets/cloudflare-workers.mjs +17 -0
  31. package/cloudflare/tovu-consent.json +6 -0
  32. package/cloudflare/tovu-deploy-targets.json +630 -0
  33. package/cloudflare/tovu-form-destinations.json +12 -0
  34. package/cloudflare/worker/forms-receiver.mjs +249 -0
  35. package/composio/mcp.json +10 -0
  36. package/composio/plugin.json +68 -0
  37. package/composio/skills/composio/SKILL.md +85 -0
  38. package/dist/core/identity-namespace.d.ts +12 -0
  39. package/dist/core/identity-namespace.js +20 -0
  40. package/dist/folder-zip-node.js +29 -7
  41. package/dist/lifecycle/bundled-digests.js +4 -3
  42. package/dist/lifecycle/install.d.ts +7 -0
  43. package/dist/lifecycle/install.js +45 -3
  44. package/dist/lifecycle/manifest.d.ts +2 -2
  45. package/dist/lifecycle/manifest.js +5 -3
  46. package/dist/lifecycle/node.js +2 -0
  47. package/dist/lifecycle/persistent-state.js +1 -1
  48. package/dist/lifecycle/ports.d.ts +8 -0
  49. package/dist/lifecycle/seed-bundled.js +4 -1
  50. package/dist/manifest.d.ts +1 -1
  51. package/dist/manifest.js +3 -2
  52. package/fly/deploy-configs/fly.mjs +115 -0
  53. package/fly/deploy-ops/fly.mjs +102 -0
  54. package/fly/mcp.json +4 -0
  55. package/fly/plugin.json +81 -0
  56. package/fly/scripts/render-deploy-workflow.mjs +150 -0
  57. package/fly/skills/fly/SKILL.md +43 -0
  58. package/fly/skills/fly/references/fly-deploy.template.yml +117 -0
  59. package/fly/skills/fly/references/fly.template.toml +81 -0
  60. package/fly/tovu-credential-schemes.json +11 -0
  61. package/fly/tovu-deploy-configs.json +21 -0
  62. package/fly/tovu-deploy-ops.json +14 -0
  63. package/github/deploy-ops/github-actions.mjs +164 -0
  64. package/github/deployments/github.mjs +415 -0
  65. package/github/mcp.json +4 -0
  66. package/github/plugin.json +105 -0
  67. package/github/skills/github/SKILL.md +249 -0
  68. package/github/skills/github/references/actions.md +224 -0
  69. package/github/skills/github/references/auth-and-tokens.md +140 -0
  70. package/github/skills/github/references/repo-files.md +135 -0
  71. package/github/skills/github/references/site-backup.md +159 -0
  72. package/github/skills/github/references/source-control.md +123 -0
  73. package/github/source-control/backup-push.mjs +242 -0
  74. package/github/source-control/commit-site.mjs +459 -0
  75. package/github/source-control/github.mjs +93 -0
  76. package/github/source-control/write-files.mjs +468 -0
  77. package/github/tovu-deploy-ops.json +13 -0
  78. package/github/tovu-source-control.json +117 -0
  79. package/github-pages/NOTES.md +19 -0
  80. package/github-pages/mcp.json +4 -0
  81. package/github-pages/plugin.json +48 -0
  82. package/github-pages/skills/github-pages/SKILL.md +44 -0
  83. package/github-pages/targets/github-pages.mjs +543 -0
  84. package/github-pages/tovu-deploy-targets.json +327 -0
  85. package/google-login/exports/google-g.svg +1 -0
  86. package/google-login/exports/sign-in-button.css +68 -0
  87. package/google-login/exports/sign-in-button.html +4 -0
  88. package/google-login/mcp.json +4 -0
  89. package/google-login/plugin.json +59 -0
  90. package/google-login/skills/google-login/SKILL.md +96 -0
  91. package/google-login/tovu-login-providers.json +21 -0
  92. package/google-workspace/README.md +132 -0
  93. package/google-workspace/form-destinations/README.md +87 -0
  94. package/google-workspace/form-destinations/dedupe.mjs +43 -0
  95. package/google-workspace/form-destinations/delivery-result.mjs +69 -0
  96. package/google-workspace/form-destinations/mapping.mjs +152 -0
  97. package/google-workspace/form-destinations/sheets.mjs +202 -0
  98. package/google-workspace/lib/auth.mjs +81 -0
  99. package/google-workspace/lib/drive-files.mjs +54 -0
  100. package/google-workspace/lib/form-destination-settings.mjs +72 -0
  101. package/google-workspace/lib/http.mjs +168 -0
  102. package/google-workspace/lib/picker-flow.mjs +171 -0
  103. package/google-workspace/lib/picker-selection.mjs +107 -0
  104. package/google-workspace/lib/product-state.mjs +93 -0
  105. package/google-workspace/lib/request-policy.mjs +163 -0
  106. package/google-workspace/lib/sheets-a1.mjs +79 -0
  107. package/google-workspace/lib/sheets-admin-csv.mjs +82 -0
  108. package/google-workspace/lib/sheets-admin-request-kinds.mjs +183 -0
  109. package/google-workspace/lib/sheets-client.mjs +286 -0
  110. package/google-workspace/lib/surface-actions.mjs +107 -0
  111. package/google-workspace/mcp.json +4 -0
  112. package/google-workspace/plugin.json +77 -0
  113. package/google-workspace/products.json +12 -0
  114. package/google-workspace/skills/google-workspace/SKILL.md +82 -0
  115. package/google-workspace/skills/google-workspace/references/connection.md +102 -0
  116. package/google-workspace/skills/google-workspace/references/form-delivery.md +50 -0
  117. package/google-workspace/skills/google-workspace/references/sheets.md +149 -0
  118. package/google-workspace/skills/google-workspace/references/troubleshooting.md +31 -0
  119. package/google-workspace/tools/account-info.mjs +18 -0
  120. package/google-workspace/tools/sheets/README.md +54 -0
  121. package/google-workspace/tools/sheets/copy-tab.mjs +23 -0
  122. package/google-workspace/tools/sheets/copy.mjs +23 -0
  123. package/google-workspace/tools/sheets/create.mjs +35 -0
  124. package/google-workspace/tools/sheets/find-replace.mjs +38 -0
  125. package/google-workspace/tools/sheets/find.mjs +52 -0
  126. package/google-workspace/tools/sheets/get-by-data-filter.mjs +23 -0
  127. package/google-workspace/tools/sheets/get.mjs +22 -0
  128. package/google-workspace/tools/sheets/values-append.mjs +27 -0
  129. package/google-workspace/tools/sheets/values-batch-clear.mjs +20 -0
  130. package/google-workspace/tools/sheets/values-batch-get.mjs +20 -0
  131. package/google-workspace/tools/sheets/values-batch-update.mjs +29 -0
  132. package/google-workspace/tools/sheets/values-by-data-filter.mjs +62 -0
  133. package/google-workspace/tools/sheets/values-clear.mjs +20 -0
  134. package/google-workspace/tools/sheets/values-get.mjs +21 -0
  135. package/google-workspace/tools/sheets/values-update.mjs +26 -0
  136. package/google-workspace/tools/sheets-admin/README.md +45 -0
  137. package/google-workspace/tools/sheets-admin/basic-filter.mjs +32 -0
  138. package/google-workspace/tools/sheets-admin/batch-update.mjs +25 -0
  139. package/google-workspace/tools/sheets-admin/developer-metadata.mjs +28 -0
  140. package/google-workspace/tools/sheets-admin/export-csv.mjs +67 -0
  141. package/google-workspace/tools/sheets-admin/filter-views.mjs +38 -0
  142. package/google-workspace/tools/sheets-admin/format.mjs +70 -0
  143. package/google-workspace/tools/sheets-admin/named-ranges.mjs +42 -0
  144. package/google-workspace/tools/sheets-admin/open-existing.mjs +34 -0
  145. package/google-workspace/tools/sheets-admin/protected-ranges-get.mjs +18 -0
  146. package/google-workspace/tools/sheets-admin/sort.mjs +18 -0
  147. package/google-workspace/tools/sheets-admin/tab-modify.mjs +36 -0
  148. package/google-workspace/tovu-admin-surfaces.json +63 -0
  149. package/google-workspace/tovu-agent-tools.json +1178 -0
  150. package/google-workspace/tovu-connections.json +35 -0
  151. package/google-workspace/tovu-form-destinations.json +10 -0
  152. package/google-workspace/ui/README.md +73 -0
  153. package/google-workspace/ui/connection-panel.handler.mjs +167 -0
  154. package/google-workspace/ui/connection-panel.html +17 -0
  155. package/google-workspace/ui/connection-panel.mjs +189 -0
  156. package/google-workspace/ui/connection-view.mjs +120 -0
  157. package/google-workspace/ui/destination-panel.handler.mjs +106 -0
  158. package/google-workspace/ui/destination-panel.html +17 -0
  159. package/google-workspace/ui/destination-panel.mjs +150 -0
  160. package/google-workspace/ui/dom.mjs +140 -0
  161. package/google-workspace/ui/panel.css +170 -0
  162. package/google-workspace/ui/picker-flow.mjs +87 -0
  163. package/google-workspace/ui/picker-view.mjs +44 -0
  164. package/google-workspace/ui/picker.handler.mjs +56 -0
  165. package/google-workspace/ui/picker.html +17 -0
  166. package/google-workspace/ui/picker.mjs +88 -0
  167. package/google-workspace/ui/surface-client.mjs +227 -0
  168. package/higgsfield-media/mcp.json +9 -0
  169. package/higgsfield-media/plugin.json +109 -0
  170. package/higgsfield-media/skills/higgsfield-media/SKILL.md +325 -0
  171. package/higgsfield-media/skills/higgsfield-media/references/failure-modes.md +116 -0
  172. package/higgsfield-media/skills/higgsfield-media/references/models-and-plan-gates.md +142 -0
  173. package/jev/mcp.json +4 -0
  174. package/jev/plugin.json +44 -0
  175. package/jev/skills/jev/SKILL.md +308 -0
  176. package/jev/skills/jev/references/cookbooks.md +197 -0
  177. package/jev/skills/jev/references/jaggedness.md +135 -0
  178. package/jev/skills/jev/references/javascript-sdk.md +146 -0
  179. package/jev/skills/jev/references/patterns.md +123 -0
  180. package/jev/skills/jev/references/primitives-reference.md +187 -0
  181. package/langfuse/plugin.json +22 -0
  182. package/meilisearch/mcp.json +4 -0
  183. package/meilisearch/plugin.json +54 -0
  184. package/meilisearch/providers/meilisearch.mjs +651 -0
  185. package/meilisearch/skills/meilisearch/SKILL.md +43 -0
  186. package/meilisearch/tovu-search-providers.json +10 -0
  187. package/netlify/NOTES.md +13 -0
  188. package/netlify/mcp.json +4 -0
  189. package/netlify/plugin.json +47 -0
  190. package/netlify/skills/netlify/SKILL.md +39 -0
  191. package/netlify/targets/netlify.mjs +384 -0
  192. package/netlify/tovu-consent.json +6 -0
  193. package/netlify/tovu-deploy-targets.json +163 -0
  194. package/opentelemetry/mcp.json +4 -0
  195. package/opentelemetry/plugin.json +52 -0
  196. package/opentelemetry/skills/opentelemetry/SKILL.md +39 -0
  197. package/package.json +161 -6
  198. package/posthog/mcp.json +4 -0
  199. package/posthog/plugin.json +46 -0
  200. package/posthog/skills/posthog/SKILL.md +36 -0
  201. package/railway/deploy-configs/railway.mjs +135 -0
  202. package/railway/deploy-ops/railway.mjs +167 -0
  203. package/railway/mcp.json +4 -0
  204. package/railway/plugin.json +45 -0
  205. package/railway/skills/railway/SKILL.md +66 -0
  206. package/railway/tovu-deploy-configs.json +16 -0
  207. package/railway/tovu-deploy-ops.json +11 -0
  208. package/render/deploy-configs/render.mjs +141 -0
  209. package/render/deploy-ops/render.mjs +152 -0
  210. package/render/mcp.json +4 -0
  211. package/render/plugin.json +47 -0
  212. package/render/skills/render/SKILL.md +63 -0
  213. package/render/tovu-deploy-configs.json +16 -0
  214. package/render/tovu-deploy-ops.json +11 -0
  215. package/resend/NOTES.md +52 -0
  216. package/resend/adapters/resend.mjs +161 -0
  217. package/resend/lib/domains.mjs +137 -0
  218. package/resend/lib/errors.mjs +25 -0
  219. package/resend/lib/feedback.mjs +118 -0
  220. package/resend/mcp.json +13 -0
  221. package/resend/plugin.json +192 -0
  222. package/resend/skills/resend/SKILL.md +107 -0
  223. package/resend/tovu-mail-adapters.json +22 -0
  224. package/s3-compatible/NOTES.md +11 -0
  225. package/s3-compatible/mcp.json +4 -0
  226. package/s3-compatible/object-stores/s3-compatible.mjs +287 -0
  227. package/s3-compatible/plugin.json +56 -0
  228. package/s3-compatible/skills/s3-compatible/SKILL.md +39 -0
  229. package/s3-compatible/targets/s3-compatible.mjs +1054 -0
  230. package/s3-compatible/tovu-deploy-targets.json +432 -0
  231. package/s3-compatible/tovu-object-stores.json +29 -0
  232. package/sentry/mcp.json +4 -0
  233. package/sentry/plugin.json +49 -0
  234. package/sentry/skills/sentry/SKILL.md +34 -0
  235. package/shopify/mcp.json +4 -0
  236. package/shopify/plugin.json +22 -0
  237. package/shopify/skills/shopify/SKILL.md +103 -0
  238. package/shopify/skills/shopify/references/catalog-sync.md +74 -0
  239. package/shopify/skills/shopify/references/domains.md +26 -0
  240. package/shopify/skills/shopify/references/live-storefront.md +34 -0
  241. package/shopify/skills/shopify/references/storefront-snippets.md +21 -0
  242. package/site-compliance/mcp.json +4 -0
  243. package/site-compliance/plugin.json +34 -0
  244. package/site-compliance/skills/site-compliance/SKILL.md +220 -0
  245. package/site-compliance/skills/site-compliance/references/ccpa-cpra.md +115 -0
  246. package/site-compliance/skills/site-compliance/references/gdpr-eu-uk.md +143 -0
  247. package/site-compliance/skills/site-compliance/references/wcag-2-2.md +129 -0
  248. package/site-import/mcp.json +4 -0
  249. package/site-import/plugin.json +41 -0
  250. package/site-import/skills/site-import/SKILL.md +232 -0
  251. package/site-import/skills/site-import/references/content-mapping.md +166 -0
  252. package/site-import/skills/site-import/references/discovery-and-classification.md +117 -0
  253. package/site-import/skills/site-import/references/theme-extraction.md +34 -0
  254. package/slide-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  255. package/slide-design/mcp.json +4 -0
  256. package/slide-design/plugin.json +26 -0
  257. package/slide-design/skills/slide-design/SKILL.md +103 -0
  258. package/slide-design/skills/slide-design/references/charts-and-exhibits.md +100 -0
  259. package/slide-design/skills/slide-design/references/html-deck.md +175 -0
  260. package/slide-design/skills/slide-design/references/layout-and-style.md +131 -0
  261. package/slide-design/skills/slide-design/references/review.md +93 -0
  262. package/slide-design/skills/slide-design/references/storyline.md +118 -0
  263. package/stripe/plugin.json +22 -0
  264. package/supabase/mcp.json +26 -0
  265. package/supabase/plugin.json +78 -0
  266. package/supabase/skills/supabase/SKILL.md +169 -0
  267. package/supabase/skills/supabase/references/failure-modes.md +101 -0
  268. package/ui-ux-design/CHANGELOG.md +28 -0
  269. package/ui-ux-design/LICENSE +201 -0
  270. package/ui-ux-design/NOTICE +14 -0
  271. package/ui-ux-design/README.md +62 -0
  272. package/ui-ux-design/com.anthropic.claude-code/hooks/hooks.json +4 -0
  273. package/ui-ux-design/mcp.json +4 -0
  274. package/ui-ux-design/plugin.json +26 -0
  275. package/ui-ux-design/skills/frontend-accessibility/SKILL.md +60 -0
  276. package/ui-ux-design/skills/gstack-design/SKILL.md +53 -0
  277. package/ui-ux-design/skills/gstack-design/references/consultation.md +30 -0
  278. package/ui-ux-design/skills/gstack-design/references/html.md +29 -0
  279. package/ui-ux-design/skills/gstack-design/references/review.md +28 -0
  280. package/ui-ux-design/skills/gstack-design/references/shotgun.md +33 -0
  281. package/ui-ux-design/skills/gstack-design/references/upstream-notes.md +23 -0
  282. package/ui-ux-design/skills/interface-design/ORIGINAL.md +16 -0
  283. package/ui-ux-design/skills/interface-design/SKILL.md +41 -0
  284. package/ui-ux-design/skills/interface-design/references/commands/audit.md +40 -0
  285. package/ui-ux-design/skills/interface-design/references/commands/critique.md +21 -0
  286. package/ui-ux-design/skills/interface-design/references/commands/extract.md +34 -0
  287. package/ui-ux-design/skills/interface-design/references/commands/init.md +31 -0
  288. package/ui-ux-design/skills/interface-design/references/commands/status.md +22 -0
  289. package/ui-ux-design/skills/interface-design/references/examples/system-precision.md +29 -0
  290. package/ui-ux-design/skills/interface-design/references/examples/system-warmth.md +29 -0
  291. package/ui-ux-design/skills/interface-design/references/system-template.md +57 -0
  292. package/ui-ux-design/skills/shadcn-ui/README.md +244 -0
  293. package/ui-ux-design/skills/shadcn-ui/SKILL.md +333 -0
  294. package/ui-ux-design/skills/shadcn-ui/examples/auth-layout.tsx +177 -0
  295. package/ui-ux-design/skills/shadcn-ui/examples/data-table.tsx +313 -0
  296. package/ui-ux-design/skills/shadcn-ui/examples/form-pattern.tsx +177 -0
  297. package/ui-ux-design/skills/shadcn-ui/resources/component-catalog.md +481 -0
  298. package/ui-ux-design/skills/shadcn-ui/resources/customization-guide.md +516 -0
  299. package/ui-ux-design/skills/shadcn-ui/resources/migration-guide.md +463 -0
  300. package/ui-ux-design/skills/shadcn-ui/resources/setup-guide.md +412 -0
  301. package/ui-ux-design/skills/shadcn-ui/scripts/verify-setup.sh +134 -0
  302. package/ui-ux-design/skills/ui-ux-design/SKILL.md +171 -0
  303. package/ui-ux-design/skills/ui-ux-design/references/brand-and-voice.md +78 -0
  304. package/ui-ux-design/skills/ui-ux-design/references/chrome-mobile-test-matrix.md +137 -0
  305. package/ui-ux-design/skills/ui-ux-design/references/components-and-states.md +112 -0
  306. package/ui-ux-design/skills/ui-ux-design/references/delight-and-motion.md +54 -0
  307. package/ui-ux-design/skills/ui-ux-design/references/foundations.md +145 -0
  308. package/ui-ux-design/skills/ui-ux-design/references/inclusive-ai-imagery.md +57 -0
  309. package/ui-ux-design/skills/ui-ux-design/references/kole-jain-uiux-concepts.md +104 -0
  310. package/ui-ux-design/skills/ui-ux-design/references/kuinetic-reference.md +184 -0
  311. package/ui-ux-design/skills/ui-ux-design/references/openai-frontend-skill.md +139 -0
  312. package/ui-ux-design/skills/ui-ux-design/references/premium-ui.md +55 -0
  313. package/ui-ux-design/skills/ui-ux-design/references/research-and-validation.md +88 -0
  314. package/ui-ux-design/skills/ui-ux-design/references/sam-crawford-premium-websites.md +103 -0
  315. package/ui-ux-design/skills/ui-ux-design/references/self-made-web-designer-core-skills.md +119 -0
  316. package/ui-ux-design/skills/ui-ux-design/references/ui-animation.md +173 -0
  317. package/ui-ux-design/skills/ui-ux-design/references/ui-loop.md +234 -0
  318. package/ui-ux-design/skills/ui-ux-design/references/visual-storytelling.md +54 -0
  319. package/ui-ux-design/skills/vercel-web-design-guidelines/SKILL.md +39 -0
  320. package/ui-ux-design/skills/web-compliance/SKILL.md +134 -0
  321. package/ui-ux-design/skills/web-compliance/references/audit-evidence.md +59 -0
  322. package/vercel/NOTES.md +23 -0
  323. package/vercel/mcp.json +4 -0
  324. package/vercel/object-stores/vercel-blob.mjs +147 -0
  325. package/vercel/plugin.json +47 -0
  326. package/vercel/skills/vercel/SKILL.md +48 -0
  327. package/vercel/targets/vercel.mjs +586 -0
  328. package/vercel/tovu-consent.json +6 -0
  329. package/vercel/tovu-deploy-targets.json +219 -0
  330. package/vercel/tovu-object-stores.json +10 -0
  331. package/wordpress/NOTES.md +33 -0
  332. package/wordpress/plugin.json +30 -0
  333. package/wordpress/skills/wordpress/SKILL.md +43 -0
  334. package/wordpress/skills/wordpress/references/free-site-redirect.md +46 -0
  335. 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 retains the existing plugin extension format.
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 }, { identityNamespace = "tovu" } = {}) {
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 : {} }) ?? {};
@@ -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,
@@ -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 retains the existing plugin extension format. */
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 retains the existing plugin extension format. */
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.identityNamespace ?? "tovu" }).errors.length === 0;
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
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {}
4
+ }
@@ -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.