@fkom13/mcp-sftp-orchestrator 6.0.0 → 11.8.0
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/.env.example +27 -15
- package/CHANGELOG.md +174 -0
- package/README.fr.md +356 -0
- package/README.md +314 -60
- package/apis.js +20 -64
- package/atomicJsonStore.js +117 -0
- package/compareEngine.js +132 -0
- package/config.js +40 -4
- package/diagnose.js +89 -0
- package/diffEngine.js +141 -0
- package/diffFormatter.js +114 -0
- package/fileOps.js +275 -0
- package/fleet.js +170 -0
- package/groups.js +25 -0
- package/guide.js +236 -0
- package/history.js +5 -44
- package/infraTopology.js +336 -0
- package/inventory.js +151 -0
- package/lhm.plugin.json +7 -0
- package/notes.js +3 -0
- package/package.json +11 -26
- package/policies.js +8 -0
- package/projects.js +108 -0
- package/queue.js +180 -125
- package/server.js +2552 -469
- package/servers.js +30 -33
- package/sftp.js +284 -86
- package/shellSessions.js +265 -0
- package/snapshotManager.js +320 -0
- package/sourceAdapter.js +410 -0
- package/ssh.js +184 -86
- package/sshPool.js +161 -148
- package/sshTrust.js +214 -0
- package/toolMetadata.js +76 -0
- package/tunnels.js +243 -0
- package/utils.js +215 -0
- package/workSession.js +139 -0
- package/publish1github.md +0 -216
- package/test_features.js +0 -112
package/README.md
CHANGED
|
@@ -1,102 +1,356 @@
|
|
|
1
|
-
# 🚀 MCP SFTP
|
|
1
|
+
# 🚀 MCP Orchestrator — Serveur d'orchestration SSH/SFTP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **v11.8.0 Security Refresh** — Les 82 tools publient désormais les annotations MCP standard. `infra_overview` reste léger sans argument, mais `infra_overview { alias: "..." }` effectue une découverte live et corrèle Nginx/domaines, ports, Docker/Compose et services. Voir `CHANGELOG.md`.
|
|
4
4
|
|
|
5
|
-
✨ **Fonctionnalités Principales**
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
**Version** : 11.8.0
|
|
7
|
+
**Tools** : 82
|
|
8
|
+
**License** : MIT
|
|
9
|
+
**Node** : >= 18.0.0
|
|
10
|
+
**Changelog** : [CHANGELOG.md](./CHANGELOG.md)
|
|
11
|
+
|
|
12
|
+
Serveur MCP (Model Context Protocol, transport **stdio**) qui donne à un agent IA la capacité d’orchestrer un parc de serveurs : SSH, SFTP, édition de fichiers locale/remote (hash-safe), diffs cross-server, shell PTY, snapshots d’infra, notes de protocole, **projets**, **sessions de travail**, inventaire, trust SSH (pubkey), groupes d’alias et audit parc.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ✨ Points forts (v11.6)
|
|
17
|
+
|
|
18
|
+
| Domaine | Capacité |
|
|
19
|
+
|---------|----------|
|
|
20
|
+
| **Exécution** | `task_exec` multi-serveur / `group:oci` / dry-run destructif / force |
|
|
21
|
+
| **Fichiers** | `file_read` / `file_edit` (chirurgical) / `file_write` + hash + dryRun + backup |
|
|
22
|
+
| **Parc** | notes, `infra_audit`, `fleet_status`, `server_inventory` |
|
|
23
|
+
| **Projets** | registre local↔remote + `project_diff` |
|
|
24
|
+
| **Travail** | `work_start` → edits → `work_end` (note d’intervention auto) |
|
|
25
|
+
| **Trust** | `ssh_authorize_key` (pubkey only, dry_run par défaut) |
|
|
26
|
+
| **Sécu** | secrets masqués, RO global + **RO par alias**, blocklist, pool SSH robuste |
|
|
27
|
+
|
|
28
|
+
---
|
|
14
29
|
|
|
15
30
|
## 📦 Installation
|
|
16
31
|
|
|
17
|
-
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/fkom13/mcp-sftp-orchestrator.git
|
|
34
|
+
cd sftp-mcp # ou tools/sftp-mcp
|
|
35
|
+
npm install
|
|
36
|
+
cp .env.example .env
|
|
37
|
+
# Éditer MCP_DATA_DIR et chemins de clés
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Prérequis : **Node.js >= 18**
|
|
41
|
+
|
|
42
|
+
---
|
|
18
43
|
|
|
19
|
-
|
|
44
|
+
## ⚙️ Configuration (`.env`)
|
|
45
|
+
|
|
46
|
+
Toutes les variables sont optionnelles.
|
|
47
|
+
|
|
48
|
+
| Variable | Défaut | Description |
|
|
49
|
+
|----------|--------|-------------|
|
|
50
|
+
| `MCP_DATA_DIR` | `~/.config/mcp-orchestrator` | Dossier data (JSON, snapshots, projets…) |
|
|
51
|
+
| `MCP_SYNC_TIMEOUT_S` | `120` | Délai (s) avant passage d’une tâche en arrière-plan |
|
|
52
|
+
| `MCP_DEFAULT_CMD_TIMEOUT_S` | `600` | Timeout SSH commande (s). `0` = infini |
|
|
53
|
+
| `MCP_INTERACTIVE_CMD_TIMEOUT_S` | `300` | Timeout interactif (s). `0` = infini |
|
|
54
|
+
| `MCP_MAX_WAIT_TIMEOUT_S` | `600` | Timeout max `task_wait` (s) |
|
|
55
|
+
| `MAX_CONNECTIONS_PER_SERVER` | `5` | Pool SSH max / serveur |
|
|
56
|
+
| `MIN_CONNECTIONS_PER_SERVER` | `1` | Pool SSH min / serveur |
|
|
57
|
+
| `IDLE_TIMEOUT` | `300000` | Fermeture connexion inactive (ms) |
|
|
58
|
+
| `KEEP_ALIVE_INTERVAL` | `30000` | Keepalive SSH (ms) |
|
|
59
|
+
| `MAX_QUEUE_SIZE` | `1000` | Taille max queue jobs |
|
|
60
|
+
| `SAVE_INTERVAL` | `5000` | Autosave queue (ms) |
|
|
61
|
+
| `MCP_ALLOWED_ROOTS` | _(vide)_ | Racines autorisées pour paths locaux (CSV) |
|
|
62
|
+
| `MCP_READONLY` | `false` | `1` = refuse écritures / exec mutantes (global) |
|
|
63
|
+
| `MCP_COMPACT` | `false` | `1` = réponses tronquées (tokens agent) |
|
|
64
|
+
| `MCP_DEBUG` | `false` | Logs détaillés stderr |
|
|
65
|
+
|
|
66
|
+
### Fichiers sous `MCP_DATA_DIR`
|
|
67
|
+
|
|
68
|
+
| Fichier | Contenu |
|
|
69
|
+
|---------|---------|
|
|
70
|
+
| `servers.json` | Alias SSH (`host`, `user`, `keyPath`/`password`, `port?`, `readonly?`) |
|
|
71
|
+
| `apis.json` | Catalogue APIs (secrets masqués en lecture tools) |
|
|
72
|
+
| `queue.json` / `queue.backup.json` | Jobs |
|
|
73
|
+
| `history.json` | Historique tâches |
|
|
74
|
+
| `server_notes.json` | Protocoles / notes par serveur |
|
|
75
|
+
| `server_groups.json` | Groupes d’alias |
|
|
76
|
+
| `projects.json` | Registre projets |
|
|
77
|
+
| `work_sessions.json` | Sessions de travail |
|
|
78
|
+
| `policies.json` | Blocklist commandes |
|
|
79
|
+
| `tunnels.json` / `tunnel_allowlist.json` | Tunnels SSH |
|
|
80
|
+
| `infra_snapshots/` | Snapshots content-addressable |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🔌 Connexion client MCP
|
|
85
|
+
|
|
86
|
+
### Grok / config.toml
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[mcp_servers.orchestrator]
|
|
90
|
+
command = "node"
|
|
91
|
+
args = ["/chemin/absolu/sftp-mcp/server.js"]
|
|
92
|
+
# optionnel:
|
|
93
|
+
# env = { MCP_DATA_DIR = "/chemin/absolu/sftp-mcp/data" }
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### OpenCode / Claude Desktop (JSON)
|
|
20
97
|
|
|
21
98
|
```json
|
|
22
99
|
{
|
|
23
100
|
"mcpServers": {
|
|
24
|
-
"
|
|
25
|
-
"command": "
|
|
26
|
-
"args": [
|
|
27
|
-
"@fkom13/mcp-sftp-orchestrator"
|
|
28
|
-
],
|
|
101
|
+
"orchestrator": {
|
|
102
|
+
"command": "node",
|
|
103
|
+
"args": ["/chemin/absolu/sftp-mcp/server.js"],
|
|
29
104
|
"env": {
|
|
30
|
-
"MCP_DATA_DIR": "
|
|
105
|
+
"MCP_DATA_DIR": "/chemin/absolu/sftp-mcp/data"
|
|
31
106
|
}
|
|
32
107
|
}
|
|
33
108
|
}
|
|
34
109
|
}
|
|
35
110
|
```
|
|
36
111
|
|
|
37
|
-
|
|
112
|
+
Après modification du code : recharger le serveur MCP (`/mcps` → `r` ou restart session). Vérifier `system_diagnostics` → `version: "11.8.0"`.
|
|
38
113
|
|
|
39
|
-
|
|
114
|
+
---
|
|
40
115
|
|
|
41
|
-
|
|
116
|
+
## 🧰 Référence des outils (82)
|
|
42
117
|
|
|
43
|
-
|
|
44
|
-
|
|
118
|
+
### Diagnostic & audit
|
|
119
|
+
| Outil | Description |
|
|
120
|
+
|-------|-------------|
|
|
121
|
+
| `help` | Guide outils + .env + astuces |
|
|
122
|
+
| `guide` | Manuel IA (workflows, cheatsheet, pitfalls, audit, security) |
|
|
123
|
+
| `system_diagnostics` | Queue, pool, serveurs/APIs **masqués**, version, readOnly |
|
|
124
|
+
| `infra_audit` | Synthèse parc + projets + notes + crashed |
|
|
125
|
+
| `infra_overview` | Serveurs + notes (vue légère) |
|
|
126
|
+
| `fleet_status` | Ping SSH parallèle (latence, load, disk) |
|
|
127
|
+
| `server_inventory` | Inventaire léger (pm2/docker/disk/home, cache 10 min) |
|
|
45
128
|
|
|
46
|
-
|
|
129
|
+
### Serveurs & groupes
|
|
130
|
+
| Outil | Description |
|
|
131
|
+
|-------|-------------|
|
|
132
|
+
| `server_add` | CRUD alias (`keyPath` ou `password`, `port`, **`readonly`**) |
|
|
133
|
+
| `server_list` | Liste (passwords masqués) |
|
|
134
|
+
| `server_remove` | Supprime un alias |
|
|
135
|
+
| `server_group_list/set/remove` | Groupes (`oci`, `contabo`…). Usage : `group:oci` ou nom de groupe |
|
|
47
136
|
|
|
48
|
-
|
|
137
|
+
### Projets (v11.6)
|
|
138
|
+
| Outil | Description |
|
|
139
|
+
|-------|-------------|
|
|
140
|
+
| `project_list` / `project_get` / `project_set` / `project_remove` | Registre |
|
|
141
|
+
| `project_resolve` | → `{ local, remote, ignore, runtime }` |
|
|
142
|
+
| `project_diff` | Diff local↔remote du projet |
|
|
49
143
|
|
|
50
|
-
|
|
144
|
+
Exemple `project_set` :
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"name": "p-image",
|
|
149
|
+
"local": { "path": "/home/.../dev-serveur/p-image" },
|
|
150
|
+
"servers": {
|
|
151
|
+
"prod": {
|
|
152
|
+
"alias": "fkomprodmini2_prod",
|
|
153
|
+
"path": "/home/ubuntu/p-image",
|
|
154
|
+
"runtime": { "pm2": "p-image", "port": 5002 },
|
|
155
|
+
"url": "https://pruna.esprit-artificiel.com"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"ignore": ["node_modules", ".git", "data"]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Sessions de travail (v11.6)
|
|
163
|
+
| Outil | Description |
|
|
164
|
+
|-------|-------------|
|
|
165
|
+
| `work_start` | Ouvre un journal (`alias`, `project`, `tag`, snapshot optionnel) |
|
|
166
|
+
| `work_log` | Event (`file_edit`, `task_exec`, …) |
|
|
167
|
+
| `work_list` | Sessions actives (+ historique) |
|
|
168
|
+
| `work_end` | Clôture + `server_note` `last_intervention` |
|
|
169
|
+
|
|
170
|
+
### Trust SSH (v11.6)
|
|
171
|
+
| Outil | Description |
|
|
172
|
+
|-------|-------------|
|
|
173
|
+
| `ssh_authorize_key` | Ajoute une **pubkey** dans `authorized_keys` distant. `dry_run` défaut. Sources : `string` \| `local_path` \| `alias` |
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"target_alias": "fkomprodmini1_prod",
|
|
178
|
+
"source": { "type": "alias", "alias": "vps_contabo" },
|
|
179
|
+
"comment": "fleet-from-contabo",
|
|
180
|
+
"dry_run": true
|
|
181
|
+
}
|
|
182
|
+
```
|
|
51
183
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
184
|
+
### Policies
|
|
185
|
+
| Outil | Description |
|
|
186
|
+
|-------|-------------|
|
|
187
|
+
| `policy_blocklist_list/add/remove` | Blocklist commandes (aussi appliquée à shell + sequences) |
|
|
55
188
|
|
|
56
|
-
###
|
|
189
|
+
### Catalogue API
|
|
190
|
+
| Outil | Description |
|
|
191
|
+
|-------|-------------|
|
|
192
|
+
| `api_add` / `api_list` / `api_remove` / `api_check` | Monitoring (clés masquées en list) |
|
|
57
193
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
194
|
+
### Exécution de tâches
|
|
195
|
+
| Outil | Description |
|
|
196
|
+
|-------|-------------|
|
|
197
|
+
| `task_exec` | SSH ; alias \| tableau \| `all` \| `group:x` ; dry_run/force destructif |
|
|
198
|
+
| `task_exec_interactive` | Prompts yes/no, menus |
|
|
199
|
+
| `task_exec_sequence` | Séquence sur un serveur (policy par étape) |
|
|
200
|
+
| `task_transfer` | SFTP upload/download/`server_to_server` |
|
|
201
|
+
| `task_transfer_multi` | Multi + globs |
|
|
63
202
|
|
|
64
|
-
###
|
|
203
|
+
### Files / Diff / Shell / Snapshots
|
|
204
|
+
| Famille | Outils |
|
|
205
|
+
|---------|--------|
|
|
206
|
+
| Files | `file_read`, `file_write`, `file_edit` |
|
|
207
|
+
| Diff | `diff_files`, `diff_folders`, `compare_all_sources` |
|
|
208
|
+
| Shell | `shell_create`, `shell_exec` (+ `skip_policy`), `shell_list`, `shell_close` |
|
|
209
|
+
| Snapshots | `snapshot_create/list/diff/restore/delete` |
|
|
65
210
|
|
|
66
|
-
|
|
67
|
-
- `get_services_status`: Récupère le statut des services (systemd, Docker, PM2).
|
|
68
|
-
- `get_fail2ban_status`: Récupère les informations du service Fail2Ban.
|
|
211
|
+
**Édition safe** : `file_read` → hash → `file_edit` + `expectedHash` (+ `dryRun` / `backup`).
|
|
69
212
|
|
|
70
|
-
###
|
|
213
|
+
### Notes serveur
|
|
214
|
+
| Outil | Description |
|
|
215
|
+
|-------|-------------|
|
|
216
|
+
| `server_note_set/get/list/remove` | Protocole (description, services, warnings, intervention) |
|
|
71
217
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
218
|
+
### Monitoring & logs
|
|
219
|
+
| Outil | Description |
|
|
220
|
+
|-------|-------------|
|
|
221
|
+
| `get_system_resources` | CPU / RAM / disque |
|
|
222
|
+
| `get_services_status` | systemd / Docker / PM2 |
|
|
223
|
+
| `get_fail2ban_status` | Fail2Ban |
|
|
224
|
+
| `check_api_health` | HTTP via SSH+curl |
|
|
225
|
+
| `get_pm2_logs` / `get_docker_logs` / `tail_file` | Logs |
|
|
75
226
|
|
|
76
|
-
###
|
|
227
|
+
### Queue
|
|
228
|
+
| Outil | Description |
|
|
229
|
+
|-------|-------------|
|
|
230
|
+
| `task_queue` / `task_status` / `task_history` / `task_wait` / `task_logs` | Suivi |
|
|
231
|
+
| `task_retry` / `task_retry_all` | Relance |
|
|
232
|
+
| `task_purge` | Purge (dry_run défaut) |
|
|
233
|
+
| `queue_stats` / `pool_stats` | Stats |
|
|
77
234
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
235
|
+
### Tmux & tunnels
|
|
236
|
+
| Outil | Description |
|
|
237
|
+
|-------|-------------|
|
|
238
|
+
| `tmux_create/exec/read/list/kill` | Sessions tmux distantes |
|
|
239
|
+
| `tunnel_create/list/close` | Tunnels SSH local/remote/socks |
|
|
240
|
+
| `tunnel_allowlist_add/remove` | Ports autorisés pour tunnels |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 📖 Workflows agent recommandés
|
|
245
|
+
|
|
246
|
+
### Début de session
|
|
247
|
+
```
|
|
248
|
+
infra_audit (ou infra_overview)
|
|
249
|
+
fleet_status
|
|
250
|
+
project_list / project_resolve
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Chantier sur un projet
|
|
254
|
+
```
|
|
255
|
+
work_start { project: "p-image", alias: "fkomprodmini2_prod", tag: "fix-x", message: "…" }
|
|
256
|
+
file_read → file_edit (expectedHash, dryRun puis apply)
|
|
257
|
+
work_log { type: "file_edit", path: "…" }
|
|
258
|
+
work_end { summary: "…" } → note serveur mise à jour
|
|
259
|
+
project_diff { name: "p-image" }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Commandes longues
|
|
263
|
+
```
|
|
264
|
+
task_exec { timeout: 0, … } → si > syncTimeout → task_wait { id }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Cibles multi-serveurs
|
|
268
|
+
```
|
|
269
|
+
task_exec { alias: "group:oci", cmd: "hostname" }
|
|
270
|
+
task_exec { alias: "all", cmd: "uptime" }
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 🏗️ Architecture
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
Client MCP (stdio)
|
|
279
|
+
│
|
|
280
|
+
server.js ─── 82 tools
|
|
281
|
+
│
|
|
282
|
+
├── queue.js File d’attente persistante + purge/retry
|
|
283
|
+
├── ssh.js / sshPool Exécution + pool (retry safe, port configurable)
|
|
284
|
+
├── sftp.js Transferts (server_to_server via sourceAdapter/pool)
|
|
285
|
+
├── sourceAdapter.js Local fs | remote SFTP pool
|
|
286
|
+
├── fileOps.js Read/write/edit + hash + dryRun + backup
|
|
287
|
+
├── diffEngine.js / compareEngine.js / diffFormatter.js
|
|
288
|
+
├── shellSessions.js PTY persistants + policy
|
|
289
|
+
├── snapshotManager.js
|
|
290
|
+
├── projects.js / workSession.js / inventory.js / groups.js / fleet.js
|
|
291
|
+
├── sshTrust.js authorized_keys (pubkey only)
|
|
292
|
+
├── servers.js / apis.js / notes.js / policies.js / tunnels.js
|
|
293
|
+
├── history.js / guide.js / config.js / utils.js
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Cycle de vie d’un job
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
pending → running → completed | failed | partial
|
|
300
|
+
↓ (redémarrage MCP pendant running)
|
|
301
|
+
crashed → task_retry → pending
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## 🔒 Sécurité
|
|
307
|
+
|
|
308
|
+
| Mécanisme | Détail |
|
|
309
|
+
|-----------|--------|
|
|
310
|
+
| Secrets | Masqués en `api_list` / diagnostics (`***` + 4 derniers car.) |
|
|
311
|
+
| Shell escape | `escapeShellArg` sur curl, logs, chemins |
|
|
312
|
+
| Blocklist | `policies.json` ; shell + sequence inclus ; `skip_policy` pour forcer |
|
|
313
|
+
| RO global | `MCP_READONLY=1` |
|
|
314
|
+
| RO alias | `"readonly": true` dans `servers.json` |
|
|
315
|
+
| Destructif | `task_exec` dry-run si pattern dangereux sans `force:true` |
|
|
316
|
+
| Trust | Pubkey only ; dry_run par défaut |
|
|
317
|
+
| Clés | Préférer `keyPath` SSH ; Vaultwarden pour secrets API |
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 🧪 Tests
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
npm test:unit # p0 + p1 + p16 (43 tests)
|
|
325
|
+
npm test # unit + smoke MCP + features
|
|
326
|
+
node diagnose.js # diagnostic local optionnel
|
|
327
|
+
```
|
|
83
328
|
|
|
84
|
-
|
|
329
|
+
| Fichier | Couverture |
|
|
330
|
+
|---------|------------|
|
|
331
|
+
| `test_p0_unit.js` | utils, policies, redact, timeouts, version |
|
|
332
|
+
| `test_p1_unit.js` | groups, purge, destructive, RO env |
|
|
333
|
+
| `test_p16_unit.js` | projects, work session, compact, sshTrust |
|
|
334
|
+
| `test_mcp.js` | smoke SDK |
|
|
335
|
+
| `test_features.js` | queue / pool / globs / prompts |
|
|
85
336
|
|
|
86
|
-
|
|
87
|
-
- `api_list`: Affiche toutes les APIs configurées.
|
|
88
|
-
- `api_remove`: Supprime une API du catalogue.
|
|
89
|
-
- `api_check`: Lance un test de santé sur une API.
|
|
337
|
+
---
|
|
90
338
|
|
|
91
|
-
|
|
339
|
+
## 🛣️ Versions récentes
|
|
92
340
|
|
|
93
|
-
|
|
94
|
-
|
|
341
|
+
| Version | Contenu | Snapshot gencodedoc |
|
|
342
|
+
|---------|---------|---------------------|
|
|
343
|
+
| **11.6.1** | Hardening multi-agent: RO transversal, server-to-server dossiers/force, allowed roots anti-symlink, quoting shell/tmux, queue + JSON stores atomiques | — |
|
|
344
|
+
| **11.6.0** | Projets, work sessions, inventory, ssh_authorize_key, RO alias, compact | **#23** (final docs) |
|
|
345
|
+
| 11.4.0 | fleet, infra_audit, groups, retry_all, purge, pool rewrite | #21 |
|
|
346
|
+
| 11.3.0 | Secrets mask, policy shell/seq, port SSH, wait partial | #20 |
|
|
347
|
+
| 10.4–10.0 | file ops, diff, shell, snapshots, notes, guide | #17–19 |
|
|
348
|
+
| 9.x / 8.x | SFTP force, timeouts, interactif, sécu de base | — |
|
|
95
349
|
|
|
96
|
-
|
|
350
|
+
Détail : **[CHANGELOG.md](./CHANGELOG.md)** · plans historiques : `ROADMAP.md`, `ROADMAP_EXTENDED.md`.
|
|
97
351
|
|
|
98
|
-
|
|
352
|
+
---
|
|
99
353
|
|
|
100
354
|
## 📄 Licence
|
|
101
355
|
|
|
102
|
-
|
|
356
|
+
MIT — Copyright (c) 2025-2026 Franck (fkom13)
|
package/apis.js
CHANGED
|
@@ -1,88 +1,44 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import config from './config.js';
|
|
3
|
+
import jsonStore from './atomicJsonStore.js';
|
|
4
4
|
|
|
5
5
|
const APIS_FILE = path.join(config.dataDir, 'apis.json');
|
|
6
|
+
const EMPTY = {};
|
|
6
7
|
|
|
7
|
-
let apis = {};
|
|
8
|
-
|
|
9
|
-
let isInitialized = false;
|
|
10
|
-
let initPromise = null;
|
|
11
|
-
|
|
12
|
-
// Charger les APIs au démarrage
|
|
13
8
|
async function loadApis() {
|
|
14
|
-
|
|
15
|
-
const data = await fs.readFile(APIS_FILE, 'utf-8');
|
|
16
|
-
apis = JSON.parse(data);
|
|
17
|
-
} catch (err) {
|
|
18
|
-
if (err.code === 'ENOENT') {
|
|
19
|
-
// Le fichier n'existe pas encore, c'est normal
|
|
20
|
-
apis = {};
|
|
21
|
-
} else {
|
|
22
|
-
console.error("Erreur lors du chargement de apis.json:", err);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function ensureInitialized() {
|
|
28
|
-
if (isInitialized) return;
|
|
29
|
-
if (!initPromise) {
|
|
30
|
-
initPromise = loadApis().then(() => { isInitialized = true; });
|
|
31
|
-
}
|
|
32
|
-
return initPromise;
|
|
9
|
+
return jsonStore.readJson(APIS_FILE, EMPTY);
|
|
33
10
|
}
|
|
34
11
|
|
|
35
|
-
// Sauvegarder les APIs
|
|
36
|
-
async function saveApis() {
|
|
37
|
-
await ensureInitialized();
|
|
38
|
-
try {
|
|
39
|
-
await fs.writeFile(APIS_FILE, JSON.stringify(apis, null, 2));
|
|
40
|
-
} catch (err) {
|
|
41
|
-
console.error("Erreur lors de la sauvegarde de apis.json:", err);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Ajouter ou mettre à jour une API
|
|
46
12
|
async function addApi(alias, apiConfig) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
13
|
+
if (!alias || typeof alias !== 'string') throw new Error("L'alias doit être une chaîne non vide.");
|
|
14
|
+
if (!apiConfig.url) throw new Error("L'URL de l'API est obligatoire.");
|
|
15
|
+
try { new URL(apiConfig.url); } catch { throw new Error(`URL invalide: ${apiConfig.url}`); }
|
|
16
|
+
|
|
17
|
+
await jsonStore.updateJson(APIS_FILE, EMPTY, (apis) => {
|
|
18
|
+
apis[alias] = apiConfig;
|
|
19
|
+
return apis;
|
|
20
|
+
});
|
|
50
21
|
return { success: true, message: `API '${alias}' ajoutée/mise à jour avec succès.` };
|
|
51
22
|
}
|
|
52
23
|
|
|
53
|
-
// Lister toutes les APIs
|
|
54
24
|
async function listApis() {
|
|
55
|
-
|
|
56
|
-
return apis;
|
|
25
|
+
return loadApis();
|
|
57
26
|
}
|
|
58
27
|
|
|
59
|
-
// Obtenir une API par son alias
|
|
60
28
|
async function getApi(alias) {
|
|
61
|
-
await
|
|
29
|
+
const apis = await loadApis();
|
|
62
30
|
const apiConfig = apis[alias];
|
|
63
|
-
if (!apiConfig) {
|
|
64
|
-
throw new Error(`L'alias d'API '${alias}' est inconnu.`);
|
|
65
|
-
}
|
|
31
|
+
if (!apiConfig) throw new Error(`L'alias d'API '${alias}' est inconnu.`);
|
|
66
32
|
return apiConfig;
|
|
67
33
|
}
|
|
68
34
|
|
|
69
|
-
// Supprimer une API
|
|
70
35
|
async function removeApi(alias) {
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
await saveApis();
|
|
36
|
+
await jsonStore.updateJson(APIS_FILE, EMPTY, (apis) => {
|
|
37
|
+
if (!apis[alias]) throw new Error(`L'alias d'API '${alias}' est inconnu.`);
|
|
38
|
+
delete apis[alias];
|
|
39
|
+
return apis;
|
|
40
|
+
});
|
|
77
41
|
return { success: true, message: `API '${alias}' supprimée avec succès.` };
|
|
78
42
|
}
|
|
79
43
|
|
|
80
|
-
|
|
81
|
-
ensureInitialized();
|
|
82
|
-
|
|
83
|
-
export default {
|
|
84
|
-
addApi,
|
|
85
|
-
listApis,
|
|
86
|
-
getApi,
|
|
87
|
-
removeApi
|
|
88
|
-
};
|
|
44
|
+
export default { addApi, listApis, getApi, removeApi };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AtomicJsonStore — primitive commune pour les petits registres JSON du MCP.
|
|
7
|
+
*
|
|
8
|
+
* Garanties intra-processus :
|
|
9
|
+
* - update(file) sérialise read -> mutate -> write par fichier ;
|
|
10
|
+
* - écriture via temp unique + fsync + rename atomique ;
|
|
11
|
+
* - backup .bak du dernier fichier valide avant remplacement ;
|
|
12
|
+
* - permissions du temp forcées à 0600 (les registres peuvent contenir des secrets) ;
|
|
13
|
+
* - un JSON corrompu n'est plus silencieusement remplacé par {} : tentative .bak,
|
|
14
|
+
* puis erreur explicite si aucune copie valide n'existe.
|
|
15
|
+
*
|
|
16
|
+
* Ce verrou protège les appels concurrents du même process MCP. Pour le SaaS
|
|
17
|
+
* multi-instance, la persistance devra migrer vers DB/transaction ou un lock distribué.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const tails = new Map();
|
|
21
|
+
|
|
22
|
+
function clone(value) {
|
|
23
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function withLock(file, fn) {
|
|
27
|
+
const key = path.resolve(file);
|
|
28
|
+
const prev = tails.get(key) || Promise.resolve();
|
|
29
|
+
let release;
|
|
30
|
+
const gate = new Promise(resolve => { release = resolve; });
|
|
31
|
+
const tail = prev.catch(() => {}).then(() => gate);
|
|
32
|
+
tails.set(key, tail);
|
|
33
|
+
|
|
34
|
+
await prev.catch(() => {});
|
|
35
|
+
try {
|
|
36
|
+
return await fn();
|
|
37
|
+
} finally {
|
|
38
|
+
release();
|
|
39
|
+
if (tails.get(key) === tail) tails.delete(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function parseFile(file) {
|
|
44
|
+
const text = await fs.readFile(file, 'utf8');
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function readJson(file, fallback, { createIfMissing = false } = {}) {
|
|
49
|
+
try {
|
|
50
|
+
return await parseFile(file);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
if (err?.code === 'ENOENT') {
|
|
53
|
+
const value = clone(fallback);
|
|
54
|
+
if (createIfMissing) await writeJsonAtomic(file, value, { backup: false });
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parse/I/O error : récupérer la dernière copie saine si possible.
|
|
59
|
+
try {
|
|
60
|
+
return await parseFile(`${file}.bak`);
|
|
61
|
+
} catch {
|
|
62
|
+
const wrapped = new Error(`JSON store illisible: ${file} (${err.message})`);
|
|
63
|
+
wrapped.code = 'JSON_STORE_CORRUPT';
|
|
64
|
+
wrapped.cause = err;
|
|
65
|
+
throw wrapped;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function writeJsonAtomic(file, value, { backup = true } = {}) {
|
|
71
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
72
|
+
|
|
73
|
+
if (backup) {
|
|
74
|
+
try { await fs.copyFile(file, `${file}.bak`); } catch (e) {
|
|
75
|
+
if (e?.code !== 'ENOENT') throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
80
|
+
let handle = null;
|
|
81
|
+
try {
|
|
82
|
+
handle = await fs.open(tmp, 'w', 0o600);
|
|
83
|
+
await handle.writeFile(JSON.stringify(value, null, 2));
|
|
84
|
+
await handle.sync();
|
|
85
|
+
await handle.close();
|
|
86
|
+
handle = null;
|
|
87
|
+
await fs.rename(tmp, file);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
if (handle) await handle.close().catch(() => {});
|
|
90
|
+
await fs.rm(tmp, { force: true }).catch(() => {});
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function ensureJson(file, fallback) {
|
|
96
|
+
return withLock(file, async () => readJson(file, fallback, { createIfMissing: true }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function updateJson(file, fallback, mutator, options = {}) {
|
|
100
|
+
return withLock(file, async () => {
|
|
101
|
+
const current = await readJson(file, fallback, { createIfMissing: false });
|
|
102
|
+
const next = await mutator(current);
|
|
103
|
+
const value = next === undefined ? current : next;
|
|
104
|
+
await writeJsonAtomic(file, value, options);
|
|
105
|
+
return value;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
withLock,
|
|
111
|
+
readJson,
|
|
112
|
+
writeJsonAtomic,
|
|
113
|
+
ensureJson,
|
|
114
|
+
updateJson
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export { withLock, readJson, writeJsonAtomic, ensureJson, updateJson };
|