@suveren/gateway 0.35.2 → 0.36.0

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.
@@ -16,7 +16,7 @@ import { existsSync, mkdirSync, openSync, readFileSync, readdirSync, writeFileSy
16
16
  import { homedir, platform, userInfo } from 'node:os';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
- import { buildLaunchAgentPlist, buildMacLauncher, buildSystemdUnit, buildWindowsTaskXml } from '../lib/autostart-templates.mjs';
19
+ import { buildLaunchAgentPlist, buildMacLauncher, buildSystemdUnit, buildWindowsTaskXml, serviceOwnsInstall } from '../lib/autostart-templates.mjs';
20
20
  import { DEFAULT_AS_URL, readConfig, writeConfig, validateAsUrl, validateCaFile, validateProxyUrl, validatePinTls, resolveAsUrl, resolvePinTls, resolvePinTlsExpectedFingerprint, resolveSimulation } from '../lib/config.mjs';
21
21
  import { createInterface } from 'node:readline/promises';
22
22
  import { readPairing as readAsPairing, recordTlsPin, formatFingerprint, normalizeFingerprint } from '../lib/as-pairing.mjs';
@@ -1038,7 +1038,27 @@ const LAUNCH_AGENT_LABEL = 'ai.suveren.gateway';
1038
1038
  * it could not see. Every command has to know about the service the same CLI
1039
1039
  * installs.
1040
1040
  */
1041
+ function serviceDefinitionText() {
1042
+ const os = platform();
1043
+ const file = os === 'darwin' ? launchAgentPath() : os === 'linux' ? systemdUnitPath() : null;
1044
+ if (!file) return null;
1045
+ try { return existsSync(file) ? readFileSync(file, 'utf8') : null; } catch { return null; }
1046
+ }
1047
+
1048
+ /** The login service runs this CLI's installation — see serviceOwnsInstall. */
1049
+ function serviceIsOurs() {
1050
+ return serviceOwnsInstall({
1051
+ definitionText: serviceDefinitionText(),
1052
+ dataDir: DATA_DIR,
1053
+ envDataDir: process.env.SUVEREN_DATA_DIR,
1054
+ same: (a, b) => resolve(a) === resolve(b),
1055
+ });
1056
+ }
1057
+
1041
1058
  function serviceRunning() {
1059
+ // Another installation's service (a second gateway under this OS user) is
1060
+ // not "running" for this CLI — never stop, restart or report it as ours.
1061
+ if (!serviceIsOurs()) return false;
1042
1062
  const os = platform();
1043
1063
  if (os === 'darwin') {
1044
1064
  const p = runQuiet('launchctl', ['print', `gui/${process.getuid()}/${LAUNCH_AGENT_LABEL}`]);
@@ -1104,6 +1124,13 @@ async function service(args) {
1104
1124
  process.exit(2);
1105
1125
  }
1106
1126
 
1127
+ if ((sub === 'install' || sub === 'uninstall') && !serviceIsOurs()) {
1128
+ console.error('The login service on this account belongs to another Suveren gateway installation');
1129
+ console.error(`(a different data folder than ${DATA_DIR}). Refusing to change it.`);
1130
+ console.error('Run this installation in the background with: suveren-gateway start --detach');
1131
+ process.exit(2);
1132
+ }
1133
+
1107
1134
  switch (sub) {
1108
1135
  case 'install': await impl.install(); break;
1109
1136
  case 'uninstall': await impl.uninstall(); break;
@@ -122,6 +122,7 @@
122
122
  }
123
123
  },
124
124
  "create_event": {
125
+ "message": { "text": "description", "verb": "Sent" },
125
126
  "summary": "Event: {summary|short}[ on {start.dateTime,start.date|date}]",
126
127
  "executionMapping": {
127
128
  "duration_minutes": "duration",
@@ -144,6 +145,7 @@
144
145
  "actionLabel": "Event created"
145
146
  },
146
147
  "update_event": {
148
+ "message": { "text": "description", "verb": "Sent" },
147
149
  "summary": "Update event[: {summary|short}]",
148
150
  "executionMapping": {
149
151
  "attendees": [
@@ -6,7 +6,7 @@
6
6
  "icon": "briefcase",
7
7
  "profile": "sales",
8
8
  "npmPackage": "@humanagencyp/erp-mcp",
9
- "npmVersion": "0.9.1",
9
+ "npmVersion": "0.9.2",
10
10
  "mcp": {
11
11
  "command": "erp-mcp",
12
12
  "args": [],
@@ -276,7 +276,7 @@
276
276
  ],
277
277
  "display": {
278
278
  "order": ["customer_id", "order_id", "number", "document_date", "service_period", "lines", "net_total", "currency", "payment_term_days", "memo"],
279
- "money": ["net_total", "customer_id.credit_limit", "customer_id.open_balance", "customer_id.available_credit"],
279
+ "money": ["net_total", "lines[].unit_price", "customer_id.credit_limit", "customer_id.open_balance", "customer_id.available_credit"],
280
280
  "currencyField": "currency",
281
281
  "labels": {
282
282
  "customer_id": "Customer",
@@ -311,7 +311,7 @@
311
311
  },
312
312
  "display": {
313
313
  "order": ["invoice_id", "number", "document_date", "credit_total", "currency", "reason", "service_period", "lines"],
314
- "money": ["credit_total"],
314
+ "money": ["credit_total", "lines[].unit_price"],
315
315
  "currencyField": "currency",
316
316
  "labels": {
317
317
  "invoice_id": "Invoice",
@@ -84,6 +84,7 @@
84
84
  },
85
85
  "overrides": {
86
86
  "send_message": {
87
+ "message": { "text": "body", "to": "to", "cc": "cc", "subject": "subject", "verb": "Sent" },
87
88
  "summary": "Email to {to|list}: {subject|short}",
88
89
  "executionMapping": {
89
90
  "to": [
@@ -116,6 +117,7 @@
116
117
  "display": { "aiText": "body" }
117
118
  },
118
119
  "create_draft": {
120
+ "message": { "text": "body", "to": "to", "cc": "cc", "subject": "subject", "verb": "Sent" },
119
121
  "summary": "Draft email to {to|list}[: {subject|short}]",
120
122
  "executionMapping": {
121
123
  "to": [
@@ -62,6 +62,7 @@
62
62
  },
63
63
  "overrides": {
64
64
  "create_post": {
65
+ "message": { "text": "text", "verb": "Published" },
65
66
  "summary": "LinkedIn post: {text|short}",
66
67
  "executionMapping": {
67
68
  "visibility": "audience"
@@ -74,6 +75,7 @@
74
75
  "actionLabel": "Post published"
75
76
  },
76
77
  "create_image_post": {
78
+ "message": { "text": "text", "verb": "Published" },
77
79
  "summary": "LinkedIn image post: {text|short}",
78
80
  "executionMapping": {
79
81
  "visibility": "audience"
@@ -86,6 +88,7 @@
86
88
  "actionLabel": "Image post published"
87
89
  },
88
90
  "create_article_post": {
91
+ "message": { "text": "text", "verb": "Published" },
89
92
  "summary": "LinkedIn article: {title,articleUrl|short}",
90
93
  "executionMapping": {
91
94
  "visibility": "audience"
@@ -85,7 +85,16 @@
85
85
  "resultDatePath": "received_at"
86
86
  }
87
87
  },
88
+ "get_attachment": {
89
+ "category": "read",
90
+ "boundField": "read_access",
91
+ "requiredValue": "unlimited",
92
+ "read": {
93
+ "ageField": "read_age_days"
94
+ }
95
+ },
88
96
  "send_message": {
97
+ "message": { "text": "body", "to": "to", "cc": "cc", "subject": "subject", "verb": "Sent" },
89
98
  "summary": "Email to {to|list}: {subject|short}",
90
99
  "executionMapping": {
91
100
  "to": [
@@ -3601,8 +3601,6 @@ function setAsBaseUrl(url) {
3601
3601
  }
3602
3602
  var OPERATOR_NAME = process.env.SUVEREN_OPERATOR_NAME ?? "Suveren";
3603
3603
  var HAP_URL = "https://www.humanagencyprotocol.org/";
3604
- var FOOTER_PROFILES = /* @__PURE__ */ new Set(["email", "calendar", "publish"]);
3605
- var CONTENT_FIELD_CANDIDATES = ["body", "text", "description", "content"];
3606
3604
  var FOOTER_RE = /\n*(?:—|--) (?:Sent|Published) by (?:an AI agent|[^\n]+?'s AI agent)[\s\S]*$/;
3607
3605
  function shouldAttachFooter() {
3608
3606
  return true;
@@ -3610,9 +3608,6 @@ function shouldAttachFooter() {
3610
3608
  function receiptUrl(receiptId) {
3611
3609
  return `${AS_BASE}/t/${receiptId}`;
3612
3610
  }
3613
- function verbForProfile(profile) {
3614
- return profile === "publish" ? "Published" : "Sent";
3615
- }
3616
3611
  function hapLine() {
3617
3612
  return `-- ${OPERATOR_NAME} implements HAP, the open Human Agency Protocol: ${HAP_URL}`;
3618
3613
  }
@@ -3620,32 +3615,22 @@ function footerText(receiptId, verb, subject) {
3620
3615
  const identity = deriveIdentityLine(subject, { operatorName: OPERATOR_NAME }).replace(/^Sent\b/, verb);
3621
3616
  return `
3622
3617
 
3623
- -- ${identity}. Receipt: ${receiptUrl(receiptId)}
3618
+ -- ${identity}. Ticket: ${receiptUrl(receiptId)}
3624
3619
 
3625
3620
  ${hapLine()}`;
3626
3621
  }
3627
- function isStringType(t) {
3628
- return t === "string" || Array.isArray(t) && t.includes("string");
3629
- }
3630
3622
  function detectContentField(tool) {
3631
- const schema = tool.inputSchema;
3632
- const props = schema?.properties ?? {};
3633
- for (const candidate of CONTENT_FIELD_CANDIDATES) {
3634
- const prop = props[candidate];
3635
- if (prop && isStringType(prop.type)) return candidate;
3636
- }
3637
- return null;
3623
+ return tool.gating?.message?.text ?? null;
3638
3624
  }
3639
3625
  function stripFooter(value) {
3640
3626
  return value.replace(FOOTER_RE, "").replace(/\n+$/, "");
3641
3627
  }
3642
3628
  function appendVerificationFooter(tool, args, receiptId, subject) {
3643
- const profile = tool.gating?.profile;
3644
- if (!profile || !FOOTER_PROFILES.has(profile)) return args;
3645
- const field = detectContentField(tool);
3646
- if (!field) return args;
3629
+ const message = tool.gating?.message;
3630
+ if (!message) return args;
3631
+ const field = message.text;
3647
3632
  const current = typeof args[field] === "string" ? args[field] : "";
3648
- return { ...args, [field]: stripFooter(current) + footerText(receiptId, verbForProfile(profile), subject) };
3633
+ return { ...args, [field]: stripFooter(current) + footerText(receiptId, message.verb, subject) };
3649
3634
  }
3650
3635
 
3651
3636
  // src/lib/content-binding.ts
@@ -4059,6 +4044,37 @@ function invalidManifestInternalReason(manifest) {
4059
4044
  }
4060
4045
  return null;
4061
4046
  }
4047
+ var VALID_MESSAGE_VERBS = ["Sent", "Published"];
4048
+ function invalidMessageReason(toolName, entry) {
4049
+ if (entry === null || typeof entry !== "object") return null;
4050
+ const message = entry.message;
4051
+ if (message === void 0) return null;
4052
+ if (message === null || typeof message !== "object") {
4053
+ return `override "${toolName}".message must be an object`;
4054
+ }
4055
+ const m = message;
4056
+ const at = `override "${toolName}".message`;
4057
+ if (typeof m.text !== "string" || m.text.length === 0) {
4058
+ return `${at}.text must be a non-empty string`;
4059
+ }
4060
+ if (typeof m.verb !== "string" || !VALID_MESSAGE_VERBS.includes(m.verb)) {
4061
+ return `${at}.verb must be one of ${VALID_MESSAGE_VERBS.join(", ")}`;
4062
+ }
4063
+ for (const key of ["to", "cc", "subject"]) {
4064
+ if (m[key] !== void 0 && (typeof m[key] !== "string" || m[key].length === 0)) {
4065
+ return `${at}.${key} must be a non-empty string`;
4066
+ }
4067
+ }
4068
+ return null;
4069
+ }
4070
+ function invalidManifestMessageReason(manifest) {
4071
+ const overrides = manifest.toolGating?.overrides ?? {};
4072
+ for (const [toolName, entry] of Object.entries(overrides)) {
4073
+ const reason = invalidMessageReason(toolName, entry);
4074
+ if (reason) return reason;
4075
+ }
4076
+ return null;
4077
+ }
4062
4078
  function invalidManifestPreviewReason(manifest) {
4063
4079
  const overrides = manifest.toolGating?.overrides ?? {};
4064
4080
  for (const [toolName, entry] of Object.entries(overrides)) {
@@ -4488,6 +4504,11 @@ function loadManifests(integrationsDir) {
4488
4504
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${internalReason}`);
4489
4505
  continue;
4490
4506
  }
4507
+ const messageReason = invalidManifestMessageReason(manifest);
4508
+ if (messageReason) {
4509
+ console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${messageReason}`);
4510
+ continue;
4511
+ }
4491
4512
  const contextReason = invalidManifestContextReason(manifest);
4492
4513
  if (contextReason) {
4493
4514
  console.error(`[ManifestLoader] Refusing manifest ${id} (${manifestPath}): ${contextReason}`);
@@ -5698,7 +5719,7 @@ function toStringArray(value) {
5698
5719
  if (typeof value === "string" && value) return [value];
5699
5720
  return [];
5700
5721
  }
5701
- async function finalizeAttachmentsForExecution(integrationManager2, integrationId, args) {
5722
+ async function finalizeAttachmentsForExecution(integrationManager2, integrationId, args, message) {
5702
5723
  const sealed = args[ATTACHMENTS_SEALED_META_KEY];
5703
5724
  if (!sealed || sealed.attachments.length === 0 && sealed.inserts.length === 0) {
5704
5725
  return { ok: true, args };
@@ -5749,15 +5770,23 @@ async function finalizeAttachmentsForExecution(integrationManager2, integrationI
5749
5770
  contentBase64: d.contentBase64
5750
5771
  }));
5751
5772
  } else {
5773
+ if (!message?.to) {
5774
+ return {
5775
+ ok: false,
5776
+ message: `Refused: "${integrationId}" sends attachments as one built message, but this tool declares no message fields (manifest \`message.text\` and \`message.to\`) \u2014 nothing was sent.`
5777
+ };
5778
+ }
5779
+ const decl = message;
5752
5780
  const arg = transport.arg;
5753
5781
  const attachments = resolvedDocs.map((d) => ({ filename: d.filename, contentType: d.contentType, contentBase64: d.contentBase64 }));
5782
+ const str = (a, name) => name && typeof a[name] === "string" ? a[name] : "";
5754
5783
  const withRaw = (a) => ({
5755
5784
  ...a,
5756
5785
  [arg]: toBase64Url(buildMimeMessage({
5757
- to: toStringArray(a.to),
5758
- cc: toStringArray(a.cc),
5759
- subject: typeof a.subject === "string" ? a.subject : "",
5760
- bodyText: typeof a.body === "string" ? a.body : "",
5786
+ to: toStringArray(a[decl.to]),
5787
+ cc: decl.cc ? toStringArray(a[decl.cc]) : [],
5788
+ subject: str(a, decl.subject),
5789
+ bodyText: str(a, decl.text),
5761
5790
  attachments
5762
5791
  }))
5763
5792
  });
@@ -5790,10 +5819,11 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
5790
5819
  return void 0;
5791
5820
  }
5792
5821
  let effectiveArgs = toolArgs;
5793
- if (binding.fields?.includes("attachments")) {
5822
+ const attachmentsArg = tool?.gating?.attachmentsArg;
5823
+ if (attachmentsArg && binding.fields?.includes(attachmentsArg)) {
5794
5824
  const sealed = toolArgs[ATTACHMENTS_SEALED_META_KEY];
5795
5825
  const fingerprints = (sealed?.attachments ?? []).map((a) => ({ filename: a.filename, sha256: a.sha256 }));
5796
- effectiveArgs = { ...toolArgs, attachments: fingerprints };
5826
+ effectiveArgs = { ...toolArgs, [attachmentsArg]: fingerprints };
5797
5827
  }
5798
5828
  const bound = selectBoundFields(binding, effectiveArgs);
5799
5829
  return {
@@ -6798,7 +6828,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
6798
6828
  let finalArgs = callArgs;
6799
6829
  let rebuildTransport;
6800
6830
  if (tool.gating?.attachmentsArg || tool.gating?.insertFields?.length) {
6801
- const finalized = await finalizeAttachmentsForExecution(integrationManager2, tool.integrationId, callArgs);
6831
+ const finalized = await finalizeAttachmentsForExecution(integrationManager2, tool.integrationId, callArgs, tool.gating?.message);
6802
6832
  if (!finalized.ok) {
6803
6833
  return { content: [{ type: "text", text: finalized.message }], isError: true };
6804
6834
  }
@@ -7887,8 +7917,9 @@ async function executeCommitted(proposal, state2, integrationManager2) {
7887
7917
  }
7888
7918
  }
7889
7919
  let finalToolArgs = toolArgs;
7920
+ let rebuildTransport;
7890
7921
  {
7891
- const finalized = await finalizeAttachmentsForExecution(integrationManager2, integrationId, toolArgs);
7922
+ const finalized = await finalizeAttachmentsForExecution(integrationManager2, integrationId, toolArgs, discovered?.gating?.message);
7892
7923
  if (!finalized.ok) {
7893
7924
  state2.executionJournal.complete(receiptId, "failed", "changed", finalized.message);
7894
7925
  recordExecutionOutcome(state2, proposal.id, receiptId, false, finalized.message);
@@ -7900,6 +7931,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
7900
7931
  };
7901
7932
  }
7902
7933
  finalToolArgs = finalized.args;
7934
+ rebuildTransport = finalized.rebuildTransport;
7903
7935
  }
7904
7936
  try {
7905
7937
  let outgoingArgs = finalToolArgs;
@@ -7908,6 +7940,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
7908
7940
  outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
7909
7941
  }
7910
7942
  outgoingArgs = attachTicketId(discovered, outgoingArgs, receiptId);
7943
+ if (rebuildTransport) outgoingArgs = rebuildTransport(outgoingArgs);
7911
7944
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
7912
7945
  }
7913
7946
  outgoingArgs = stripSourceLink(outgoingArgs);
@@ -9542,7 +9575,8 @@ var IntegrationManager = class {
9542
9575
  internal: ext.internal,
9543
9576
  attachmentsArg: ext.attachmentsArg,
9544
9577
  insertFields: ext.insertFields,
9545
- aiTextArg: ext.display?.aiText
9578
+ aiTextArg: ext.display?.aiText,
9579
+ message: ext.message
9546
9580
  };
9547
9581
  }
9548
9582
  return {
@@ -11733,12 +11767,11 @@ async function startIntegrationForService(serviceId) {
11733
11767
  (c) => c.id === serviceId || Object.values(c.envKeys ?? {}).some((ref) => typeof ref === "string" && ref.startsWith(`${serviceId}.`))
11734
11768
  );
11735
11769
  for (const config of candidates) {
11770
+ await startOneIntegration(config);
11736
11771
  if (integrationManager.isRunning(config.id) && integrationManager.credentialsChangedSinceStart(config)) {
11737
11772
  console.error(`[Suveren MCP] ${config.id}: credentials changed since start \u2014 restarting`);
11738
11773
  await startOneIntegration(config, { restart: true });
11739
- continue;
11740
11774
  }
11741
- await startOneIntegration(config);
11742
11775
  }
11743
11776
  }
11744
11777
  async function startOneIntegration(config, opts = {}) {