@biznetgio/neo-app 0.4.42 → 0.4.44
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 +91 -26
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -87,6 +87,53 @@ async function confirmCost(api, args, spec) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// billableGate routes a billable one-shot create through the SAME server-side generic
|
|
91
|
+
// confirmation subsystem as deletes (handoff: generic confirmation for billable creates),
|
|
92
|
+
// so every create is approved + audited server-side and single-use. It returns either:
|
|
93
|
+
// { query } — append to the create POST; the server consumes the confirmation
|
|
94
|
+
// { requiresConfirmation } — a preview; CREATES NOTHING
|
|
95
|
+
// A confirmationId re-plays an already-approved confirmation; confirm:true is a one-call
|
|
96
|
+
// shortcut that mints + auto-approves one; neither returns the cost + a pending confirmationId.
|
|
97
|
+
async function billableGate(api, { projectId, kind, name, confirmationId, confirm }, spec) {
|
|
98
|
+
if (confirmationId) return { query: `?confirmationId=${encodeURIComponent(confirmationId)}` };
|
|
99
|
+
|
|
100
|
+
// Estimate the cost (best-effort) for display and the confirmation summary.
|
|
101
|
+
let orgId = null;
|
|
102
|
+
try { orgId = await api.resolveOrgId(); } catch { orgId = null; }
|
|
103
|
+
let book = null;
|
|
104
|
+
try { book = (await api.get(orgId ? `/api/v1/orgs/${orgId}/pricing` : '/api/v1/pricing')).data; }
|
|
105
|
+
catch { try { book = (await api.get('/api/v1/pricing')).data; } catch { book = null; } }
|
|
106
|
+
let cost = null;
|
|
107
|
+
if (book) { try { cost = estimateCost(book, spec); } catch { cost = null; } }
|
|
108
|
+
const costNote = cost ? `~${cost.monthly} ${cost.currency}/mo (${cost.hourly}/h)` : '';
|
|
109
|
+
|
|
110
|
+
// Mint a server-side billable_create confirmation bound to this exact resource.
|
|
111
|
+
// Retry a transient 5xx/dropped connection: minting has no lasting side effect (an
|
|
112
|
+
// unused pending confirmation just expires), so this is safe and keeps a flaky gateway
|
|
113
|
+
// from failing an otherwise-fine create (same spirit as the api-layer idempotent retry).
|
|
114
|
+
let mint;
|
|
115
|
+
for (let attempt = 1; ; attempt++) {
|
|
116
|
+
try { mint = (await api.post('/api/v1/confirmations', { action: 'billable_create', projectId, kind, name, estimatedCost: costNote })).data; break; }
|
|
117
|
+
catch (e) {
|
|
118
|
+
if (attempt < 3 && (e?.status >= 500 || e?.status === 0)) { await new Promise((r) => setTimeout(r, 300 * attempt)); continue; }
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const id = mint.confirmationId;
|
|
123
|
+
if (confirm === true) {
|
|
124
|
+
// One-call shortcut: approve the just-minted confirmation, then let the create consume it.
|
|
125
|
+
await api.post(`/api/v1/confirmations/${id}/decide`, { decision: 'approve' });
|
|
126
|
+
return { query: `?confirmationId=${encodeURIComponent(id)}` };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
requiresConfirmation: true,
|
|
130
|
+
cost,
|
|
131
|
+
priceBook: orgId ? `org:${orgId}` : 'global-defaults',
|
|
132
|
+
confirmationId: id,
|
|
133
|
+
message: `Billable ${kind} — show the user this cost, then approve server-side: neo_confirm_action("${id}", "approve"), and re-call this tool with confirmationId="${id}". (Shortcut: re-call with confirm:true.) Nothing was created.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
90
137
|
const SECRET_KEY = /(password|secret|secretkey|accesskey|connectionstring|connstr|dsn|token|privatekey|apikey|authorization)/i;
|
|
91
138
|
// Deployment snapshots (exact keys) embed rendered configs with plaintext creds — a
|
|
92
139
|
// queue's `--pass`, a DB's POSTGRES_PASSWORD. Redact by EXACT key so we don't also
|
|
@@ -163,6 +210,13 @@ export const TOOLS = [
|
|
|
163
210
|
environments: ['staging', 'production'],
|
|
164
211
|
pricing,
|
|
165
212
|
costConsent: { estimateAvailable: true, dryRunIncludesCost: true, planApplyEnforced: true, note: 'neo_plan_stack returns a costed immutable plan; neo_apply_stack is server-enforced (hash/expiry/one-shot) so a billable stack requires a reviewed plan (handoff 3.3-3.5)' },
|
|
213
|
+
// Project/organization lifecycle (issue #107 §3). Honest to THIS build: project
|
|
214
|
+
// deletion is zero-write-preview → exact-name server confirmation → cascade delete;
|
|
215
|
+
// a dedicated costed plan (neo_plan_project_deletion) is not yet exposed, so
|
|
216
|
+
// requiresPlan is false. Organization deletion is unsupported and out of scope —
|
|
217
|
+
// no org-delete tool exists, and deleting a project never deletes its organization.
|
|
218
|
+
project: { delete: true, cascadeDelete: true, requiresPlan: false, requiresConfirmation: true, note: 'neo_delete_project: zero-write preview → exact-name, single-use server confirmation → cascade delete of all child resources.' },
|
|
219
|
+
organization: { delete: false, note: 'Organization deletion is unsupported and out of scope; no organization-delete tool is exposed.' },
|
|
166
220
|
minClientVersion: pkgVersion(),
|
|
167
221
|
deprecations: [],
|
|
168
222
|
};
|
|
@@ -217,7 +271,7 @@ export const TOOLS = [
|
|
|
217
271
|
},
|
|
218
272
|
{
|
|
219
273
|
name: 'neo_confirm_action',
|
|
220
|
-
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.',
|
|
274
|
+
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 — never manufacture it). If the confirmation has ackRequired:true (a project with production resources), approving ALSO requires acknowledgeNoRefund:true. 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.',
|
|
221
275
|
mutating: true,
|
|
222
276
|
schema: {
|
|
223
277
|
type: 'object',
|
|
@@ -225,11 +279,12 @@ export const TOOLS = [
|
|
|
225
279
|
confirmationId: str('Confirmation id (from a destructive preview or a plan)'),
|
|
226
280
|
decision: { type: 'string', enum: ['approve', 'decline', 'cancel'], description: 'approve | decline | cancel' },
|
|
227
281
|
confirmationPhrase: str('For a deletion: the EXACT target name, typed by the human, required to approve'),
|
|
282
|
+
acknowledgeNoRefund: { type: 'boolean', description: 'Required to approve when the confirmation has ackRequired:true (production project delete): the human explicitly acknowledges there is no refund, credit, or proration.' },
|
|
228
283
|
},
|
|
229
284
|
required: ['confirmationId', 'decision'],
|
|
230
285
|
},
|
|
231
286
|
async call(api, args) {
|
|
232
|
-
const { status, data } = await api.post(`/api/v1/confirmations/${args.confirmationId}/decide`, { decision: args.decision, confirmationPhrase: args.confirmationPhrase });
|
|
287
|
+
const { status, data } = await api.post(`/api/v1/confirmations/${args.confirmationId}/decide`, { decision: args.decision, confirmationPhrase: args.confirmationPhrase, acknowledgeNoRefund: args.acknowledgeNoRefund === true });
|
|
233
288
|
return result(status, data);
|
|
234
289
|
},
|
|
235
290
|
},
|
|
@@ -698,22 +753,23 @@ export const TOOLS = [
|
|
|
698
753
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
699
754
|
replicas: int('Read/failover replicas for HA (optional; postgres/CNPG only)'),
|
|
700
755
|
deploymentMode: { type: 'string', description: 'single | ha (optional)' },
|
|
701
|
-
confirm: { type: 'boolean', description: 'Approve & Create:
|
|
756
|
+
confirm: { type: 'boolean', description: 'Approve & Create in one call: mints + auto-approves a server-side billable confirmation, then creates. Without confirm (or confirmationId) the tool returns the cost + a pending confirmationId and creates nothing.' },
|
|
757
|
+
confirmationId: str('An APPROVED billable_create confirmation id (from neo_confirm_action) — re-call with it to create. Alternative to confirm:true.'),
|
|
702
758
|
},
|
|
703
759
|
required: ['projectId', 'name', 'engine', 'environment'],
|
|
704
760
|
},
|
|
705
761
|
async call(api, args) {
|
|
706
762
|
requireEnv(args.environment);
|
|
707
|
-
const gate = await
|
|
708
|
-
if (gate) return gate;
|
|
709
|
-
const { projectId, confirm, orgId, ...body } = args;
|
|
710
|
-
const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases`, body);
|
|
763
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'database', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, { storageGB: gbFromSize(args.volumeSize), replicas: args.replicas });
|
|
764
|
+
if (gate.requiresConfirmation) return gate;
|
|
765
|
+
const { projectId, confirm, confirmationId, orgId, ...body } = args;
|
|
766
|
+
const { status, data } = await api.post(`/api/v1/projects/${projectId}/databases${gate.query}`, body);
|
|
711
767
|
return redactSecrets(result(status, data));
|
|
712
768
|
},
|
|
713
769
|
},
|
|
714
770
|
{
|
|
715
771
|
name: 'neo_deploy_database',
|
|
716
|
-
description: 'Deploy an individually-created managed database so it actually runs (provisions the cluster, sets its private host/port). Required after neo_create_database before the DB is reachable, bindable, or can have public access enabled.
|
|
772
|
+
description: 'Deploy an individually-created managed database so it actually runs (provisions the cluster, sets its private host/port). Required after neo_create_database before the DB is reachable, bindable, or can have public access enabled. For CNPG postgres this returns ready:false — the Cluster is created but initdb runs ~1-3 min and SQL is NOT reachable yet; the deployment status stays "running" and flips to "success" when a ready instance comes up. Do NOT treat the response as terminal: poll neo_verify_database until it passes (authoritative SQL round-trip) before connecting/binding. Credentials redacted.',
|
|
717
773
|
mutating: true,
|
|
718
774
|
schema: { type: 'object', properties: { databaseId: str('Database id (from neo_create_database)') }, required: ['databaseId'] },
|
|
719
775
|
async call(api, args) {
|
|
@@ -868,16 +924,17 @@ export const TOOLS = [
|
|
|
868
924
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
869
925
|
volumeSize: str('Storage size, e.g. "10Gi" (optional)'),
|
|
870
926
|
dlqEnabled: { type: 'boolean', description: 'Enable a dead-letter queue (optional, default true). Pass false to disable — this is now honored.' },
|
|
871
|
-
confirm: { type: 'boolean', description: 'Approve & Create:
|
|
927
|
+
confirm: { type: 'boolean', description: 'Approve & Create in one call: mints + auto-approves a server-side billable confirmation, then creates. Without confirm (or confirmationId) the tool returns the cost + a pending confirmationId and creates nothing.' },
|
|
928
|
+
confirmationId: str('An APPROVED billable_create confirmation id (from neo_confirm_action) — re-call with it to create. Alternative to confirm:true.'),
|
|
872
929
|
},
|
|
873
930
|
required: ['projectId', 'name', 'environment'],
|
|
874
931
|
},
|
|
875
932
|
async call(api, args) {
|
|
876
933
|
requireEnv(args.environment);
|
|
877
|
-
const gate = await
|
|
878
|
-
if (gate) return gate;
|
|
879
|
-
const { projectId, confirm, orgId, ...body } = args;
|
|
880
|
-
const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues`, body);
|
|
934
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'queue', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, { storageGB: gbFromSize(args.volumeSize) });
|
|
935
|
+
if (gate.requiresConfirmation) return gate;
|
|
936
|
+
const { projectId, confirm, confirmationId, orgId, ...body } = args;
|
|
937
|
+
const { status, data } = await api.post(`/api/v1/projects/${projectId}/queues${gate.query}`, body);
|
|
881
938
|
return redactSecrets(result(status, data));
|
|
882
939
|
},
|
|
883
940
|
},
|
|
@@ -1005,20 +1062,21 @@ export const TOOLS = [
|
|
|
1005
1062
|
retryCount: int('Retries on failure (optional) → backoffLimit'),
|
|
1006
1063
|
concurrencyPolicy: { type: 'string', enum: ['Allow', 'Forbid', 'Replace'], description: 'Overlap policy if a run is still going when the next is due (optional, default Allow)' },
|
|
1007
1064
|
environment: { type: 'string', enum: ['staging', 'production'], description: 'REQUIRED — staging or production (never defaulted, handoff §2)' },
|
|
1008
|
-
confirm: { type: 'boolean', description: 'Approve & Create
|
|
1065
|
+
confirm: { type: 'boolean', description: 'Approve & Create in one call: mints + auto-approves a server-side billable confirmation (crons are billed per run — variable), then creates. Without confirm (or confirmationId) the tool returns the cost + a pending confirmationId and creates nothing.' },
|
|
1066
|
+
confirmationId: str('An APPROVED billable_create confirmation id (from neo_confirm_action) — re-call with it to create. Alternative to confirm:true.'),
|
|
1009
1067
|
},
|
|
1010
1068
|
required: ['projectId', 'name', 'mode', 'scheduleType', 'environment'],
|
|
1011
1069
|
},
|
|
1012
1070
|
async call(api, args) {
|
|
1013
1071
|
requireEnv(args.environment);
|
|
1014
|
-
const gate = await
|
|
1015
|
-
if (gate) return gate;
|
|
1072
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'cron', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, {}); // cron cost is variable (per-run) — gate surfaces the rate
|
|
1073
|
+
if (gate.requiresConfirmation) return gate;
|
|
1016
1074
|
// Merge contentType into headers for convenience (unless already set).
|
|
1017
1075
|
if (args.contentType) {
|
|
1018
1076
|
args = { ...args, headers: { 'Content-Type': args.contentType, ...(args.headers || {}) } };
|
|
1019
1077
|
}
|
|
1020
|
-
const { projectId, confirm, orgId, contentType, ...body } = args;
|
|
1021
|
-
const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons`, body);
|
|
1078
|
+
const { projectId, confirm, confirmationId, orgId, contentType, ...body } = args;
|
|
1079
|
+
const { status, data } = await api.post(`/api/v1/projects/${projectId}/crons${gate.query}`, body);
|
|
1022
1080
|
return redactSecrets(result(status, data));
|
|
1023
1081
|
},
|
|
1024
1082
|
},
|
|
@@ -1071,15 +1129,16 @@ export const TOOLS = [
|
|
|
1071
1129
|
region: str('Region (optional; see neo_list_regions on the portal)'),
|
|
1072
1130
|
acl: { type: 'string', description: 'private | public-read (optional, default private)' },
|
|
1073
1131
|
quotaGB: int('Storage quota in GB (optional)'),
|
|
1074
|
-
confirm: { type: 'boolean', description: 'Approve & Create:
|
|
1132
|
+
confirm: { type: 'boolean', description: 'Approve & Create in one call: mints + auto-approves a server-side billable confirmation, then creates. Without confirm (or confirmationId) the tool returns the cost + a pending confirmationId and creates nothing.' },
|
|
1133
|
+
confirmationId: str('An APPROVED billable_create confirmation id (from neo_confirm_action) — re-call with it to create. Alternative to confirm:true.'),
|
|
1075
1134
|
},
|
|
1076
1135
|
required: ['projectId', 'name'],
|
|
1077
1136
|
},
|
|
1078
1137
|
async call(api, args) {
|
|
1079
|
-
const gate = await
|
|
1080
|
-
if (gate) return gate;
|
|
1081
|
-
const { projectId, confirm, orgId, ...body } = args;
|
|
1082
|
-
const { status, data } = await api.post(`/api/v1/projects/${projectId}/buckets`, body);
|
|
1138
|
+
const gate = await billableGate(api, { projectId: args.projectId, kind: 'bucket', name: args.name, confirmationId: args.confirmationId, confirm: args.confirm }, { objectStorageGB: gbFromSize(args.quotaGB) });
|
|
1139
|
+
if (gate.requiresConfirmation) return gate;
|
|
1140
|
+
const { projectId, confirm, confirmationId, orgId, ...body } = args;
|
|
1141
|
+
const { status, data } = await api.post(`/api/v1/projects/${projectId}/buckets${gate.query}`, body);
|
|
1083
1142
|
return redactSecrets(result(status, data));
|
|
1084
1143
|
},
|
|
1085
1144
|
},
|
|
@@ -1404,15 +1463,21 @@ export const TOOLS = [
|
|
|
1404
1463
|
if (args.confirmationId) {
|
|
1405
1464
|
return deleteAndConverge(api, `/api/v1/apps/${args.appId}`, args.appId, 'service', `?confirmationId=${encodeURIComponent(args.confirmationId)}`);
|
|
1406
1465
|
}
|
|
1466
|
+
// GET /apps/{id} returns { application: {...} }, so unwrap it — reading .data directly
|
|
1467
|
+
// left every field undefined and the preview showed service:{} (retest B).
|
|
1407
1468
|
let svc = null;
|
|
1408
|
-
try {
|
|
1469
|
+
try { const d = (await api.get(`/api/v1/apps/${args.appId}`)).data; svc = (d && d.application) || d; } catch { /* preview best-effort */ }
|
|
1409
1470
|
let c;
|
|
1410
1471
|
try { c = (await api.post('/api/v1/confirmations', { action: 'service_delete', targetId: args.appId })).data; }
|
|
1411
1472
|
catch (e) { return { deleted: false, error: 'confirmation_failed', message: 'Could not create a deletion confirmation: ' + (e.message || e) }; }
|
|
1412
|
-
|
|
1473
|
+
// Prefer the server confirmation's authoritative identity (what neo_get_confirmation shows);
|
|
1474
|
+
// fall back to the unwrapped GET, then to just the id.
|
|
1475
|
+
const svcView = (c.summary && c.summary.service) ||
|
|
1476
|
+
(svc ? { id: svc.id, name: svc.name, type: svc.serviceType, environment: svc.environment } : { id: args.appId });
|
|
1477
|
+
const phrase = (c.summary && c.summary.phraseRequired) || svcView.name;
|
|
1413
1478
|
return {
|
|
1414
1479
|
deleted: false, requiresConfirmation: true,
|
|
1415
|
-
service:
|
|
1480
|
+
service: svcView,
|
|
1416
1481
|
warning: 'IRREVERSIBLE: deletes the service and tears down its running workload.',
|
|
1417
1482
|
confirmationId: c.confirmationId, phraseRequired: phrase,
|
|
1418
1483
|
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}".`,
|