@nodefony/devkit 10.0.0-beta.2 → 10.0.0-beta.4

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 (65) hide show
  1. package/README.md +151 -5
  2. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
  3. package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
  4. package/dist/index.js +24 -11
  5. package/dist/nodefony/config/config.js +16 -137
  6. package/dist/nodefony/config/defineModuleConfig.js +1 -2
  7. package/dist/nodefony/controllers/DevkitController.js +7 -14
  8. package/dist/nodefony/controllers/McpController.js +27 -163
  9. package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
  10. package/dist/nodefony/mcp/observe.js +235 -0
  11. package/dist/nodefony/mcp/profileReport.js +242 -0
  12. package/dist/nodefony/mcp/profileRoute.js +521 -0
  13. package/dist/nodefony/mcp/shotClient.js +84 -0
  14. package/dist/nodefony/mcp/shutdown.js +44 -0
  15. package/dist/nodefony/mcp/tools.js +696 -0
  16. package/dist/nodefony/service/DevkitService.js +93 -29
  17. package/dist/nodefony/src/card.js +1 -2
  18. package/dist/nodefony/src/errors/DevkitError.js +1 -2
  19. package/dist/nodefony/src/profileSession.js +14 -0
  20. package/dist/types/index.d.ts +15 -1
  21. package/dist/types/nodefony/config/config.d.ts +1 -1
  22. package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
  23. package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
  24. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
  25. package/dist/types/nodefony/mcp/observe.d.ts +83 -0
  26. package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
  27. package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
  28. package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
  29. package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
  30. package/dist/types/nodefony/mcp/tools.d.ts +109 -0
  31. package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
  32. package/dist/types/nodefony/src/profileSession.d.ts +12 -0
  33. package/docs/index.md +217 -20
  34. package/package.json +17 -12
  35. package/skills/nodefony-add-crud/SKILL.md +5 -0
  36. package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
  37. package/skills/nodefony-add-service/SKILL.md +14 -3
  38. package/skills/nodefony-authentication/SKILL.md +234 -0
  39. package/skills/nodefony-browser/SKILL.md +65 -21
  40. package/skills/nodefony-browser/references/socket.md +13 -4
  41. package/skills/nodefony-browser/references/sondes.md +13 -0
  42. package/skills/nodefony-browser/scripts/audit.mjs +59 -10
  43. package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
  44. package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
  45. package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
  46. package/skills/nodefony-browser/scripts/socket.mjs +70 -4
  47. package/skills/nodefony-browser/scripts/watch.mjs +101 -0
  48. package/skills/nodefony-dev/SKILL.md +78 -82
  49. package/skills/nodefony-dev/scripts/docs.mjs +24 -8
  50. package/skills/nodefony-devops/SKILL.md +98 -74
  51. package/skills/nodefony-devops/references/compose.md +8 -0
  52. package/skills/nodefony-devops/references/dimensionnement.md +122 -0
  53. package/skills/nodefony-devops/references/frontal.md +282 -48
  54. package/skills/nodefony-devops/references/kubernetes.md +41 -14
  55. package/skills/nodefony-devops/references/secrets.md +30 -5
  56. package/skills/nodefony-devops/references/variables.md +66 -31
  57. package/skills/nodefony-migrate-schema/SKILL.md +5 -2
  58. package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
  59. package/skills/nodefony-protect-route/SKILL.md +124 -21
  60. package/skills/nodefony-security-review/SKILL.md +127 -0
  61. package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
  62. package/skills/nodefony-troubleshoot/SKILL.md +225 -0
  63. package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
  64. package/skills/nodefony-upgrade/SKILL.md +130 -0
  65. package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
@@ -1,15 +1,20 @@
1
+ import { asProfilerReader } from "../mcp/observe.js";
2
+ import { readShotTarget } from "../mcp/shotClient.js";
3
+ import { PROFILE_SESSION_FACTOR } from "../src/profileSession.js";
4
+ import { selectBuiltinMcpTools } from "../mcp/tools.js";
1
5
  import defaults from "../config/config.js";
2
6
  import { buildCard } from "../src/card.js";
3
- import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js";
4
- import __decorate from "../../_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorate.js";
5
- import { Module, Nodefony, Service, extend, injectable, mcpDeclaredScopes } from "nodefony";
6
- //#region nodefony/service/DevkitService.ts
7
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
8
+ import { ADMIN_DEFAULT_ROLE, Nodefony, ROLES_WITHIN, Service, extend, injectable } from "nodefony";
9
+ import { defineMcpGate, defineTicketSessionGate, mcpDeclaredScopes } from "@nodefony/ai";
10
+ // nodefony/service/DevkitService.ts
7
11
  /**
8
- * Réponse partagée quand la porte MCP n'est pas protégée — le cas par défaut.
9
- * Allouer un tableau vide pour dire « rien » est la dépense que la règle de
10
- * lazy-allocation proscrit.
12
+ * Ce que l'agent lit en arrivant sur la porte de développement — la phrase
13
+ * d'ouverture des `instructions`, suivie par le moteur des outils servis.
11
14
  */
12
- const EMPTY_PROTECTED_RESOURCES = Object.freeze([]);
15
+ const DEVKIT_MCP_INSTRUCTIONS = "Outils d'introspection d'une application Nodefony — ils répondent depuis l'application qui TOURNE, pas depuis une lecture des sources. Commence par la carte de visite si tu arrives sur cette application. Pour MESURER plutôt que lire le code — une page lente, trop de requêtes SQL, l'effet d'une modification — nodefony_profile_route tire lui-même N requêtes sur un chemin et rend son profil ; nodefony_observe mesure le trafic que tu génères (tests, navigation). Avant d'écrire un script de banc.";
16
+ /** Ce qu'accorde la porte de développement à qui l'atteint sans jeton. */
17
+ const DEV_OPERATOR_ROLES = [ADMIN_DEFAULT_ROLE];
13
18
  /**
14
19
  * Service principal du module — la logique vit ici, pas dans les controllers
15
20
  * (un controller traduit du HTTP/WS ; un service, lui, est réutilisable par la
@@ -20,7 +25,7 @@ const EMPTY_PROTECTED_RESOURCES = Object.freeze([]);
20
25
  *
21
26
  * Un service porte DEUX noms, et c'est normal :
22
27
  * `@injectable()` → nomme la CLASSE (`DevkitService`),
23
- * c'est ce qu'on écrit dans `@inject("…")`
28
+ * c'est ce qu'on écrit dans `@Inject("…")`
24
29
  * `super("devkit", …)` → nomme l'INSTANCE, sa clé dans le conteneur,
25
30
  * c'est ce qu'on écrit dans `kernel.get("…")`
26
31
  * Les deux mènent à la MÊME instance : le conteneur les réconcilie via la classe.
@@ -36,13 +41,13 @@ const EMPTY_PROTECTED_RESOURCES = Object.freeze([]);
36
41
  * ```ts
37
42
  * // 1. INJECTION par le constructeur — la dépendance est DÉCLARÉE, donc le
38
43
  * // conteneur l'ordonnance et elle se voit dans la signature (nom de CLASSE).
39
- * import { inject, injectable, Service, Module } from "nodefony";
44
+ * import { Inject, injectable, Service, Module } from "nodefony";
40
45
  *
41
46
  * @injectable()
42
47
  * class ReportService extends Service {
43
48
  * constructor(
44
49
  * module: Module,
45
- * @inject("DevkitService") private devkit: DevkitService,
50
+ * @Inject("DevkitService") private devkit: DevkitService,
46
51
  * ) {
47
52
  * super("report", module.container, module.notificationsCenter);
48
53
  * }
@@ -56,6 +61,10 @@ const EMPTY_PROTECTED_RESOURCES = Object.freeze([]);
56
61
  let DevkitService = class DevkitService extends Service {
57
62
  module;
58
63
  cfg;
64
+ /** La porte MCP de développement — définie au premier usage (`mcpGate()`). */
65
+ #mcpGate = null;
66
+ /** La porte de session du profilage — définie au premier usage (`profileSessionGate()`). */
67
+ #profileSessionGate = null;
59
68
  constructor(module) {
60
69
  const options = module.options;
61
70
  const merged = extend(true, {}, defaults, options ?? {});
@@ -130,10 +139,37 @@ let DevkitService = class DevkitService extends Service {
130
139
  return {
131
140
  broker: this.get("adminBroker") ?? void 0,
132
141
  getCard: () => this.getCard(),
133
- projectRoot: kernel?.path ?? process.cwd()
142
+ projectRoot: kernel?.path ?? process.cwd(),
143
+ getProfiler: () => asProfilerReader(this.get("profiler")),
144
+ servesTraffic: () => kernel?.runProfile.servers === true,
145
+ onShutdown: (listener) => {
146
+ if (kernel === null) return () => {};
147
+ kernel.prependOnceListener("onTerminate", listener);
148
+ return () => {
149
+ kernel.removeListener("onTerminate", listener);
150
+ };
151
+ },
152
+ profileRoute: {
153
+ getTarget: () => readShotTarget(this.get("server-http")),
154
+ issueTicket: (identifier) => this.profileSessionGate().issue(identifier),
155
+ closeSession: async (ref) => this.profileSessionGate().close(this.get("sessions"), ref),
156
+ rolesWithin: this.get(ROLES_WITHIN) ?? void 0
157
+ }
134
158
  };
135
159
  }
136
160
  /**
161
+ * Les outils `nodefony_*` retenus par l'allowlist `mcp.tools`.
162
+ *
163
+ * Recomposés à chaque appel, jamais mémorisés : la collecte a lieu à la
164
+ * requête MCP, sur une porte qui n'existe qu'en développement — un cache
165
+ * serait une structure de plus à invalider pour un gain nul.
166
+ *
167
+ * @returns les outils, dans l'ordre de l'allowlist
168
+ */
169
+ mcpTools() {
170
+ return selectBuiltinMcpTools(this.cfg.mcp.tools, this.mcpToolDeps());
171
+ }
172
+ /**
137
173
  * Les scopes que la porte MCP EXIGE — dérivés des outils qu'elle déclare.
138
174
  *
139
175
  * 🔴 **Aucune liste de configuration ne double celle-ci**, et c'est la
@@ -149,11 +185,7 @@ let DevkitService = class DevkitService extends Service {
149
185
  * @returns les scopes dédupliqués et triés
150
186
  */
151
187
  declaredMcpScopes() {
152
- return mcpDeclaredScopes({
153
- builtins: this.cfg.mcp.tools,
154
- deps: this.mcpToolDeps(),
155
- modules: this.module.kernel?.modules
156
- });
188
+ return mcpDeclaredScopes({ modules: this.module.kernel?.modules });
157
189
  }
158
190
  /**
159
191
  * Ce que ce module protège, à publier en RFC 9728 — la porte MCP, ou rien.
@@ -178,22 +210,54 @@ let DevkitService = class DevkitService extends Service {
178
210
  * @returns une entrée pour la porte MCP, ou aucune
179
211
  */
180
212
  publishedProtectedResources() {
181
- const mcp = this.cfg.mcp;
182
- const authz = mcp.authorization;
183
- if (!mcp.enabled || authz.authorizationServers.length === 0) return EMPTY_PROTECTED_RESOURCES;
184
- return [{
185
- resource: authz.resource,
186
- authorizationServers: authz.authorizationServers,
187
- scopesSupported: this.declaredMcpScopes(),
188
- resourceName: authz.resourceName,
189
- resourceDocumentation: authz.resourceDocumentation
190
- }];
213
+ return this.mcpGate().protectedResources(this.module.kernel?.modules);
214
+ }
215
+ /**
216
+ * La porte MCP de développement — définie au premier usage, une fois.
217
+ *
218
+ * Bâtie sur `defineMcpGate` comme celle de n'importe quelle application : ses
219
+ * réglages viennent de `devkit.mcp.*`, elle sert les outils de TOUS les modules
220
+ * chargés (`tools: "modules"` — l'outillage éprouve aussi ceux de l'app), et
221
+ * accorde le rôle d'opérateur à qui l'atteint sans jeton. Ce rôle n'est sûr que
222
+ * parce que son PÉRIMÈTRE la protège (module `policy: "dev"`, absent en
223
+ * production) ; une porte protégée par un serveur d'autorisation le tire, elle,
224
+ * des scopes du jeton.
225
+ *
226
+ * @returns la porte, prête à servir.
227
+ */
228
+ mcpGate() {
229
+ if (this.#mcpGate === null) {
230
+ const mcp = this.cfg.mcp;
231
+ const gateProtected = mcp.authorization.authorizationServers.length > 0;
232
+ this.#mcpGate = defineMcpGate({
233
+ enabled: mcp.enabled,
234
+ allowedOrigins: mcp.allowedOrigins,
235
+ allowRemote: mcp.allowRemote,
236
+ authorization: mcp.authorization,
237
+ tools: "modules",
238
+ instructions: DEVKIT_MCP_INSTRUCTIONS,
239
+ unprotectedRoles: gateProtected ? [] : [...DEV_OPERATOR_ROLES]
240
+ });
241
+ }
242
+ return this.#mcpGate;
243
+ }
244
+ /**
245
+ * La porte de session du profilage — définie au premier usage, une fois.
246
+ *
247
+ * Bâtie sur `defineTicketSessionGate` comme le ferait une application pour
248
+ * ses propres outils : locale seulement (défaut de la porte), facteur d'audit
249
+ * {@link PROFILE_SESSION_FACTOR}.
250
+ *
251
+ * @returns la porte, prête à émettre et à servir.
252
+ */
253
+ profileSessionGate() {
254
+ this.#profileSessionGate ??= defineTicketSessionGate({ factor: PROFILE_SESSION_FACTOR });
255
+ return this.#profileSessionGate;
191
256
  }
192
257
  status() {
193
258
  return { ready: this.cfg.enabled };
194
259
  }
195
260
  };
196
- DevkitService = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], DevkitService);
261
+ DevkitService = __decorate([injectable()], DevkitService);
197
262
  var DevkitService_default = DevkitService;
198
- //#endregion
199
263
  export { DevkitService_default as default };
@@ -1,2 +1 @@
1
- import { buildCard, renderCard } from "nodefony";
2
- export { buildCard, renderCard };
1
+ export { buildCard, renderCard } from "nodefony";
@@ -1,4 +1,4 @@
1
- //#region nodefony/src/errors/DevkitError.ts
1
+ // nodefony/src/errors/DevkitError.ts
2
2
  /**
3
3
  * Erreur du module devkit.
4
4
  *
@@ -17,5 +17,4 @@ var DevkitError = class extends Error {
17
17
  this.name = "DevkitError";
18
18
  }
19
19
  };
20
- //#endregion
21
20
  export { DevkitError, DevkitError as default };
@@ -0,0 +1,14 @@
1
+ // nodefony/src/profileSession.ts
2
+ /**
3
+ * Facteur d'authentification déclaré au flux de session : l'audit écrit
4
+ * `login.success` avec cette raison, ce qui distingue une session de profilage
5
+ * d'une vraie connexion.
6
+ */
7
+ const PROFILE_SESSION_FACTOR = "devkit";
8
+ /**
9
+ * Chemin de la route de connexion du profilage, que l'outil appelle. Hors du
10
+ * motif de la zone d'administration (`^/nodefony/[^/]+/api`). Le contrôleur
11
+ * l'écrit en littéraux ; un test tient l'égalité.
12
+ */
13
+ const PROFILE_SESSION_PATH = "/nodefony/devkit/profile-session";
14
+ export { PROFILE_SESSION_FACTOR, PROFILE_SESSION_PATH };
@@ -1,8 +1,10 @@
1
1
  import { Kernel, Module } from "nodefony";
2
+ import type { IMcpTool } from "nodefony";
2
3
  import { type IDevkitConfigInput } from "./nodefony/config/config.js";
3
4
  import DevkitService from "./nodefony/service/DevkitService.js";
4
5
  import DevkitController from "./nodefony/controllers/DevkitController.js";
5
6
  import McpController from "./nodefony/controllers/McpController.js";
7
+ import ProfileSessionController from "./nodefony/controllers/ProfileSessionController.js";
6
8
  /**
7
9
  * @nodefony/devkit — Outillage de developpement d une application Nodefony : carte de visite et portes de decouverte pour un agent
8
10
  *
@@ -43,9 +45,21 @@ declare class DevkitModule extends Module {
43
45
  * fautif nommé, plutôt qu'en `undefined.x` au premier appel en production.
44
46
  */
45
47
  onKernelRegister(): Promise<this>;
48
+ /**
49
+ * Déclare les outils `nodefony_*` — comme n'importe quel module.
50
+ *
51
+ * Aucun cas particulier côté collecte : `@nodefony/ai` les ramasse avec ceux
52
+ * de l'application, et seul le préfixe `nodefony_`, réservé aux modules
53
+ * `@nodefony/*`, les distingue. Un service absent (module en cours de
54
+ * démontage) ne déclare rien plutôt que de lever.
55
+ *
56
+ * @returns les outils retenus par l'allowlist `mcp.tools`
57
+ */
58
+ getMcpTools(): IMcpTool[];
46
59
  }
47
60
  export default DevkitModule;
48
- export { DevkitService, DevkitController, McpController };
61
+ export { DevkitService, DevkitController, McpController, ProfileSessionController, };
62
+ export { builtinMcpTools, selectBuiltinMcpTools, BUILTIN_MCP_TOOL_KEYS, type BuiltinMcpToolKey, type IMcpToolDeps, } from "./nodefony/mcp/tools.js";
49
63
  export { buildCard } from "./nodefony/src/card.js";
50
64
  export { defineDevkitConfig } from "./nodefony/config/defineModuleConfig.js";
51
65
  export { devkitConfigSchema, type IDevkitConfig, type IDevkitConfigInput, } from "./nodefony/config/config.js";
@@ -5,7 +5,6 @@ export declare const devkitConfigSchema: z.ZodObject<{
5
5
  enabled: z.ZodDefault<z.ZodBoolean>;
6
6
  allowedOrigins: z.ZodDefault<z.ZodArray<z.ZodString>>;
7
7
  allowRemote: z.ZodDefault<z.ZodBoolean>;
8
- tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
9
8
  authorization: z.ZodDefault<z.ZodObject<{
10
9
  authorizationServers: z.ZodDefault<z.ZodArray<z.ZodString>>;
11
10
  resource: z.ZodDefault<z.ZodString>;
@@ -14,6 +13,7 @@ export declare const devkitConfigSchema: z.ZodObject<{
14
13
  resourceDocumentation: z.ZodOptional<z.ZodString>;
15
14
  anonymous: z.ZodDefault<z.ZodBoolean>;
16
15
  }, z.core.$strict>>;
16
+ tools: z.ZodDefault<z.ZodArray<z.ZodString>>;
17
17
  }, z.core.$strict>>;
18
18
  }, z.core.$strict>;
19
19
  /** Config telle que l'APP l'écrit dans `use()` — tous les champs optionnels. */
@@ -1,8 +1,8 @@
1
1
  import { Controller } from "@nodefony/framework";
2
2
  import type { ContextType } from "@nodefony/http";
3
3
  /**
4
- * Serveur **Model Context Protocol** de l'application — la porte par laquelle
5
- * un agent externe l'interroge.
4
+ * Porte **Model Context Protocol** de DÉVELOPPEMENT — celle par laquelle
5
+ * l'agent de code du développeur interroge l'application.
6
6
  *
7
7
  * ## Pourquoi une route, et pas un process
8
8
  *
@@ -19,7 +19,7 @@ import type { ContextType } from "@nodefony/http";
19
19
  *
20
20
  * **Par défaut, le périmètre — et lui seul.** Ce module est `policy: "dev"`,
21
21
  * donc cette route n'existe pas en production ; restent les deux gardes que la
22
- * spec impose au transport, portées par {@link checkMcpAccess} : l'en-tête
22
+ * spec impose au transport (`checkMcpAccess`, `@nodefony/ai`) : l'en-tête
23
23
  * `Origin` et la localité de l'appelant. Elles visent le seul vecteur réel
24
24
  * contre un serveur local — une page web ouverte dans le navigateur du
25
25
  * développeur.
@@ -46,25 +46,21 @@ import type { ContextType } from "@nodefony/http";
46
46
  * Rôle déclaré + aucun vérificateur = `503` et journal `CRITIC`, jamais une
47
47
  * porte qui laisse passer les porteurs sans les lire.
48
48
  *
49
- * **Mince par design** : tout le protocole vit AU CŒUR (`nodefony`, en fonctions
50
- * pures) ; ce controller ne fait que traduire HTTP ↔ JSON-RPC et fournir au
51
- * collecteur ce que lui seul connaît — le service du module, le broker
52
- * d'administration, la racine du projet. C'est ce qui permet à un autre module
53
- * d'ouvrir la même porte ailleurs (en production, sous authentification) sans
54
- * réécrire une ligne de protocole.
49
+ * **Mince par design** : la porte est définie par `defineMcpGate` (`@nodefony/ai`)
50
+ * dans `DevkitService.mcpGate()`, à partir de `devkit.mcp.*` ; ce controller
51
+ * ne fait que la servir. Une application ouvre SA porte, en développement ou en
52
+ * production, exactement de la même façon.
55
53
  */
56
54
  declare class McpController extends Controller {
57
55
  #private;
58
56
  constructor(context: ContextType);
59
57
  /**
60
- * `POST /nodefony/mcp` — un message JSON-RPC entre, une réponse sort.
61
- *
62
- * Les trois statuts que rend cette route sont ceux que la spec impose, et pas
63
- * un de plus : `202` sans corps pour une notification acceptée, `403` pour un
64
- * appel dont l'origine ou l'adresse est refusée, `404` pour une méthode
65
- * inconnue (afin de la distinguer d'un serveur qui n'hébergerait pas
66
- * d'endpoint MCP du tout).
58
+ * `/nodefony/mcp` — `POST` sert un message JSON-RPC ; `GET` et `DELETE`
59
+ * rendent `405` + `Allow: POST` (verbes retirés par la révision
60
+ * `2026-07-28`). Sans eux, la requête tombait dans le repli SPA du Studio
61
+ * (`/nodefony/{page}`) : `200` + HTML, que le client prenait pour un flux
62
+ * rompu et rouvrait à chaque seconde, indéfiniment.
67
63
  */
68
- mcp(body: unknown, origin?: string, protocolVersion?: string, authorization?: string): Promise<import("@nodefony/http").Http2Response | import("@nodefony/http").HttpResponse | import("@nodefony/http").WebsocketResponse>;
64
+ mcp(body: unknown): Promise<unknown>;
69
65
  }
70
66
  export default McpController;
@@ -0,0 +1,31 @@
1
+ import { Controller } from "@nodefony/framework";
2
+ import type { ContextType } from "@nodefony/http";
3
+ /**
4
+ * Ouvre la session au nom de laquelle `nodefony_profile_route` tire ses
5
+ * requêtes.
6
+ *
7
+ * **Mince par design** : la mécanique (ticket à usage unique, localité,
8
+ * ouverture par le flux de session de l'application, verdict uniforme) est la
9
+ * porte `defineTicketSessionGate` de `@nodefony/ai`, définie dans
10
+ * `DevkitService.profileSessionGate()`. Ce controller ne fait que DÉCLARER la
11
+ * route, comme le ferait une application pour ses propres outils.
12
+ *
13
+ * Ce qui protège cette route : le ticket (émis dans le process, valable dix
14
+ * secondes, jamais affiché ni journalisé, identité fixée à l'émission), la
15
+ * localité de l'appelant (défaut de la porte), et la `policy: "dev"` du module.
16
+ *
17
+ * Elle vit sous `/nodefony/devkit/`, hors du motif de la zone d'administration
18
+ * (`^/nodefony/[^/]+/api`), qui exigerait une session déjà ouverte.
19
+ */
20
+ declare class ProfileSessionController extends Controller {
21
+ #private;
22
+ constructor(context: ContextType);
23
+ /**
24
+ * `POST /nodefony/devkit/profile-session` — rachète le ticket et ouvre la
25
+ * session. `200` rend le compte et le pseudonyme de session ; tout refus est
26
+ * un `403` au message unique ; `503` quand l'application n'a pas de flux de
27
+ * session.
28
+ */
29
+ profileSession(): Promise<unknown>;
30
+ }
31
+ export default ProfileSessionController;
@@ -1,4 +1,6 @@
1
- import type { ICard, ICardAppInfo, ICardDoor, ICardInput, ICardVerb, IMcpToolDeps } from "nodefony";
1
+ import type { IMcpGate, ITicketSessionGate } from "@nodefony/ai";
2
+ import type { ICard, ICardAppInfo, ICardDoor, ICardInput, ICardVerb, IMcpTool } from "nodefony";
3
+ import type { IMcpToolDeps } from "../mcp/tools.js";
2
4
  import type { IDevkitConfig } from "../config/config.js";
3
5
  /**
4
6
  * Le contrat de la carte est celui du CŒUR — ces noms n'en sont que les alias,
@@ -58,6 +60,11 @@ export interface IDevkitService {
58
60
  * dont sont dérivés les scopes publiés.
59
61
  */
60
62
  mcpToolDeps(): IMcpToolDeps;
63
+ /**
64
+ * Les outils `nodefony_*` que l'allowlist `mcp.tools` retient — ceux que le
65
+ * module déclare par `getMcpTools()`.
66
+ */
67
+ mcpTools(): IMcpTool[];
61
68
  /**
62
69
  * Les scopes que la porte MCP EXIGE, dérivés des outils déclarés.
63
70
  *
@@ -66,4 +73,18 @@ export interface IDevkitService {
66
73
  * n'exige rien, et le champ est alors omis du document.
67
74
  */
68
75
  declaredMcpScopes(): readonly string[];
76
+ /**
77
+ * La porte MCP de développement (`POST /nodefony/mcp`), bâtie sur
78
+ * `defineMcpGate` à partir de `devkit.mcp.*` — définie une fois, au premier usage.
79
+ */
80
+ mcpGate(): IMcpGate;
81
+ /**
82
+ * La porte de session du profilage (`POST /nodefony/devkit/profile-session`),
83
+ * bâtie sur `defineTicketSessionGate` — définie une fois, au premier usage.
84
+ *
85
+ * L'outil de profilage y émet le ticket qui ouvrira SA session ; la route la
86
+ * sert. Le magasin de tickets n'est alloué qu'au premier ticket émis : une
87
+ * requête reçue n'alloue rien.
88
+ */
89
+ profileSessionGate(): ITicketSessionGate;
69
90
  }
@@ -0,0 +1,83 @@
1
+ import { type IMcpCaller, type IMcpTool } from "nodefony";
2
+ import { type IProfilerReader } from "@nodefony/http";
3
+ import { type RequestLogs } from "./profileReport.js";
4
+ import { type OnShutdown } from "./shutdown.js";
5
+ /** Fenêtre d'observation par défaut, en secondes. */
6
+ export declare const OBSERVE_DEFAULT_SEC = 10;
7
+ /**
8
+ * Fenêtre maximale, en secondes. Sous les 60 s qu'un client MCP attend par
9
+ * défaut avant d'abandonner une requête : au-delà, l'agent couperait l'appel
10
+ * juste avant le rapport.
11
+ */
12
+ export declare const OBSERVE_MAX_SEC = 50;
13
+ /** Intervalle entre deux relevés, en ms : un instantané de progression par relevé. */
14
+ export declare const OBSERVE_TICK_MS = 2000;
15
+ /**
16
+ * Requêtes en erreur dont on lit le journal, au plus — les mêmes que les
17
+ * exemples d'erreurs du rapport, dans le même ordre.
18
+ */
19
+ export declare const OBSERVE_ERROR_LOGS_MAX = 10;
20
+ /** La sonde du process : ce que l'outil lit du process à chaque relevé. */
21
+ export interface IObserveProbe {
22
+ read(): {
23
+ eventLoopMs: number;
24
+ eventLoopMaxMs: number;
25
+ heapUsed: number;
26
+ };
27
+ dispose(): void;
28
+ }
29
+ /**
30
+ * Ce que rend un outil de MESURE dans un process qui ne sert aucune requête —
31
+ * l'application démarrée en console, sous la porte stdio (`nodefony ai:stdio`).
32
+ * Sans ce refus, `nodefony_observe` y attendrait un trafic qui n'arrive jamais
33
+ * et rendrait « aucune requête » avec aplomb.
34
+ */
35
+ export declare const NOT_SERVING_MESSAGE: string;
36
+ /** Ce dont l'outil a besoin — injecté, pour l'éprouver sans application. */
37
+ export interface IObserveDeps {
38
+ /** Le profileur de l'application, ou `undefined` (production, `@nodefony/http` absent). */
39
+ getProfiler: () => IProfilerReader | undefined;
40
+ /**
41
+ * Ce process sert-il des requêtes ? Rend `false` sous la porte stdio :
42
+ * l'outil le dit ({@link NOT_SERVING_MESSAGE}). Absent : supposé servir.
43
+ */
44
+ servesTraffic?: (() => boolean) | undefined;
45
+ /** Intervalle entre deux relevés (défaut {@link OBSERVE_TICK_MS}). */
46
+ tickMs?: number | undefined;
47
+ /** Fabrique de la sonde du process (défaut : `ProcessProbe` du cœur). */
48
+ createProbe?: (() => IObserveProbe) | undefined;
49
+ /**
50
+ * Lit les lignes de journal d'une requête (syslog de l'application, par
51
+ * `requestId`). Absent : le rapport renvoie à `nodefony_admin_call`.
52
+ */
53
+ readRequestLogs?: ((requestId: string, caller: IMcpCaller) => Promise<RequestLogs>) | undefined;
54
+ /**
55
+ * S'abonne à l'arrêt du serveur pour la durée d'UN appel, et rend de quoi
56
+ * se désabonner. L'arrêt attend la promesse de l'écouteur (bornée) : le
57
+ * temps que le rapport partiel parte.
58
+ */
59
+ onShutdown?: OnShutdown | undefined;
60
+ }
61
+ /**
62
+ * Reconnaît un profileur par sa FORME : le conteneur le rend sous le nom
63
+ * `"profiler"`, et ce module ne dépend pas de sa classe.
64
+ *
65
+ * @param value - ce que le conteneur a rendu.
66
+ * @returns le lecteur, ou `undefined` s'il ne tient pas le contrat.
67
+ */
68
+ export declare function asProfilerReader(value: unknown): IProfilerReader | undefined;
69
+ /**
70
+ * L'outil `nodefony_observe` : observe l'application qui tourne pendant une
71
+ * fenêtre bornée et rend ce que le profileur de développement a mesuré —
72
+ * routes lentes et où part leur temps, N+1, requêtes SQL coûteuses, erreurs,
73
+ * charge du process.
74
+ *
75
+ * C'est un outil LONG : un relevé toutes les deux secondes, chacun envoyé à
76
+ * l'agent en progression ; un client qui l'annule l'arrête au relevé suivant,
77
+ * qu'il ferme le flux de la réponse ou envoie `notifications/cancelled`. Rien
78
+ * n'est armé en dehors d'un appel : un minuteur par relevé,
79
+ * libéré par l'annulation elle-même, et la sonde du process libérée en sortie.
80
+ *
81
+ * @param deps - le profileur et, pour les tests, la cadence et la sonde.
82
+ */
83
+ export declare function observeTool(deps: IObserveDeps): IMcpTool;
@@ -0,0 +1,138 @@
1
+ import { type IProfileReport } from "@nodefony/http";
2
+ /**
3
+ * Le rapport de profil tel qu'un AGENT de code le lit — rendu partagé par les
4
+ * outils qui observent l'application (`nodefony_observe`) ou la sollicitent.
5
+ *
6
+ * L'ordre est celui de la décision : un verdict d'abord, puis ce qu'il faut
7
+ * corriger (une anomalie par ligne, nommée par route, contrôleur et forme SQL),
8
+ * les tableaux ensuite, et les données brutes en dernier pour comparer deux
9
+ * appels. Chaque section est bornée : le texte entier reste sous la limite que
10
+ * la porte impose à une réponse d'outil.
11
+ */
12
+ /** p95 à partir duquel une route est signalée lente (développement, machine locale). */
13
+ export declare const SLOW_ROUTE_P95_MS = 500;
14
+ /**
15
+ * Pire blocage de la boucle d'événements à partir duquel on le signale : au-delà,
16
+ * TOUTES les requêtes en cours ont attendu, quelle que soit leur route.
17
+ */
18
+ export declare const EVENT_LOOP_BLOCK_MS = 100;
19
+ /** Les lignes de journal d'une requête, ou la raison pour laquelle on ne les a pas. */
20
+ export type RequestLogs = {
21
+ kind: "lines";
22
+ lines: string[];
23
+ /** Lignes lues mais écartées (détail sous WARNING, ou au-delà du plafond). */
24
+ hidden?: number | undefined;
25
+ } | {
26
+ kind: "unavailable";
27
+ reason: string;
28
+ };
29
+ /** Au nom de qui les requêtes d'un tir ont été jouées. */
30
+ export type ShotIdentity = {
31
+ kind: "anonymous";
32
+ /** Pourquoi aucun compte n'a été joué ; `null` quand l'appelant l'a demandé. */
33
+ why: string | null;
34
+ } | {
35
+ kind: "account";
36
+ identifier: string;
37
+ roles: readonly string[];
38
+ /** Un autre compte que le propriétaire du jeton (`as`). */
39
+ impersonated: boolean;
40
+ };
41
+ /** Ce que le client du tir a reçu, statut par statut. */
42
+ export interface IShotClientStatus {
43
+ ok: number;
44
+ redirected: number;
45
+ unauthorized: number;
46
+ forbidden: number;
47
+ clientError: number;
48
+ serverError: number;
49
+ /** Requêtes sans réponse (connexion refusée, délai dépassé). */
50
+ failed: number;
51
+ /** Première destination de redirection reçue. */
52
+ location: string | null;
53
+ /** Première erreur réseau reçue. */
54
+ failure: string | null;
55
+ }
56
+ /**
57
+ * Le tir de `nodefony_profile_route` : ce que l'outil a envoyé, au nom de qui,
58
+ * et ce que son client a reçu. Absent pour `nodefony_observe`, qui n'envoie rien.
59
+ */
60
+ export interface IShotSummary {
61
+ method: "GET" | "HEAD";
62
+ path: string;
63
+ requested: number;
64
+ sent: number;
65
+ concurrency: number;
66
+ identity: ShotIdentity;
67
+ /** Lot de chauffe, tiré AVANT et hors des mesures. */
68
+ warmup: {
69
+ count: number;
70
+ p50Ms: number | null;
71
+ maxMs: number | null;
72
+ };
73
+ client: IShotClientStatus;
74
+ /** Réponses reçues dont le profil n'a pas été retrouvé. */
75
+ missing: number;
76
+ /** Tir arrêté faute de temps, avant d'avoir tout envoyé. */
77
+ truncated: boolean;
78
+ /** Budget de temps du tir, en ms. */
79
+ budgetMs: number;
80
+ }
81
+ /** Le déroulé de l'observation : ce que le rapport doit dire de sa propre fiabilité. */
82
+ export interface IObservationContext {
83
+ /** Fenêtre demandée, en ms. */
84
+ windowMs: number;
85
+ /** Durée réellement observée, en ms. */
86
+ elapsedMs: number;
87
+ /** Relevés effectués. */
88
+ ticks: number;
89
+ /** Profils disparus avant d'être lus (tampon débordé, identifiant réutilisé). */
90
+ lost: number;
91
+ /** L'agent a abandonné l'appel avant la fin de la fenêtre. */
92
+ cancelled: boolean;
93
+ /** Le serveur s'est arrêté pendant la fenêtre (rechargement, arrêt). */
94
+ interrupted: boolean;
95
+ /**
96
+ * Journal des requêtes en erreur, lu au relevé qui les a vues ; `null` quand
97
+ * l'outil n'a pas de quoi le lire.
98
+ */
99
+ errorLogs: ReadonlyMap<string, RequestLogs> | null;
100
+ /** Route à laquelle l'observation était restreinte, s'il y en avait une. */
101
+ route: string | null;
102
+ /** Requêtes écartées par ce filtre de route. */
103
+ filteredRoute: number;
104
+ pid: number;
105
+ uptimeSec: number;
106
+ /**
107
+ * Retard de la boucle d'événements sur la fenêtre (ms), si mesuré : moyen,
108
+ * et pire blocage (qu'une moyenne noie).
109
+ */
110
+ eventLoop: {
111
+ meanMs: number;
112
+ maxMs: number;
113
+ } | null;
114
+ /**
115
+ * Variation du tas sur la fenêtre (octets), si mesurée — sans GC forcé : du
116
+ * déchet en attente autant que de la rétention, donc indicative.
117
+ */
118
+ heapDeltaBytes: number | null;
119
+ /** Le tir, quand c'est l'outil qui a envoyé les requêtes. */
120
+ shot?: IShotSummary | undefined;
121
+ }
122
+ /**
123
+ * Rend le rapport complet pour l'agent.
124
+ *
125
+ * @param report - l'agrégation des profils observés.
126
+ * @param ctx - le déroulé de l'observation.
127
+ * @returns le texte, borné.
128
+ */
129
+ export declare function renderProfileReport(report: IProfileReport, ctx: IObservationContext): string;
130
+ /**
131
+ * La ligne d'un relevé intermédiaire, envoyée en progression à l'agent.
132
+ *
133
+ * @param report - l'agrégation à cet instant.
134
+ * @param elapsedSec - secondes écoulées.
135
+ * @param windowSec - fenêtre demandée.
136
+ * @param lost - profils perdus jusqu'ici.
137
+ */
138
+ export declare function progressLine(report: IProfileReport, elapsedSec: number, windowSec: number, lost: number): string;