@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.
@@ -6,7 +6,7 @@
6
6
  "icon": "users",
7
7
  "profile": "customers",
8
8
  "npmPackage": "@humanagencyp/crm-mcp",
9
- "npmVersion": "1.5.0",
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.0",
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 (simulation)",
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": {
@@ -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 manifestIsSimulated(manifest) {
3534
- return Boolean(manifest?.simulation);
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 until it does. Refusing by default (suveren-invoicing-plan.md \xA76).`
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
- return attemptWithCandidates(passers);
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() && !manifestIsSimulated(getManifest(config.id))) {
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 connector whose manifest
9123
- * carries the `simulation` marker, or a built-in that declares `simulation: true`.
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 manifestIsSimulated(getManifest(id));
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 has no manifest `simulation` marker, which is
9291
- // exactly the condition startIntegrationLocked refuses under — so
9292
- // the UI always reflects why a real connector never started,
9293
- // without needing a separate tracking path for this one reason.
9294
- // This is `paused`, NOT `error`: it is not a problem, it's the
9295
- // mode working as designed (see IntegrationStatus.paused). Genuine
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() && !manifestIsSimulated(getManifest(config.id)) ? "simulation" : void 0
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 s = byId.get(t.integrationId) ?? { id: t.integrationId, profile: t.gating.profile, actionTypes: [], writeTools: [], readTools: [] };
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(t.integrationId, s);
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) => manifestIsSimulated(m) && !running.has(m.id)).map((m) => m.name);
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) {