@suveren/gateway 0.35.0 → 0.35.1
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/content/integrations/crm.json +18 -2
- package/content/integrations/erp.json +9 -1
- package/content/integrations/mail.json +4 -1
- package/dist/mcp-server/http.mjs +104 -20
- package/dist/ui/assets/{index-M1sLTJjt.js → index-BHxzXFpf.js} +28 -28
- package/dist/ui/assets/{index-c8cmIEPx.css → index-XiOgY6b5.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"icon": "users",
|
|
7
7
|
"profile": "customers",
|
|
8
8
|
"npmPackage": "@humanagencyp/crm-mcp",
|
|
9
|
-
"npmVersion": "1.
|
|
9
|
+
"npmVersion": "1.6.0",
|
|
10
10
|
"mcp": {
|
|
11
11
|
"command": "crm-mcp",
|
|
12
12
|
"args": []
|
|
@@ -26,17 +26,33 @@
|
|
|
26
26
|
"type": "text",
|
|
27
27
|
"placeholder": "simulation (default) — live needs an adapter for your CRM",
|
|
28
28
|
"optional": true
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"key": "liveAdapter",
|
|
32
|
+
"label": "Live adapter (optional)",
|
|
33
|
+
"type": "text",
|
|
34
|
+
"placeholder": "local — your own company's CRM data. Leave empty unless Mode is \"live\"",
|
|
35
|
+
"optional": true
|
|
29
36
|
}
|
|
30
37
|
],
|
|
31
38
|
"envMapping": {
|
|
32
39
|
"DATABASE_URL": "databaseUrl",
|
|
33
|
-
"CRM_MODE": "mode"
|
|
40
|
+
"CRM_MODE": "mode",
|
|
41
|
+
"CRM_LIVE_ADAPTER": "liveAdapter"
|
|
34
42
|
}
|
|
35
43
|
},
|
|
36
44
|
"simulation": {
|
|
37
45
|
"field": "mode",
|
|
38
46
|
"default": "simulation"
|
|
39
47
|
},
|
|
48
|
+
"modeLabels": {
|
|
49
|
+
"simulation": "Test data",
|
|
50
|
+
"live": { "local": "Local" }
|
|
51
|
+
},
|
|
52
|
+
"modeNotes": {
|
|
53
|
+
"live": { "local": "On this computer — no shared access" }
|
|
54
|
+
},
|
|
55
|
+
"adapterField": "liveAdapter",
|
|
40
56
|
"personalDefault": true,
|
|
41
57
|
"oauth": null,
|
|
42
58
|
"context": {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"icon": "briefcase",
|
|
7
7
|
"profile": "sales",
|
|
8
8
|
"npmPackage": "@humanagencyp/erp-mcp",
|
|
9
|
-
"npmVersion": "0.9.
|
|
9
|
+
"npmVersion": "0.9.1",
|
|
10
10
|
"mcp": {
|
|
11
11
|
"command": "erp-mcp",
|
|
12
12
|
"args": [],
|
|
@@ -62,6 +62,14 @@
|
|
|
62
62
|
"field": "mode",
|
|
63
63
|
"default": "simulation"
|
|
64
64
|
},
|
|
65
|
+
"modeLabels": {
|
|
66
|
+
"simulation": "Test data",
|
|
67
|
+
"live": { "local": "Local" }
|
|
68
|
+
},
|
|
69
|
+
"modeNotes": {
|
|
70
|
+
"live": { "local": "On this computer — no shared access" }
|
|
71
|
+
},
|
|
72
|
+
"adapterField": "liveAdapter",
|
|
65
73
|
"personalDefault": true,
|
|
66
74
|
"oauth": null,
|
|
67
75
|
"documents": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mail",
|
|
3
|
-
"name": "Email
|
|
3
|
+
"name": "Email",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"description": "Simulated inbox for testing an agent's email replies — nothing leaves the laptop",
|
|
6
6
|
"icon": "mail",
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"field": "mode",
|
|
41
41
|
"default": "simulation"
|
|
42
42
|
},
|
|
43
|
+
"modeLabels": {
|
|
44
|
+
"simulation": "Test mailbox"
|
|
45
|
+
},
|
|
43
46
|
"personalDefault": false,
|
|
44
47
|
"oauth": null,
|
|
45
48
|
"attachments": {
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -3530,8 +3530,11 @@ function isSimulationMode() {
|
|
|
3530
3530
|
if (policySimulation !== void 0) return policySimulation;
|
|
3531
3531
|
return process.env.SUVEREN_SIMULATION === "1";
|
|
3532
3532
|
}
|
|
3533
|
-
function
|
|
3534
|
-
|
|
3533
|
+
function connectorCurrentlySimulated(manifest, fields) {
|
|
3534
|
+
const decl = manifest?.simulation;
|
|
3535
|
+
if (!decl) return false;
|
|
3536
|
+
const value = (fields?.[decl.field] ?? "").trim().toLowerCase() || decl.default.trim().toLowerCase();
|
|
3537
|
+
return value === "simulation";
|
|
3535
3538
|
}
|
|
3536
3539
|
var SIMULATION_BLOCK_REASON = "blocked: simulation mode \u2014 real systems are off";
|
|
3537
3540
|
|
|
@@ -4415,6 +4418,39 @@ function invalidManifestReadGateReason(manifest) {
|
|
|
4415
4418
|
}
|
|
4416
4419
|
return null;
|
|
4417
4420
|
}
|
|
4421
|
+
function invalidModeLabelsReason(manifest) {
|
|
4422
|
+
if (!manifest.modeLabels) return null;
|
|
4423
|
+
if (manifest.modeLabels === null || typeof manifest.modeLabels !== "object" || Array.isArray(manifest.modeLabels)) {
|
|
4424
|
+
return "modeLabels must be an object mapping mode values to labels";
|
|
4425
|
+
}
|
|
4426
|
+
if (!manifest.simulation) {
|
|
4427
|
+
return 'modeLabels is set but this manifest declares no "simulation" marker \u2014 mode labels need a mode field to key off';
|
|
4428
|
+
}
|
|
4429
|
+
const fieldKeys = new Set(manifest.credentials?.fields?.map((f) => f.key) ?? []);
|
|
4430
|
+
let needsAdapterField = false;
|
|
4431
|
+
for (const [mode, label] of Object.entries(manifest.modeLabels)) {
|
|
4432
|
+
if (typeof label === "string") {
|
|
4433
|
+
if (label.length === 0) return `modeLabels.${mode} must be a non-empty string`;
|
|
4434
|
+
continue;
|
|
4435
|
+
}
|
|
4436
|
+
if (label === null || typeof label !== "object" || Array.isArray(label)) {
|
|
4437
|
+
return `modeLabels.${mode} must be a non-empty string or an object keyed by adapter value`;
|
|
4438
|
+
}
|
|
4439
|
+
needsAdapterField = true;
|
|
4440
|
+
for (const [adapter, adapterLabel] of Object.entries(label)) {
|
|
4441
|
+
if (typeof adapterLabel !== "string" || adapterLabel.length === 0) {
|
|
4442
|
+
return `modeLabels.${mode}.${adapter} must be a non-empty string`;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
if (needsAdapterField && !manifest.adapterField) {
|
|
4447
|
+
return 'modeLabels has an adapter-keyed entry but "adapterField" is missing \u2014 name the credential field holding the adapter';
|
|
4448
|
+
}
|
|
4449
|
+
if (manifest.adapterField && !fieldKeys.has(manifest.adapterField)) {
|
|
4450
|
+
return `adapterField "${manifest.adapterField}" does not name a declared credential field`;
|
|
4451
|
+
}
|
|
4452
|
+
return null;
|
|
4453
|
+
}
|
|
4418
4454
|
var manifests = /* @__PURE__ */ new Map();
|
|
4419
4455
|
function loadManifests(integrationsDir) {
|
|
4420
4456
|
const dir = resolve(
|
|
@@ -4482,6 +4518,11 @@ function loadManifests(integrationsDir) {
|
|
|
4482
4518
|
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${valuesReason}`);
|
|
4483
4519
|
continue;
|
|
4484
4520
|
}
|
|
4521
|
+
const modeLabelsReason = invalidModeLabelsReason(manifest) ?? (manifest.modeNotes ? invalidModeLabelsReason({ ...manifest, modeLabels: manifest.modeNotes })?.replace(/modeLabels/g, "modeNotes") ?? null : null);
|
|
4522
|
+
if (modeLabelsReason) {
|
|
4523
|
+
console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${modeLabelsReason}`);
|
|
4524
|
+
continue;
|
|
4525
|
+
}
|
|
4485
4526
|
manifests.set(id, manifest);
|
|
4486
4527
|
loaded++;
|
|
4487
4528
|
} catch (err) {
|
|
@@ -4612,7 +4653,7 @@ function isAttachmentSourceAllowed(profileId, profile, allowedSourcesValue, ref)
|
|
|
4612
4653
|
if (!attachmentSourcesFieldDeclared(profile)) {
|
|
4613
4654
|
return {
|
|
4614
4655
|
allowed: false,
|
|
4615
|
-
reason: `Profile "${profileId}" does not yet declare "${ATTACHMENT_SOURCES_FIELD}" \u2014 no mandate can authorize an attachment
|
|
4656
|
+
reason: `Profile "${profileId}" does not yet declare "${ATTACHMENT_SOURCES_FIELD}" \u2014 no mandate can authorize an attachment \u2014 a mandate on a newer email profile that lists the source is needed.`
|
|
4616
4657
|
};
|
|
4617
4658
|
}
|
|
4618
4659
|
const allowedSet = new Set(
|
|
@@ -4627,6 +4668,22 @@ function isAttachmentSourceAllowed(profileId, profile, allowedSourcesValue, ref)
|
|
|
4627
4668
|
}
|
|
4628
4669
|
return { allowed: true, reason: "" };
|
|
4629
4670
|
}
|
|
4671
|
+
function candidatesAllowingAttachments(candidates, refs, profileOf) {
|
|
4672
|
+
if (refs.length === 0) return { allowed: candidates };
|
|
4673
|
+
let refusal;
|
|
4674
|
+
const allowed = candidates.filter((c) => {
|
|
4675
|
+
const value = typeof c.context?.[ATTACHMENT_SOURCES_FIELD] === "string" ? c.context[ATTACHMENT_SOURCES_FIELD] : void 0;
|
|
4676
|
+
for (const ref of refs) {
|
|
4677
|
+
const check = isAttachmentSourceAllowed(c.profileId, profileOf(c.profileId), value, ref);
|
|
4678
|
+
if (!check.allowed) {
|
|
4679
|
+
refusal ??= check.reason;
|
|
4680
|
+
return false;
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return true;
|
|
4684
|
+
});
|
|
4685
|
+
return { allowed, refusal: allowed.length ? void 0 : refusal };
|
|
4686
|
+
}
|
|
4630
4687
|
|
|
4631
4688
|
// src/lib/preview.ts
|
|
4632
4689
|
import { computeContentHash } from "@hap/core";
|
|
@@ -6632,7 +6689,20 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
|
|
|
6632
6689
|
};
|
|
6633
6690
|
}
|
|
6634
6691
|
const contextKeys = getProfile5(passers[0].profileId)?.scopeSchema?.keyOrder ?? [];
|
|
6635
|
-
|
|
6692
|
+
let pool = passers;
|
|
6693
|
+
if (tool.gating?.attachmentsArg) {
|
|
6694
|
+
const raw = args[tool.gating.attachmentsArg];
|
|
6695
|
+
const refs = Array.isArray(raw) ? raw.filter((r) => !!r && typeof r === "object" && typeof r.connector === "string" && typeof r.kind === "string") : [];
|
|
6696
|
+
const narrowed = candidatesAllowingAttachments(passers, refs, (id) => getProfile5(id));
|
|
6697
|
+
if (narrowed.allowed.length === 0) {
|
|
6698
|
+
return {
|
|
6699
|
+
content: [{ type: "text", text: `Refused: attachment not allowed by any ${tool.gating.profile ?? ""} mandate. ${narrowed.refusal ?? ""}`.trim() }],
|
|
6700
|
+
isError: true
|
|
6701
|
+
};
|
|
6702
|
+
}
|
|
6703
|
+
pool = narrowed.allowed;
|
|
6704
|
+
}
|
|
6705
|
+
return attemptWithCandidates(pool);
|
|
6636
6706
|
async function attemptWithCandidates(candidates) {
|
|
6637
6707
|
const selection = selectAuthorization(
|
|
6638
6708
|
contextKeys,
|
|
@@ -8899,7 +8969,7 @@ var IntegrationManager = class {
|
|
|
8899
8969
|
if (opts.skipIfRunning) return existing.tools;
|
|
8900
8970
|
await this.stopIntegrationLocked(config.id);
|
|
8901
8971
|
}
|
|
8902
|
-
if (isSimulationMode() && !
|
|
8972
|
+
if (isSimulationMode() && !this.isSimulationSafe(config.id)) {
|
|
8903
8973
|
throw new Error(`${config.id}: ${SIMULATION_BLOCK_REASON}`);
|
|
8904
8974
|
}
|
|
8905
8975
|
if (config.npmPackage) {
|
|
@@ -9119,13 +9189,24 @@ var IntegrationManager = class {
|
|
|
9119
9189
|
return this.builtins.get(id)?.def.alwaysListed === true;
|
|
9120
9190
|
}
|
|
9121
9191
|
/**
|
|
9122
|
-
* Whether `id` may run while simulation mode is on: a
|
|
9123
|
-
*
|
|
9192
|
+
* Whether `id` may run while simulation mode is on: a built-in that
|
|
9193
|
+
* declares `simulation: true`, or a connector whose CURRENT setting (the
|
|
9194
|
+
* credential/setting field its manifest's `simulation.field` names,
|
|
9195
|
+
* defaulting per `simulation.default` when unset) resolves to simulated —
|
|
9196
|
+
* NOT merely a connector whose manifest supports a simulated mode at all.
|
|
9197
|
+
* A connector set to a real mode (e.g. ERP's `mode=live`) is refused here
|
|
9198
|
+
* exactly like one with no simulation capability at all.
|
|
9199
|
+
*
|
|
9200
|
+
* Reads `serviceCredentials` directly, keyed by `id` — the same convention
|
|
9201
|
+
* `resolveEnvKeys`'s manifest-mapping fallback and `getServiceCredentials`
|
|
9202
|
+
* already rely on: a connector's OWN credential fields (mode, liveAdapter,
|
|
9203
|
+
* …) are pushed to this gateway under its own id (control-plane's
|
|
9204
|
+
* `pushServiceCredentials(id, fields)`), never a shared/other service id.
|
|
9124
9205
|
*/
|
|
9125
9206
|
isSimulationSafe(id) {
|
|
9126
9207
|
const builtin = this.builtins.get(id);
|
|
9127
9208
|
if (builtin) return builtin.def.simulation === true;
|
|
9128
|
-
return
|
|
9209
|
+
return connectorCurrentlySimulated(getManifest(id), this.serviceCredentials.get(id));
|
|
9129
9210
|
}
|
|
9130
9211
|
/**
|
|
9131
9212
|
* The built-in's own refusal for this call, checked before the gate (no proposal,
|
|
@@ -9287,15 +9368,17 @@ var IntegrationManager = class {
|
|
|
9287
9368
|
toolCount: 0,
|
|
9288
9369
|
readAgeDays: readAgeOf(config),
|
|
9289
9370
|
// Computed fresh, not cached: true whenever simulation mode is on
|
|
9290
|
-
// AND this connector
|
|
9291
|
-
//
|
|
9292
|
-
//
|
|
9293
|
-
//
|
|
9294
|
-
//
|
|
9295
|
-
//
|
|
9371
|
+
// AND this connector is not currently simulation-safe (see
|
|
9372
|
+
// `isSimulationSafe` — no capability at all, OR capability
|
|
9373
|
+
// declared but currently SET to a real mode), which is exactly
|
|
9374
|
+
// the condition startIntegrationLocked refuses under — so the UI
|
|
9375
|
+
// always reflects why a real connector never started, without
|
|
9376
|
+
// needing a separate tracking path for this one reason. This is
|
|
9377
|
+
// `paused`, NOT `error`: it is not a problem, it's the mode
|
|
9378
|
+
// working as designed (see IntegrationStatus.paused). Genuine
|
|
9296
9379
|
// start failures still surface via the one-shot `warning` string
|
|
9297
9380
|
// returned by /internal/add-integration — they never land here.
|
|
9298
|
-
paused: isSimulationMode() && !
|
|
9381
|
+
paused: isSimulationMode() && !this.isSimulationSafe(config.id) ? "simulation" : void 0
|
|
9299
9382
|
});
|
|
9300
9383
|
}
|
|
9301
9384
|
}
|
|
@@ -10354,8 +10437,9 @@ function briefRefusal(args) {
|
|
|
10354
10437
|
function connectedSystems(im) {
|
|
10355
10438
|
const byId = /* @__PURE__ */ new Map();
|
|
10356
10439
|
for (const t of im.getAllTools()) {
|
|
10357
|
-
if (im.isBuiltin(t.integrationId) || !t.gating?.profile || t.gating.category === "disabled") continue;
|
|
10358
|
-
const
|
|
10440
|
+
if (im.isBuiltin(t.integrationId) || t.internal || !t.gating?.profile || t.gating.category === "disabled") continue;
|
|
10441
|
+
const key = `${t.integrationId}|${t.gating.profile}`;
|
|
10442
|
+
const s = byId.get(key) ?? { id: t.integrationId, profile: t.gating.profile, actionTypes: [], writeTools: [], readTools: [] };
|
|
10359
10443
|
if (t.gating.category === "read") {
|
|
10360
10444
|
s.readTools.push(t.originalName);
|
|
10361
10445
|
} else {
|
|
@@ -10363,9 +10447,9 @@ function connectedSystems(im) {
|
|
|
10363
10447
|
const at = t.gating.staticExecution?.action_type;
|
|
10364
10448
|
if (typeof at === "string" && !s.actionTypes.includes(at)) s.actionTypes.push(at);
|
|
10365
10449
|
}
|
|
10366
|
-
byId.set(
|
|
10450
|
+
byId.set(key, s);
|
|
10367
10451
|
}
|
|
10368
|
-
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
10452
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id) || a.profile.localeCompare(b.profile));
|
|
10369
10453
|
}
|
|
10370
10454
|
function shortName(idOrName) {
|
|
10371
10455
|
const stripped = idOrName.replace(/@.*$/, "").split("/").pop() ?? idOrName;
|
|
@@ -10586,7 +10670,7 @@ function setupBuiltin(deps) {
|
|
|
10586
10670
|
const guides = loadGuides();
|
|
10587
10671
|
const systems = connectedSystems(deps.integrationManager);
|
|
10588
10672
|
const running = new Set(systems.map((s) => s.id));
|
|
10589
|
-
const inactive = getAllManifests().filter((m) =>
|
|
10673
|
+
const inactive = getAllManifests().filter((m) => deps.integrationManager.isSimulationSafe(m.id) && !running.has(m.id)).map((m) => m.name);
|
|
10590
10674
|
const header = guideHeader(systems, inactive);
|
|
10591
10675
|
const topic = typeof args.topic === "string" ? args.topic.trim().toLowerCase() : "";
|
|
10592
10676
|
if (!topic) {
|