@enfyra/mcp-server 0.0.42 → 0.0.43

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/mcp-server",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "MCP server for Enfyra - manage your Enfyra instance via Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -287,6 +287,18 @@ const scope = {
287
287
  'Mutate @QUERY.filter before canonical CRUD runs.',
288
288
  ],
289
289
  },
290
+ {
291
+ name: 'Pre-hook encrypted field normalization',
292
+ code: `const value = @BODY.api_token_encrypted
293
+ if (value && value.slice(0, 7) !== "enc:v1:") {
294
+ @BODY.api_token_encrypted = @HELPERS.$encrypt.encrypt(value)
295
+ }`,
296
+ notes: [
297
+ 'Use Enfyra pre-hooks for request-body normalization before canonical CRUD persists the record.',
298
+ 'Do not implement encrypted field normalization as a Knex/database hook.',
299
+ 'Use $encrypt for encryption and $ssh.generateKeyPair for SSH key generation; do not use $secrets.',
300
+ ],
301
+ },
290
302
  {
291
303
  name: 'Post-hook response shaping',
292
304
  code: `if (@ERROR) {
@@ -355,6 +367,75 @@ return @DATA`,
355
367
  'Use route/pre-hook filters for row-level access.',
356
368
  ],
357
369
  },
370
+ {
371
+ name: 'Admin menu and extension permission gates',
372
+ code: `<template>
373
+ <section class="space-y-4">
374
+ <PermissionGate :condition="canReadCloudProjects">
375
+ <template #default>
376
+ <div class="flex items-center justify-between gap-3">
377
+ <h2 class="text-lg font-semibold">Cloud projects</h2>
378
+
379
+ <PermissionGate :condition="canCreateCloudProject">
380
+ <UButton icon="i-lucide-plus" label="Create instance" @click="openCreate = true" />
381
+ </PermissionGate>
382
+ </div>
383
+
384
+ <div v-for="project in projects" :key="project.id" class="rounded-lg border p-4">
385
+ <NuxtLink :to="\`/cloud/projects/\${project.id}\`" class="font-medium">
386
+ {{ project.name }}
387
+ </NuxtLink>
388
+
389
+ <div class="mt-3 flex gap-2">
390
+ <PermissionGate :condition="canUpdateCloudProject">
391
+ <UButton icon="i-lucide-pause" variant="soft" label="Disable" @click="openDisable(project)" />
392
+ </PermissionGate>
393
+
394
+ <PermissionGate :condition="canDeleteCloudProject">
395
+ <UButton icon="i-lucide-trash-2" color="error" variant="soft" label="Delete" @click="openDelete(project)" />
396
+ </PermissionGate>
397
+ </div>
398
+ </div>
399
+ </template>
400
+
401
+ <template #fallback>
402
+ <EmptyState title="No access" description="You do not have permission to view Cloud projects." />
403
+ </template>
404
+ </PermissionGate>
405
+ </section>
406
+ </template>
407
+
408
+ <script setup>
409
+ const { checkPermissionCondition } = usePermissions()
410
+
411
+ const canReadCloudProjects = computed(() => checkPermissionCondition({
412
+ or: [
413
+ { route: '/cloud/projects', methods: ['GET'] },
414
+ { route: '/cloud_projects', methods: ['GET'] }
415
+ ]
416
+ }))
417
+
418
+ const canCreateCloudProject = computed(() => checkPermissionCondition({
419
+ or: [{ route: '/cloud_projects', methods: ['POST'] }]
420
+ }))
421
+
422
+ const canUpdateCloudProject = computed(() => checkPermissionCondition({
423
+ or: [{ route: '/cloud_projects', methods: ['PATCH'] }]
424
+ }))
425
+
426
+ const canDeleteCloudProject = computed(() => checkPermissionCondition({
427
+ or: [{ route: '/cloud_projects', methods: ['DELETE'] }]
428
+ }))
429
+ </script>`,
430
+ notes: [
431
+ 'This is menu/extension visibility, not row-level RLS.',
432
+ 'Set menu_definition.permission on every sensitive admin menu. Example for /cloud/hosts: { or: [{ route: "/cloud/admin/hosts", methods: ["GET"] }, { route: "/cloud_servers", methods: ["GET"] }] }.',
433
+ 'Admin pages are sensitive. Use permission gates by default, not as an optional polish step.',
434
+ 'Menus should only be visible when the user has at least GET permission for the page route or backing data route.',
435
+ 'Inside the extension, gate each action by its own route/method: GET for page visibility, POST for create/flow-trigger buttons, PATCH for normal record edits, DELETE for native delete routes.',
436
+ 'Server route permissions remain mandatory; UI gates are for clear operator UX and least-privilege surfaces.',
437
+ ],
438
+ },
358
439
  ],
359
440
  },
360
441
  websocket: {
@@ -553,13 +634,17 @@ create_extension({
553
634
  name: "ReportsPage",
554
635
  description: "Reports dashboard",
555
636
  menuId: "<created-menu-id>",
556
- code: "<template><section class=\\"min-h-full w-full space-y-4\\">Reports</section></template><script setup>useHeaderActionRegistry({ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', onClick: () => {}, order: 0 })</script>",
637
+ code: "<template><section class=\\"min-h-full w-full space-y-4\\"><div class=\\"grid gap-4 md:grid-cols-3\\"><UCard><p class=\\"text-sm text-muted\\">Total</p><p class=\\"mt-2 text-2xl font-semibold\\">0</p></UCard></div></section></template><script setup>const { registerPageHeader } = usePageHeaderRegistry(); registerPageHeader({ title: 'Reports', description: 'Operational report overview.', leadingIcon: 'lucide:bar-chart-3', gradient: 'cyan', variant: 'minimal' }); useHeaderActionRegistry({ id: 'refresh-reports', label: 'Refresh', icon: 'lucide:refresh-cw', onClick: () => {}, order: 0 })</script>",
557
638
  isEnabled: true
558
639
  })`,
559
640
  notes: [
560
641
  'Menu provides navigation; extension provides content.',
561
642
  'Use menu_definition.label, not title.',
562
643
  'For page extensions, create the menu first and pass menuId to create_extension.',
644
+ 'Page extensions must register the app-shell PageHeader with usePageHeaderRegistry instead of rendering a custom top header.',
645
+ 'Use variant: "minimal" for operational pages unless a larger header is intentionally needed.',
646
+ 'Do not put ordinary KPI cards in PageHeader.stats; render metrics in the extension body.',
647
+ 'Put page-level actions in useHeaderActionRegistry or useSubHeaderActionRegistry.',
563
648
  'Page extensions should be full-bleed by default and responsive from the first version.',
564
649
  'The extension root is already inside eApp main; do not add root-level page padding.',
565
650
  ],
@@ -589,6 +674,8 @@ create_menu({
589
674
  'Design the menu/page split before generating dashboard code.',
590
675
  'Keep /dashboard as a summary and distribution page, not a detailed operations table.',
591
676
  'Use focused pages for operational domains.',
677
+ 'Each page extension must use usePageHeaderRegistry for the app-shell title strip and should not render a duplicate top header in the body.',
678
+ 'PageHeader.stats is reserved for deliberate overview headers; operational KPIs belong in body cards/tables.',
592
679
  'Provisioning pages should not show raw history rows as the primary UI; group by project/run and translate step keys into operator-facing labels.',
593
680
  'Operational lists should use pagination plus search/filter controls; do not rely on arbitrary fixed limits such as limit=50.',
594
681
  'UTabs is available in eApp extension runtime for page-level sections.',
@@ -121,6 +121,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
121
121
  '- Each route has **publishedMethods** (which HTTP verbs are “public”) and **routePermissions** (roles/users for protected access).',
122
122
  '- If the **current request method** is listed in **publishedMethods** for that route, the server allows the call **without** a Bearer token (`RoleGuard`).',
123
123
  '- Otherwise the client must send an **Authorization** header with **Bearer** JWT from login. Then the user must satisfy **routePermissions** (unless root admin).',
124
+ '- Cloud owner-scoped GET handlers must keep normal users filtered to their own `cloud_projects.owner`, but root admin operational views must bypass that owner filter with `@USER.isRootAdmin`. Apply this consistently to `cloud_projects`, `cloud_subscriptions`, `cloud_payment_orders`, `cloud_provisioning_history`, and `/cloud/projects` so admin data routes match admin summary APIs.',
124
125
  '- MCP tools that use `fetchAPI` authenticate with the configured admin credentials; explain to users that **direct HTTP** calls need a token unless the route/method is published.',
125
126
  '',
126
127
  '### Post-hooks (REST)',
@@ -130,6 +131,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
130
131
  '### Dynamic script syntax preference',
131
132
  '- When writing server-side Enfyra scripts, prefer template macros over raw `$ctx` access: use `@BODY`, `@QUERY`, `@PARAMS`, `@USER`, `@REPOS`, `@HELPERS`, `@SOCKET`, `@TRIGGER`, `@DATA`, `@ERROR`, and `@THROW400`–`@THROW503`.',
132
133
  '- Use Enfyra native throw helpers for intentional errors: `@THROW400("message")`, `@THROW403()`, `@THROW404("resource", id)`, or `$ctx.$throw[400]("message")`. Do not generate `throw new Error(...)` for user/domain errors in handlers, hooks, flows, websocket events, OAuth scripts, or admin-generated scripts.',
134
+ '- For encrypted persisted fields such as `*_encrypted`, use an Enfyra route pre-hook, not a Knex/database hook. Mutate the body before persistence: `const value = @BODY.field_encrypted; if (value && value.slice(0, 7) !== "enc:v1:") @BODY.field_encrypted = @HELPERS.$encrypt.encrypt(value);`.',
135
+ '- ASV exposes `$helpers.$encrypt.encrypt/decrypt` for encrypted strings and `$helpers.$ssh.generateKeyPair` for SSH keys. Do not generate `$helpers.$secrets` usage.',
136
+ '- Script-backed records use one shared persistence contract: `sourceCode` is the editable source, `scriptLanguage` controls compilation, and `compiledCode` is generated by the server from `sourceCode`. Do not hand-edit or send stale `compiledCode` from generated tools; save `sourceCode`/`scriptLanguage` through `PATCH /<script_table>/<id>` and let the server persist generated `compiledCode` internally. Public metadata may mark `compiledCode` non-updatable, but the server engine must still preserve the generated value after normalization.',
137
+ '- Enfyra Cloud host provisioning with PgBouncer must preserve tenant database isolation. PgBouncer should use per-tenant `DATABASE_URLS` entries with each tenant `db_user`, `db_password`, and `db_name`, and PostgreSQL 16/SCRAM hosts need `AUTH_TYPE=plain`. Do not route all tenants through PostgreSQL `postgres` just to make PgBouncer connect; that bypasses tenant DB permissions.',
138
+ '- Enfyra Cloud Docker health checks must compare exact healthy states. `true healthy` passes, `true starting` keeps waiting, and `true unhealthy` fails. Do not use broad substring logic where `unhealthy` accidentally counts as healthy.',
133
139
  '- Before saving generated script code, validate it with `POST /admin/script/validate` when available. It compiles with the server kernel and parses the executable async body without running side effects. Enfyra App `FormCodeEditor` also exposes a `Validate` action for this endpoint; use it before save/run when editing through the UI. If unavailable, use `run_admin_test`/`test_flow_step` as the closest validation path before saving.',
134
140
  '- Do not coerce dynamic script values with `String(...)`, `Number(...)`, or `Boolean(...)`. Enfyra payloads, user ids, record ids, and relation ids should keep their runtime type; validate required values and pass them through directly.',
135
141
  '- Use raw `$ctx` only when there is no template macro for the field or helper you need.',
@@ -256,10 +262,11 @@ export function buildMcpServerInstructions(apiBaseUrl) {
256
262
  '- Enfyra supports automated workflows via **`flow_definition`**, **`flow_step_definition`**, and **`flow_execution_definition`** tables.',
257
263
  '- **Flow** (`flow_definition`): `name`, `triggerType` (`schedule`, `manual`), `triggerConfig` (JSON), `timeout`, `maxExecutions` (default 100, auto-cleanup old history), `isEnabled`.',
258
264
  '- **Step** (`flow_step_definition`): `flow` → flow id, `key` (unique identifier for data chain), `stepOrder`, `type` (`script`, `condition`, `query`, `create`, `update`, `delete`, `http`, `trigger_flow`, `sleep`, `log`), `config` (JSON), `timeout`, `onError` (`stop`, `skip`, `retry`), `retryAttempts`, `parent` → self-ref to condition step (null = root), `branch` (`true`/`false` — which branch of parent condition).',
259
- '- **Execution history** (`flow_execution_definition`): `flow` → flow id, `status`, `payload`, `context` (full data chain), `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. Query separately — NOT nested under flow_definition.',
265
+ '- **Execution history** (`flow_execution_definition`): `flow` → flow id, `status`, `payload`, `completedSteps`, `currentStep`, `error`, `startedAt`, `completedAt`, `duration`. There is no persisted `context` column; failed executions store diagnostics in `error`. Query separately — NOT nested under flow_definition.',
260
266
  '- **triggerConfig examples**: schedule: `{"cron":"0 2 * * *","timezone":"UTC"}`, manual: `{}`. For event/webhook use cases, create a handler/hook with `@TRIGGER("flow-name", payload)` instead.',
261
267
  '- **Step config examples**: script: `{"code":"return #user_definition.find({limit:10})"}`, condition: `{"code":"return @FLOW_LAST?.data?.length > 0"}` (uses JS truthy/falsy: `return user` = truthy if exists, `return null` = falsy), query: `{"table":"user_definition","filter":{"status":{"_eq":"active"}},"limit":10}`, http: `{"url":"https://api.example.com","method":"POST","body":{}}` (auto Content-Type: application/json; **http `url` must be public-safe**—see Safety), sleep: `{"ms":5000}`, trigger_flow: `{"flowId":2}`.',
262
268
  '- **Data chain**: Steps access previous results via `@FLOW.<stepKey>` and `@FLOW_LAST`. Input payload via `@FLOW_PAYLOAD`. Repos via `#table_name`.',
269
+ '- **Flow step output discipline:** Script/condition steps must return only the small values that later steps genuinely need, such as ids, booleans, status keys, or counters. Do not return full records, host objects, package objects, DB URLs, SSH keys, API tokens, generated passwords, or other secrets. Later steps should re-query the records they need from ids/payload. Flow execution history already records errors; successful runs do not need full success snapshots.',
263
270
  '- **Template syntax (flows)**: `@FLOW_PAYLOAD` → `$ctx.$flow.$payload` (input data), `@FLOW_LAST` → `$ctx.$flow.$last`, `@FLOW` → `$ctx.$flow`, `@FLOW_META` → `$ctx.$flow.$meta`, `#table_name` → `$ctx.$repos.table_name`, `@HELPERS` → `$ctx.$helpers`, `@THROW400`–`@THROW503` / `@THROW` → `$ctx.$throw[...]`. Trigger other flows in handlers via `@TRIGGER(name, payload)` or `$ctx.$trigger(name, payload)`.',
264
271
  '- **Condition branching**: Condition step uses JavaScript truthy/falsy evaluation (e.g. `return user` → truthy if exists, falsy if null/0/undefined). Children with matching `parent: {id: conditionStepId}` and `branch: "true"/"false"` execute. Root steps (no parent) always execute sequentially.',
265
272
  '- **Safety**: Max nesting depth 10 (flow triggering flow). Circular flow detection prevents A→B→A loops. HTTP steps: **SSRF hardening** — only `http`/`https`; blocks `localhost`, private IPs, and hostnames resolving to private IPs (use internet-facing URLs like `https://api.example.com`, not internal services, unless server policy changes). Default HTTP timeout 30s (AbortController). `$trigger()` available inside flow steps.',
@@ -282,8 +289,25 @@ export function buildMcpServerInstructions(apiBaseUrl) {
282
289
  '- **Partial reload default:** ESV automatically triggers partial reloads for metadata, routes, extensions, flows, handlers, and related caches after successful writes. Do not reflexively call `/admin/reload`, `/admin/reload/metadata`, or `/admin/reload/routes` after each change. Verify naturally first; use manual reload only when verification shows stale behavior, a reload event failed, or a concrete error indicates the partial reload did not apply.',
283
290
  '- **Dashboard stats:** time range buttons must change the query filter and reload stats. Cloud dashboard should summarize flow execution errors from `flow_execution_definition` and billing stats from order/subscription records; successful/no-error flow runs do not need a standalone provisioning menu.',
284
291
  '- **Page layout default:** page extensions should render full-bleed inside the app shell by default. The extension root is already inside the eApp page `<main>`, so do not add root-level page padding such as `p-4 sm:p-6 xl:p-8`; use spacing between internal sections only. Do not wrap the entire page in a centered card/container unless explicitly requested. Use responsive grids/stacks from the first version so the page works on desktop, tablet, and mobile.',
292
+ '- **PageHeader is mandatory for page extensions:** eApp already renders `CommonPageHeader` from `usePageHeaderRegistry()` in the app shell. Page extensions must call `const { registerPageHeader } = usePageHeaderRegistry()` and register app-level context such as `{ title, description, leadingIcon, gradient, variant }` instead of rendering their own top `<header>` inside extension content. Use `variant: "minimal"` for operational/admin detail pages unless the page intentionally needs a larger title strip.',
293
+ '- **Do not misuse PageHeader stats:** `PageHeader.stats` renders prominent stat cards inside the shell header. Do not put normal operational KPIs, host capacity, billing totals, or detail metrics there by default; keep those as body cards/tables where the operator can scan them with the page content. Only use PageHeader stats for a deliberately compact overview page where the stats are truly header-level context.',
294
+ '- **Page actions belong in registries:** Move page-level buttons into `useHeaderActionRegistry` or `useSubHeaderActionRegistry`; keep the extension body for operational content only. Sensitive registry actions must include a `permission` condition, for example `{ id: "create", label: "Create instance", permission: { and: [{ route: "/cloud_projects", methods: ["POST"] }] }, onClick }`.',
295
+ '- **Extension navigation:** prefer `NuxtLink` or Nuxt UI components with `:to` for visible navigation links and drill-down cards/buttons. Use `navigateTo(...)` only for imperative navigation after submit, confirm, mutation, or another side effect.',
296
+ '- **Extension runtime scope:** eApp exposes Vue APIs and injected Nuxt/Enfyra composables both to script global scope and Vue app `globalProperties`. Template expressions may call injected helpers directly, for example after a save handler can call `navigateTo("/data/cloud_projects")`, because Vue compiles template helpers to `_ctx.*`.',
297
+ '- **Extension CSS affects shell utility ordering:** dynamic extension CSS is injected after the app shell CSS. Shell/page-header code must not put conflicting plain Tailwind utilities on the same element, such as `flex-col` plus `flex-row`, `items-start` plus `items-center`, or `text-left` plus `text-center`. Choose one mutually exclusive class per state; otherwise extension CSS can change which utility wins and shift shell layout.',
285
298
  '- **Admin record links:** when an admin extension links to backend records for management or inspection, point to eApp data routes such as `/data/landing_terms`, `/data/cloud_projects`, or `/data/cloud_payment_orders`. Do not use public landing-page paths from record fields such as `/cloud-terms` unless the explicit intent is previewing the public website.',
286
- '- **Do not downgrade extension code to ES5 to appease tooling.** eApp extension runtime should support normal browser/runtime APIs such as `Array.includes`, `Set`, `Promise.all`, `String.replace`, `Intl.DateTimeFormat`, and `Intl.NumberFormat`. If diagnostics reject these, fix eApp extension checker/runtime contract instead of rewriting generated extension code around the limitation.',
299
+ '- **Admin menu visibility is permission-driven, not RLS:** Cloud/admin menu entries are sensitive and must set `menu_definition.permission` so they are visible only to users who have at least GET permission for the backing route or table. Permission conditions use HTTP `methods`, not CRUD `actions`. Do not show a Cloud menu merely because an extension exists or because the path is hardcoded. Example: `/cloud/hosts` menu should require `{ or: [{ route: "/cloud/admin/hosts", methods: ["GET"] }, { route: "/cloud_servers", methods: ["GET"] }] }`.',
300
+ '- **PermissionGate is mandatory inside admin extensions:** every sensitive action button, form, mutation, destructive workflow, and data shortcut must be wrapped in `PermissionGate` or guarded with `usePermissions()` before rendering/enabling. Default gates: list/detail visibility needs `methods: ["GET"]`; create and custom flow-trigger routes usually need `methods: ["POST"]`; native record edits need `methods: ["PATCH"]`; native delete routes need `methods: ["DELETE"]`. Root admin still passes through normal permission helpers, but extension code must not rely on root-only assumptions.',
301
+ '- **Extension permission UX:** if the current user can read a page but cannot perform an action, hide the action by default. If hiding would confuse the workflow, render a disabled state with a short reason. Never let the button render active and depend only on the server rejection; server permissions are the final boundary, not the UI contract.',
302
+ '- **Cloud project admin operations:** use canonical `cloud_projects` table routes as the single source of truth. Admin manual create uses `POST /cloud_projects` with schema-safe fields `owner: { id }`, `plan: { id }`, `admin_email`, `admin_password`, `name`, `subdomain`, and `status: "creating"`. The UI must show tenant admin credential as an email/password pair and include a generate-password action before create; do not build an email-only credential form. Project detail is the place for destructive lifecycle actions. Disable uses `PATCH /cloud_projects/:id` with body `{ status: "disabled" }` and `confirm_tenant_id`/`confirm_hash` in query params. Enable uses `PATCH /cloud_projects/:id` with body `{ status: "running" }`. Delete uses `DELETE /cloud_projects/:id` with typed `confirm_tenant_id`, returned `requiredConfirmHash`, and the matching hash before triggering `cloud-delete-project`. Do not create separate one-off `/cloud/admin/projects/*` action routes for create/disable/enable/delete when the canonical table route can own the workflow.',
303
+ '- **Flow schedule UI:** schedule trigger editors must keep the server contract as `triggerConfig.cron` and `triggerConfig.timezone`, but the UI should not be a bare cron field plus giant timezone dropdown. Provide common cadence presets, readable current-schedule summary, searchable access to all IANA timezones, suggested timezone shortcuts, and a custom cron escape hatch so operators can configure recurring checks without remembering cron syntax.',
304
+ '- **Admin operation UI:** use eApp `CommonModal` for create, disable, delete, and multi-field confirmation workflows. `CommonModal` is the app-matched wrapper around Nuxt UI modal and follows the `UModal` contract: `v-model:open`, `#header`, `#body`, and `#footer`. Keep the page body as compact summary/actions; do not use raw `UModal`, custom fixed overlays, or large inline confirmation panels for these workflows.',
305
+ '- **FormEditor is preferred for table-record forms:** when an extension creates or edits a concrete table record such as `cloud_host_settings`, `cloud_project_settings`, `cloud_servers`, or `cloud_projects`, use `FormEditor`/`FormEditorLazy` inside the modal/page instead of hand-building every input. Customize layout with `sections`, `includes`, and `field-map`; reserve custom inputs only for workflow-specific fields that are not table columns.',
306
+ '- **Modal form layout:** inside `CommonModal`, stack each form control vertically with label text above a full-width input/control. Use a small grid/space stack such as `grid gap-4`, `p.text-sm.font-medium`, then `UInput class="mt-2 w-full"`. Do not place modal labels and inputs side by side unless the user explicitly asks for a dense horizontal form.',
307
+ '- **Confirmation modal flow:** destructive/admin confirmation modals must read top-to-bottom as the operator workflow. For server-hash confirmations, render: tenant/id input first, then a full-width `Request hash` button, then a disabled hash input that is auto-filled by the server response, then the final destructive action in the footer. The final action stays disabled until the typed id matches and the server hash has been requested. Do not ask operators to manually type or edit the hash.',
308
+ '- **Cloud host deletion:** do not delete `cloud_servers` directly from an extension or custom route. Root admin host deletion must be detail-only from `/cloud/hosts/:id`, call `POST /cloud/admin/hosts/delete`, require typed `confirm_host_id`, block when any project is running or attached, return a server-generated `requiredConfirmHash` for an empty host, and require the hash back via `confirmHash` query before triggering `cloud-delete-host`.',
309
+ '- **Cloud host reconciliation is host-rooted:** use `cloud_reconciliation_reports` as the persisted audit source for host/control-plane/runtime mismatch checks, and use `cloud_servers.projects` as the canonical inverse list of physical projects attached to a host. Do not model reconciliation from the project side or make projects manage themselves; the host owns capacity and runtime scan responsibility. `cloud-reconcile-hosts` is a scheduled flow using `triggerConfig.cron`/`timezone`; `POST /cloud/admin/hosts/reconcile` should only trigger that flow for a host and return job metadata. Host list/detail pages should consume `/cloud/admin/hosts`, which includes the latest `reconciliation_report`, instead of querying raw report rows separately.',
310
+ '- **Do not downgrade extension code to ES5 to appease tooling.** eApp extension runtime should support normal browser/runtime APIs such as `Array.includes`, `Set`, `Promise.all`, `String.replace`, `Intl.DateTimeFormat`, and `Intl.NumberFormat`. If diagnostics reject these, fix eApp extension checker/runtime contract instead of rewriting generated extension code around the limitation. Vue extension diagnostics should only TypeScript-check `<script setup lang="ts">`; plain `<script setup>` extension code is JavaScript and must not emit TypeScript diagnostics into the form.',
287
311
  '',
288
312
  '#### Injected Vue API functions:',
289
313
  '- Reactivity: `ref`, `reactive`, `computed`, `readonly`, `shallowRef`, `shallowReactive`',
@@ -302,12 +326,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
302
326
  '#### Injected Enfyra composables:',
303
327
  '- **useApi:** API client with auto error handling & toast. Returns `{ data, error, pending, status, execute, refresh }`. Does NOT auto-run — must call `execute()`. Example: `const { data, execute } = useApi(\'/user_definition\', { query: { limit: 10 } }); onMounted(() => execute());`',
304
328
  '- **useAuth:** Authentication. Returns `{ me, login, logout, fetchUser, isLoggedIn, isLoading, oauthLogin }`. `me` is reactive user object with `isRootAdmin`, `role`, `allowedRoutePermissions`.',
305
- '- **usePermissions:** Permission checks. Returns `{ hasPermission(route, method), hasAnyPermission(routes, actions), hasAllPermissions(routes, actions), checkPermissionCondition(condition) }`. Actions: `read`, `create`, `update`, `delete`.',
329
+ '- **usePermissions:** Permission checks. Returns `{ hasPermission(route, method), hasAnyPermission(routes, methods), hasAllPermissions(routes, methods), checkPermissionCondition(condition) }`. Permission conditions use HTTP method names directly, for example `{ or: [{ route: "/posts", methods: ["GET"] }] }`.',
306
330
  '- **useSchema:** Schema management. Returns `{ schemas, schema, fetchSchema, schemaLoading, definition, fieldMap, getField(key), editableFields, generateEmptyForm(), validate(record), getIncludeFields(), useFormChanges() }`.',
307
331
  '- **useGlobalState:** Global app state. Returns `{ settings, storageConfigs, aiConfigs, appPackages, sidebarVisible, sidebarCollapsed, routeLoading, toggleSidebar(), setRouteLoading(), fetchAppPackages(), packageCacheState }`.',
308
332
  '- **useScreen:** Responsive helpers. Returns `{ width, height, isMobile, isTablet, isDesktop, isLargeDesktop, screenType }`.',
309
333
  '- **useConfirm:** Confirmation dialogs. Returns `{ confirm({ title, content, confirmText, cancelText }), isVisible, options, onConfirm, onCancel }`.',
310
- '- **useHeaderActionRegistry:** Register header actions. Pass array: `useHeaderActionRegistry([{ id, label, onClick, color, icon, order, side, global }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component }`.',
334
+ '- **useHeaderActionRegistry:** Register header actions. Pass array: `useHeaderActionRegistry([{ id, label, onClick, color, icon, order, side, global, permission }])`. Action has `{ id, label, onClick, color, icon, order, side: \'left\'|\'right\', global, component, permission }`; admin actions should set `permission` by default.',
311
335
  '- **useSubHeaderActionRegistry:** Same as header but for sub-header.',
312
336
  '- **usePageHeaderRegistry:** Page title strip. `{ registerPageHeader, clearPageHeader, pageHeader, hasPageHeader }`. Config: `title`, optional `description`, `stats`, `variant`, `gradient` (`purple`|`blue`|`cyan`|`none` — horizontal strip + leading icon tint), `leadingIcon` (icon name), `hideLeadingIcon`. Call `registerPageHeader` again when title/stats must update (plain object snapshot, not refs inside the config).',
313
337
  '- **useMenuRegistry:** Menu management. Returns `{ menuItems, menuGroups, registerMenuItem, unregisterMenuItem, getMenuItemsBySidebar, findParentMenuIdByPath }`.',
@@ -319,12 +343,12 @@ export function buildMcpServerInstructions(apiBaseUrl) {
319
343
  '- **useMounted:** Mount state helper.',
320
344
  '',
321
345
  '#### Injected UI Components (auto-resolved):',
322
- '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
346
+ '- **Common:** `EmptyState`, `LoadingState`, `ErrorState`, `PageHeader`, `FormCard`, `CommonModal`, `Modal`, `Drawer`, `BreadCrumbs`, `ListItem`, `LazyImage`, `GlobalConfirm`, `UploadModal`, `UploadModalLazy`, `AvatarInitials`, `BrandingHeader`, `SettingsCard`, `RouteLoading`',
323
347
  '- **Data Table:** `DataTable`, `DataTableLazy`, `ColumnSelector`',
324
348
  '- **Form:** `FormEditor`, `FormEditorLazy` (same API, lazy-loaded), `FilterEditor`, `FilterHistory`, `FieldSelector`',
325
349
  '- **File Manager:** `FileManager`, `FileView`, `FileGridCard`, `CreateFolderModal`',
326
350
  '- **Menu:** `MenuRenderer`, `MenuItemEditor`',
327
- '- **UI:** `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
351
+ '- **UI:** `NuxtLink`, `UButton`, `UCard`, `UInput`, `UTable`, `UBadge` (if available)',
328
352
  '- **Tabs:** `UTabs` is available in current eApp extension runtime. Use it for page-level sections when a page would otherwise become too long.',
329
353
  '- **Extension:** `Widget` — embed widget extension via `<Widget :id="extensionId" />`',
330
354
  '- **WebSocket:** `WebSocketManager`',
@@ -352,7 +376,7 @@ export function buildMcpServerInstructions(apiBaseUrl) {
352
376
  '- **useApi:** Must call `execute()` — does NOT auto-run. Supports batch operations with `ids` or `files` options.',
353
377
  '- **Header actions:** `useHeaderActionRegistry([{ id: \'refresh\', label: \'Refresh\', onClick: fn, color: \'primary\', icon: \'lucide:refresh\', order: 0 }])`',
354
378
  '- **Schema:** Call `fetchSchema()` first, then use `definition.value`, `editableFields.value`, `getField(\'fieldName\')`.',
355
- '- **Permissions:** Use `checkPermissionCondition({ or: [{ route: \'/posts\', actions: [\'read\'] }] })` for complex rules.',
379
+ '- **Permissions:** Use `checkPermissionCondition({ or: [{ route: \'/posts\', methods: [\'GET\'] }] })` for complex rules. In templates, wrap sensitive controls with `<PermissionGate :condition="{ and: [{ route: \'/admin/action\', methods: [\'POST\'] }] }">...</PermissionGate>` instead of only disabling them visually.',
356
380
  '- **After create/update:** Tell user to refresh (F5). Changes may not appear until reload.',
357
381
  '',
358
382
  '#### Minimal example:',