@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.
- package/README.md +151 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +27 -163
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/card.js +1 -2
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +13 -17
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +217 -20
- package/package.json +17 -12
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-realtime-channel/SKILL.md +66 -9
- package/skills/nodefony-add-service/SKILL.md +14 -3
- package/skills/nodefony-authentication/SKILL.md +234 -0
- package/skills/nodefony-browser/SKILL.md +65 -21
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/references/sondes.md +13 -0
- package/skills/nodefony-browser/scripts/audit.mjs +59 -10
- package/skills/nodefony-browser/scripts/inspect.mjs +123 -5
- package/skills/nodefony-browser/scripts/lib/browser.mjs +112 -6
- package/skills/nodefony-browser/scripts/lib/probes.mjs +219 -7
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +78 -82
- package/skills/nodefony-dev/scripts/docs.mjs +24 -8
- package/skills/nodefony-devops/SKILL.md +98 -74
- package/skills/nodefony-devops/references/compose.md +8 -0
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +282 -48
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +30 -5
- package/skills/nodefony-devops/references/variables.md +66 -31
- package/skills/nodefony-migrate-schema/SKILL.md +5 -2
- package/skills/nodefony-migrate-schema/references/verdicts.md +20 -19
- package/skills/nodefony-protect-route/SKILL.md +124 -21
- package/skills/nodefony-security-review/SKILL.md +127 -0
- package/skills/nodefony-security-review/scripts/review-diff.mjs +1090 -0
- package/skills/nodefony-troubleshoot/SKILL.md +225 -0
- package/skills/nodefony-troubleshoot/scripts/log-errors.mjs +465 -0
- package/skills/nodefony-upgrade/SKILL.md +130 -0
- 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
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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 `@
|
|
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 {
|
|
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
|
-
* @
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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()
|
|
261
|
+
DevkitService = __decorate([injectable()], DevkitService);
|
|
197
262
|
var DevkitService_default = DevkitService;
|
|
198
|
-
//#endregion
|
|
199
263
|
export { DevkitService_default as default };
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { buildCard, renderCard };
|
|
1
|
+
export { buildCard, renderCard } from "nodefony";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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** :
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
|
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 {
|
|
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;
|