@biznetgio/neo-app 0.4.36 → 0.4.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/mcp/tools.js +77 -43
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { resolve, basename } from 'node:path';
|
|
6
6
|
import { statSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { createHash } from 'node:crypto';
|
|
8
7
|
import { deploySource } from '../deploySource.js';
|
|
9
8
|
import { pkgVersion } from '../util.js';
|
|
10
9
|
import { estimateCost } from '../pricing.js';
|
|
@@ -44,8 +43,8 @@ function requireEnv(env) {
|
|
|
44
43
|
// actually converges (GET → 404), so a delete isn't reported done while the record
|
|
45
44
|
// still exists. Returns a durable operation reference (`del-<id>`) + convergence
|
|
46
45
|
// state (handoff 3.6; 0.4.34 retest §9 "every delete returns a durable operation ID").
|
|
47
|
-
async function deleteAndConverge(api, path, resourceId, kind) {
|
|
48
|
-
const { status, data } = await api.del(path);
|
|
46
|
+
async function deleteAndConverge(api, path, resourceId, kind, delQuery = '') {
|
|
47
|
+
const { status, data } = await api.del(path + delQuery);
|
|
49
48
|
let converged = false, checks = 0;
|
|
50
49
|
for (let i = 0; i < 12; i++) {
|
|
51
50
|
checks++;
|
|
@@ -67,15 +66,6 @@ function gbFromSize(v) {
|
|
|
67
66
|
return m ? parseInt(m[1], 10) : undefined;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
// deletionHashOf is an immutable approval token for a project deletion: a short hash
|
|
71
|
-
// of the exact inventory being deleted. The preview returns it; a cascade delete must
|
|
72
|
-
// echo it back, and the tool recomputes the live inventory and refuses a mismatch — so
|
|
73
|
-
// a project can't be deleted against a stale/partial inventory (0.4.31 retest §7).
|
|
74
|
-
function deletionHashOf(pid, inv, total) {
|
|
75
|
-
const names = Object.entries(inv).flatMap(([k, a]) => a.map((n) => k + ':' + n)).sort();
|
|
76
|
-
return createHash('sha256').update(JSON.stringify({ pid, total, names })).digest('hex').slice(0, 16);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
69
|
// confirmCost is the Approve & Create gate for billable one-shot creates: unless
|
|
80
70
|
// args.confirm===true it returns the live-priced cost and creates NOTHING — the
|
|
81
71
|
// agent shows the user, then re-calls with confirm:true. plan/apply remains the
|
|
@@ -214,6 +204,33 @@ export const TOOLS = [
|
|
|
214
204
|
return (await api.get(`/api/v1/projects/${args.projectId}`)).data;
|
|
215
205
|
},
|
|
216
206
|
},
|
|
207
|
+
{
|
|
208
|
+
name: 'neo_get_confirmation',
|
|
209
|
+
description: 'Read a confirmation record (the server-side approval for a high-risk action) by id — its action, status, redacted summary, whether an exact-name phrase is required, and expiry. Read-only; never contains secrets.',
|
|
210
|
+
mutating: false,
|
|
211
|
+
schema: { type: 'object', properties: { confirmationId: str('Confirmation id (from a destructive preview or a plan)') }, required: ['confirmationId'] },
|
|
212
|
+
async call(api, args) {
|
|
213
|
+
return (await api.get(`/api/v1/confirmations/${args.confirmationId}`)).data;
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'neo_confirm_action',
|
|
218
|
+
description: 'Record a human DECISION on a confirmation (NEVER creates or deletes anything itself) — the server-enforced approval step for a high-risk action (project/service deletion, billable create). decision is approve | decline | cancel. Approving a DELETION requires confirmationPhrase to exactly equal the target name (the human must type it). After approve, call the action tool again with this confirmationId to actually perform it (single-use). Show the human the summary from the preview and ask once; never infer consent.',
|
|
219
|
+
mutating: true,
|
|
220
|
+
schema: {
|
|
221
|
+
type: 'object',
|
|
222
|
+
properties: {
|
|
223
|
+
confirmationId: str('Confirmation id (from a destructive preview or a plan)'),
|
|
224
|
+
decision: { type: 'string', enum: ['approve', 'decline', 'cancel'], description: 'approve | decline | cancel' },
|
|
225
|
+
confirmationPhrase: str('For a deletion: the EXACT target name, typed by the human, required to approve'),
|
|
226
|
+
},
|
|
227
|
+
required: ['confirmationId', 'decision'],
|
|
228
|
+
},
|
|
229
|
+
async call(api, args) {
|
|
230
|
+
const { status, data } = await api.post(`/api/v1/confirmations/${args.confirmationId}/decide`, { decision: args.decision, confirmationPhrase: args.confirmationPhrase });
|
|
231
|
+
return result(status, data);
|
|
232
|
+
},
|
|
233
|
+
},
|
|
217
234
|
{
|
|
218
235
|
name: 'neo_list_services',
|
|
219
236
|
description: 'List services/apps in a project.',
|
|
@@ -1286,12 +1303,34 @@ export const TOOLS = [
|
|
|
1286
1303
|
// the server hard-deletes the record and tears down the running workload. ---
|
|
1287
1304
|
{
|
|
1288
1305
|
name: 'neo_delete_service',
|
|
1289
|
-
description: '
|
|
1306
|
+
description: 'Delete a service (web / worker / static_site) and tear down its workload. SAFE BY DEFAULT: with no confirmationId it returns the service details + a server-side CONFIRMATION, deleting NOTHING. Then: show the user, call neo_confirm_action(confirmationId, "approve", confirmationPhrase=<EXACT service name>), then re-call with that confirmationId. Server-enforced, single-use, exact-name. Destructive and irreversible.',
|
|
1290
1307
|
mutating: true,
|
|
1291
1308
|
destructive: true,
|
|
1292
|
-
schema: {
|
|
1309
|
+
schema: {
|
|
1310
|
+
type: 'object',
|
|
1311
|
+
properties: {
|
|
1312
|
+
appId: str('Service/app id (from neo_list_services)'),
|
|
1313
|
+
confirmationId: str('An APPROVED confirmation id (from neo_confirm_action). Omit to get the confirmation.'),
|
|
1314
|
+
},
|
|
1315
|
+
required: ['appId'],
|
|
1316
|
+
},
|
|
1293
1317
|
async call(api, args) {
|
|
1294
|
-
|
|
1318
|
+
if (args.confirmationId) {
|
|
1319
|
+
return deleteAndConverge(api, `/api/v1/apps/${args.appId}`, args.appId, 'service', `?confirmationId=${encodeURIComponent(args.confirmationId)}`);
|
|
1320
|
+
}
|
|
1321
|
+
let svc = null;
|
|
1322
|
+
try { svc = (await api.get(`/api/v1/apps/${args.appId}`)).data; } catch { /* preview best-effort */ }
|
|
1323
|
+
let c;
|
|
1324
|
+
try { c = (await api.post('/api/v1/confirmations', { action: 'service_delete', targetId: args.appId })).data; }
|
|
1325
|
+
catch (e) { return { deleted: false, error: 'confirmation_failed', message: 'Could not create a deletion confirmation: ' + (e.message || e) }; }
|
|
1326
|
+
const phrase = (c.summary && c.summary.phraseRequired) || (svc && svc.name);
|
|
1327
|
+
return {
|
|
1328
|
+
deleted: false, requiresConfirmation: true,
|
|
1329
|
+
service: svc ? { id: svc.id, name: svc.name, type: svc.serviceType, environment: svc.environment } : { id: args.appId },
|
|
1330
|
+
warning: 'IRREVERSIBLE: deletes the service and tears down its running workload.',
|
|
1331
|
+
confirmationId: c.confirmationId, phraseRequired: phrase,
|
|
1332
|
+
message: `To delete: call neo_confirm_action("${c.confirmationId}", "approve", confirmationPhrase="${phrase}") — the human must type the exact service name — then re-call neo_delete_service with confirmationId="${c.confirmationId}".`,
|
|
1333
|
+
};
|
|
1295
1334
|
},
|
|
1296
1335
|
},
|
|
1297
1336
|
{
|
|
@@ -1390,21 +1429,24 @@ export const TOOLS = [
|
|
|
1390
1429
|
},
|
|
1391
1430
|
{
|
|
1392
1431
|
name: 'neo_delete_project',
|
|
1393
|
-
description: 'Delete a project and ALL its resources (apps, databases, queues, crons, buckets, namespace + PVCs/CNPG). SAFE BY DEFAULT:
|
|
1432
|
+
description: 'Delete a project and ALL its resources (apps, databases, queues, crons, buckets, namespace + PVCs/CNPG). SAFE BY DEFAULT: with no confirmationId it returns a PREVIEW (name, org, full inventory, metering, no-refund) AND a server-side CONFIRMATION, deleting NOTHING. Then: show the user, call neo_confirm_action(confirmationId, "approve", confirmationPhrase=<EXACT project name>), then re-call this tool with that confirmationId. Server-enforced, single-use, exact-name. Destructive and irreversible. Only the creator or an org owner/admin may delete.',
|
|
1394
1433
|
mutating: true,
|
|
1395
1434
|
destructive: true,
|
|
1396
1435
|
schema: {
|
|
1397
1436
|
type: 'object',
|
|
1398
1437
|
properties: {
|
|
1399
1438
|
projectId: str('Project id'),
|
|
1400
|
-
|
|
1401
|
-
cascade: { type: 'boolean', description: 'Required true to delete a NON-EMPTY project — acknowledges every resource in the inventory is destroyed.' },
|
|
1402
|
-
deletionHash: str('For a non-empty project: the deletionHash from the preview. The tool refuses if the live inventory no longer matches it (immutable approval).'),
|
|
1439
|
+
confirmationId: str('An APPROVED confirmation id (from neo_confirm_action). Omit to get the preview + a fresh confirmation.'),
|
|
1403
1440
|
},
|
|
1404
1441
|
required: ['projectId'],
|
|
1405
1442
|
},
|
|
1406
1443
|
async call(api, args) {
|
|
1407
1444
|
const pid = args.projectId;
|
|
1445
|
+
// With an approved confirmation: the server consumes it atomically and deletes.
|
|
1446
|
+
if (args.confirmationId) {
|
|
1447
|
+
const { status, data } = await api.del(`/api/v1/projects/${pid}?confirmationId=${encodeURIComponent(args.confirmationId)}`);
|
|
1448
|
+
return { deleted: true, project: { id: pid }, result: result(status, data) };
|
|
1449
|
+
}
|
|
1408
1450
|
// Gather inventory + usage BEFORE any delete (this is also the metering snapshot).
|
|
1409
1451
|
const safeGet = async (p) => { try { return (await api.get(p)).data; } catch { return null; } };
|
|
1410
1452
|
const [proj, apps, dbs, queues, crons, buckets, usage] = await Promise.all([
|
|
@@ -1429,36 +1471,28 @@ export const TOOLS = [
|
|
|
1429
1471
|
// project is empty — treat it as non-empty so the empty-path can't silently
|
|
1430
1472
|
// delete hidden resources (the preview must be trustworthy).
|
|
1431
1473
|
const inventoryIncomplete = [apps, dbs, queues, crons, buckets].some((x) => x === null);
|
|
1432
|
-
|
|
1474
|
+
// Create a server-side, single-use, exact-name confirmation for this deletion.
|
|
1475
|
+
let confirmationId, phraseRequired = proj && proj.name;
|
|
1476
|
+
try {
|
|
1477
|
+
const c = (await api.post('/api/v1/confirmations', { action: 'project_delete', projectId: pid })).data;
|
|
1478
|
+
confirmationId = c.confirmationId;
|
|
1479
|
+
phraseRequired = (c.summary && c.summary.phraseRequired) || phraseRequired;
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
return { deleted: false, error: 'confirmation_failed', message: 'Could not create a deletion confirmation: ' + (e.message || e) };
|
|
1482
|
+
}
|
|
1483
|
+
return {
|
|
1484
|
+
deleted: false,
|
|
1485
|
+
requiresConfirmation: true,
|
|
1433
1486
|
project: proj ? { id: proj.id, name: proj.name, organizationId: proj.organizationId } : { id: pid },
|
|
1434
1487
|
resourceInventory: inv,
|
|
1435
1488
|
resourceCount: total,
|
|
1436
1489
|
meteringSnapshot: usage ? { period: usage.period, total: usage.total ?? usage.totalCost, currency: usage.currency } : null,
|
|
1437
1490
|
inventoryIncomplete: inventoryIncomplete || undefined,
|
|
1438
|
-
|
|
1439
|
-
|
|
1491
|
+
warning: 'IRREVERSIBLE: deletes the project, every resource above, and their data (database/queue volumes included). No refund for prepaid or accrued usage. The organization itself remains.',
|
|
1492
|
+
confirmationId,
|
|
1493
|
+
phraseRequired,
|
|
1494
|
+
message: `Show the user this inventory. To delete: call neo_confirm_action("${confirmationId}", "approve", confirmationPhrase="${phraseRequired}") — the human must type the exact project name — then re-call neo_delete_project with confirmationId="${confirmationId}".`,
|
|
1440
1495
|
};
|
|
1441
|
-
// Treat an unverifiable inventory as non-empty — never allow the no-cascade path
|
|
1442
|
-
// when we could not confirm the project is actually empty.
|
|
1443
|
-
const requireCascade = total > 0 || inventoryIncomplete;
|
|
1444
|
-
if (args.confirm !== true) {
|
|
1445
|
-
return { ...preview, deleted: false, requiresConfirmation: true, message: requireCascade ? 'Show this inventory to the user. To delete, re-call with confirm:true, cascade:true, and deletionHash.' : 'Empty project. To delete, re-call with confirm:true.' };
|
|
1446
|
-
}
|
|
1447
|
-
if (requireCascade && args.cascade !== true) {
|
|
1448
|
-
return { ...preview, deleted: false, error: 'cascade_required', message: inventoryIncomplete ? 'Could not fully verify the project inventory — re-call with confirm:true AND cascade:true to delete anyway.' : `This project has ${total} resource(s). Re-call with confirm:true AND cascade:true to delete them all.` };
|
|
1449
|
-
}
|
|
1450
|
-
// Immutable approval: a non-empty cascade delete must echo the preview's
|
|
1451
|
-
// deletionHash, and it must still match the live inventory.
|
|
1452
|
-
if (total > 0) {
|
|
1453
|
-
if (!args.deletionHash) {
|
|
1454
|
-
return { ...preview, deleted: false, error: 'deletion_hash_required', message: 'Pass deletionHash (from the preview) to confirm you are deleting exactly this inventory.' };
|
|
1455
|
-
}
|
|
1456
|
-
if (args.deletionHash !== preview.deletionHash) {
|
|
1457
|
-
return { ...preview, deleted: false, error: 'inventory_changed', message: 'The project inventory changed since your preview — re-preview and pass the new deletionHash.' };
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
const { status, data } = await api.del(`/api/v1/projects/${pid}`);
|
|
1461
|
-
return { deleted: true, project: preview.project, removed: inv, resourceCount: total, meteringSnapshot: preview.meteringSnapshot, result: result(status, data) };
|
|
1462
1496
|
},
|
|
1463
1497
|
},
|
|
1464
1498
|
];
|