@biznetgio/neo-app 0.4.63 → 0.4.64
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 +46 -5
package/package.json
CHANGED
package/src/mcp/tools.js
CHANGED
|
@@ -68,6 +68,19 @@ const allowCrossEnvProp = { allowCrossEnvironment: { type: 'boolean', descriptio
|
|
|
68
68
|
// invariant in sequence (each must start from a clean, unpinned session).
|
|
69
69
|
export function resetSessionEnv() { sessionEnv = null; }
|
|
70
70
|
|
|
71
|
+
// assertResourceEnv enforces the session pin on an EXISTING resource addressed by id (JNG #136 §4):
|
|
72
|
+
// if the session is pinned and the resource lives in a different environment, refuse so an
|
|
73
|
+
// ID-based op can't touch the wrong environment's resource. allowCrossEnvironment overrides it;
|
|
74
|
+
// resources with no environment (buckets are shared) and an unpinned session are not guarded.
|
|
75
|
+
async function assertResourceEnv(api, getPath, allowCross = false) {
|
|
76
|
+
if (!sessionEnv || allowCross) return;
|
|
77
|
+
let env;
|
|
78
|
+
try { env = (await api.get(getPath)).data?.environment; } catch { return; } // can't resolve → don't block
|
|
79
|
+
if (env && env !== sessionEnv) {
|
|
80
|
+
throw new Error(`environment_mismatch: this session is pinned to "${sessionEnv}" but that resource is in "${env}". Staging and production must not be mixed (JNG #136). Pass allowCrossEnvironment:true to act on it anyway, or re-pin with neo_set_context.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
// deleteAndConverge deletes a resource, then polls its endpoint until the inventory
|
|
72
85
|
// actually converges (GET → 404), so a delete isn't reported done while the record
|
|
73
86
|
// still exists. Returns a durable operation reference (`del-<id>`) + convergence
|
|
@@ -443,11 +456,13 @@ export const TOOLS = [
|
|
|
443
456
|
healthTimeoutSeconds: int('Health-check timeout (s)'),
|
|
444
457
|
healthGraceSeconds: int('Health-check grace period (s)'),
|
|
445
458
|
confirm: { type: 'boolean', description: 'Approve a cost change (compute/replicas/autoscale/volume): without it the new cost is returned and nothing is applied.' },
|
|
459
|
+
...allowCrossEnvProp,
|
|
446
460
|
},
|
|
447
461
|
required: ['appId'],
|
|
448
462
|
},
|
|
449
463
|
async call(api, args) {
|
|
450
|
-
const { appId, confirm, orgId, computeTier, ...rest } = args;
|
|
464
|
+
const { appId, confirm, orgId, computeTier, allowCrossEnvironment, ...rest } = args;
|
|
465
|
+
await assertResourceEnv(api, `/api/v1/apps/${appId}`, allowCrossEnvironment); // JNG #136 §4
|
|
451
466
|
const body = { ...rest };
|
|
452
467
|
if (computeTier) { body.billingMode = 'bundled'; body.cpuLimit = computeTier; }
|
|
453
468
|
else if (args.cpuLimit || args.memoryLimit) { body.billingMode = 'unbundled'; }
|
|
@@ -595,8 +610,9 @@ export const TOOLS = [
|
|
|
595
610
|
description:
|
|
596
611
|
'Trigger a deployment of an app (service). Returns a deployment id — pass it to neo_wait_deployment to block until it finishes (don\'t sleep/guess), or a pending_approval result when org policy gates it. Deploy to staging first.',
|
|
597
612
|
mutating: true,
|
|
598
|
-
schema: { type: 'object', properties: { appId: str('App/service id') }, required: ['appId'] },
|
|
613
|
+
schema: { type: 'object', properties: { appId: str('App/service id'), ...allowCrossEnvProp }, required: ['appId'] },
|
|
599
614
|
async call(api, args) {
|
|
615
|
+
await assertResourceEnv(api, `/api/v1/apps/${args.appId}`, args.allowCrossEnvironment); // JNG #136 §4
|
|
600
616
|
const { status, data } = await api.post(`/api/v1/apps/${args.appId}/deploy`);
|
|
601
617
|
return result(status, data);
|
|
602
618
|
},
|
|
@@ -679,8 +695,9 @@ export const TOOLS = [
|
|
|
679
695
|
name: 'neo_restart_service',
|
|
680
696
|
description: 'Rolling-restart a service with its CURRENT image + config — no rebuild (JNG #128). Use after a config-only change (neo_set_service_variable, neo_update_service) that doesn\'t need a new build, instead of a full neo_deploy. Returns immediately; poll neo_list_pods for the new pods.',
|
|
681
697
|
mutating: true,
|
|
682
|
-
schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)') }, required: ['appId'] },
|
|
698
|
+
schema: { type: 'object', properties: { appId: str('Service/app id (from neo_list_services)'), ...allowCrossEnvProp }, required: ['appId'] },
|
|
683
699
|
async call(api, args) {
|
|
700
|
+
await assertResourceEnv(api, `/api/v1/apps/${args.appId}`, args.allowCrossEnvironment); // JNG #136 §4
|
|
684
701
|
return (await api.post(`/api/v1/apps/${args.appId}/restart`, {})).data;
|
|
685
702
|
},
|
|
686
703
|
},
|
|
@@ -695,11 +712,13 @@ export const TOOLS = [
|
|
|
695
712
|
command: str('Shell command to run (executed via /bin/sh -c)'),
|
|
696
713
|
pod: str('Specific pod name (optional; from neo_list_pods) — else the first running pod'),
|
|
697
714
|
timeoutSeconds: int('Max seconds to wait (default 30, max 120)'),
|
|
715
|
+
...allowCrossEnvProp,
|
|
698
716
|
},
|
|
699
717
|
required: ['appId', 'command'],
|
|
700
718
|
},
|
|
701
719
|
async call(api, args) {
|
|
702
|
-
const { appId, ...body } = args;
|
|
720
|
+
const { appId, allowCrossEnvironment, ...body } = args;
|
|
721
|
+
await assertResourceEnv(api, `/api/v1/apps/${appId}`, allowCrossEnvironment); // JNG #136 §4
|
|
703
722
|
return (await api.post(`/api/v1/apps/${appId}/exec`, body)).data;
|
|
704
723
|
},
|
|
705
724
|
},
|
|
@@ -722,10 +741,12 @@ export const TOOLS = [
|
|
|
722
741
|
databaseId: str('Database id (postgres, deployed)'),
|
|
723
742
|
sql: str('SQL to run (piped into psql)'),
|
|
724
743
|
readOnly: { type: 'boolean', description: 'true (default) runs in a read-only session; false allows writes' },
|
|
744
|
+
...allowCrossEnvProp,
|
|
725
745
|
},
|
|
726
746
|
required: ['databaseId', 'sql'],
|
|
727
747
|
},
|
|
728
748
|
async call(api, args) {
|
|
749
|
+
await assertResourceEnv(api, `/api/v1/databases/${args.databaseId}`, args.allowCrossEnvironment); // JNG #136 §4
|
|
729
750
|
return (await api.post(`/api/v1/databases/${args.databaseId}/query`, { sql: args.sql, readOnly: args.readOnly })).data;
|
|
730
751
|
},
|
|
731
752
|
},
|
|
@@ -772,10 +793,12 @@ export const TOOLS = [
|
|
|
772
793
|
key: str('Variable name'),
|
|
773
794
|
value: str('Variable value'),
|
|
774
795
|
secret: { type: 'boolean', description: 'Store as a secret (default false)' },
|
|
796
|
+
...allowCrossEnvProp,
|
|
775
797
|
},
|
|
776
798
|
required: ['appId', 'key', 'value'],
|
|
777
799
|
},
|
|
778
800
|
async call(api, args) {
|
|
801
|
+
await assertResourceEnv(api, `/api/v1/apps/${args.appId}`, args.allowCrossEnvironment); // JNG #136 §4
|
|
779
802
|
const { status, data } = await api.post(`/api/v1/apps/${args.appId}/variables`, { key: args.key, value: args.value, secret: !!args.secret });
|
|
780
803
|
return result(status, data); // {status:"set", key, redeployRequired} — no value returned
|
|
781
804
|
},
|
|
@@ -1541,11 +1564,29 @@ export const TOOLS = [
|
|
|
1541
1564
|
targetType: { type: 'string', enum: ['neo_db', 'neo_queue', 'neo_object_storage'], description: 'neo_db | neo_queue | neo_object_storage — the resource to attach' },
|
|
1542
1565
|
targetId: str('The database, queue, or bucket id to bind'),
|
|
1543
1566
|
envKey: str('Env var name / prefix (optional; default DATABASE_URL, NATS_URL, or S3 prefix)'),
|
|
1567
|
+
...allowCrossEnvProp,
|
|
1544
1568
|
},
|
|
1545
1569
|
required: ['appId', 'targetType', 'targetId'],
|
|
1546
1570
|
},
|
|
1547
1571
|
async call(api, args) {
|
|
1548
|
-
const { appId, ...body } = args;
|
|
1572
|
+
const { appId, allowCrossEnvironment, ...body } = args;
|
|
1573
|
+
// Environment isolation (JNG #136 §4): the consumer must match the session pin, and a
|
|
1574
|
+
// consumer must not bind a target from a DIFFERENT environment. Buckets are shared (no env).
|
|
1575
|
+
if (!allowCrossEnvironment) {
|
|
1576
|
+
let cEnv;
|
|
1577
|
+
try { cEnv = (await api.get(`/api/v1/apps/${appId}`)).data?.environment; } catch { /* can't resolve → don't block */ }
|
|
1578
|
+
if (sessionEnv && cEnv && cEnv !== sessionEnv) {
|
|
1579
|
+
throw new Error(`environment_mismatch: this session is pinned to "${sessionEnv}" but that service is in "${cEnv}" (JNG #136). Pass allowCrossEnvironment:true to override, or re-pin with neo_set_context.`);
|
|
1580
|
+
}
|
|
1581
|
+
if (args.targetType !== 'neo_object_storage') {
|
|
1582
|
+
const tPath = args.targetType === 'neo_db' ? `/api/v1/databases/${args.targetId}` : `/api/v1/queues/${args.targetId}`;
|
|
1583
|
+
let tEnv;
|
|
1584
|
+
try { tEnv = (await api.get(tPath)).data?.environment; } catch { /* ignore */ }
|
|
1585
|
+
if (cEnv && tEnv && cEnv !== tEnv) {
|
|
1586
|
+
throw new Error(`environment_mismatch: cannot bind a "${tEnv}" ${args.targetType} to a "${cEnv}" service — binding across environments is refused (JNG #136). Pass allowCrossEnvironment:true to override.`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1549
1590
|
const { status, data } = await api.post(`/api/v1/apps/${appId}/bindings`, body);
|
|
1550
1591
|
return redactSecrets(result(status, data));
|
|
1551
1592
|
},
|