@sellable/mcp 0.1.902 → 0.1.903
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/dist/tools/integrations.d.ts +22 -0
- package/dist/tools/integrations.js +157 -2
- package/package.json +1 -1
|
@@ -77,6 +77,28 @@ export type IntegrationsResultEnvelope = {
|
|
|
77
77
|
guidance: string | null;
|
|
78
78
|
result: unknown;
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* A local Fly/Hermes path has no meaning inside Pipedream's cloud action runner.
|
|
82
|
+
* The live Gmail actions hand each `attachments[]` value to Nodemailer as
|
|
83
|
+
* `attachment.path`; Nodemailer accepts a data URI there, so a small PDF can cross
|
|
84
|
+
* the existing JSON call without a public bucket or a durable File Stash.
|
|
85
|
+
*
|
|
86
|
+
* Two MiB keeps the base64 body below the call route's dedicated 3 MiB ceiling.
|
|
87
|
+
* This is intentionally narrower than Gmail's own attachment limit: it is the
|
|
88
|
+
* bounded hot path for receipts and similar one-page PDFs, not a general file
|
|
89
|
+
* transfer service.
|
|
90
|
+
*/
|
|
91
|
+
export declare const INTEGRATION_LOCAL_PDF_MAX_BYTES: number;
|
|
92
|
+
/**
|
|
93
|
+
* Materialize only the two live Gmail actions whose file-ref contract is known.
|
|
94
|
+
* Remote URLs/data URIs remain byte-identical. Endpoint-run effects are already
|
|
95
|
+
* authorized against their original argument hash, so they may not introduce a
|
|
96
|
+
* local path that would change after authorization.
|
|
97
|
+
*/
|
|
98
|
+
export declare function materializeIntegrationLocalPdfAttachments(toolName: string, args: Record<string, unknown>, options: {
|
|
99
|
+
allowLocal: boolean;
|
|
100
|
+
env?: NodeJS.ProcessEnv;
|
|
101
|
+
}): Promise<Record<string, unknown>>;
|
|
80
102
|
type IntegrationHomeWorkspacePin = {
|
|
81
103
|
ok: true;
|
|
82
104
|
workspaceId?: string;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import { readMcpAgentServiceContext } from "../agent-service-auth.js";
|
|
2
5
|
import { getApi, SellableApiError } from "../api.js";
|
|
3
6
|
import { getConfig } from "../auth.js";
|
|
@@ -92,6 +95,152 @@ const CALL_PATH = "/api/v3/sellable-agent/integrations/tools/call";
|
|
|
92
95
|
*/
|
|
93
96
|
const READ_TIMEOUT_MS = 25_000;
|
|
94
97
|
const CALL_TIMEOUT_MS = 45_000;
|
|
98
|
+
/**
|
|
99
|
+
* A local Fly/Hermes path has no meaning inside Pipedream's cloud action runner.
|
|
100
|
+
* The live Gmail actions hand each `attachments[]` value to Nodemailer as
|
|
101
|
+
* `attachment.path`; Nodemailer accepts a data URI there, so a small PDF can cross
|
|
102
|
+
* the existing JSON call without a public bucket or a durable File Stash.
|
|
103
|
+
*
|
|
104
|
+
* Two MiB keeps the base64 body below the call route's dedicated 3 MiB ceiling.
|
|
105
|
+
* This is intentionally narrower than Gmail's own attachment limit: it is the
|
|
106
|
+
* bounded hot path for receipts and similar one-page PDFs, not a general file
|
|
107
|
+
* transfer service.
|
|
108
|
+
*/
|
|
109
|
+
export const INTEGRATION_LOCAL_PDF_MAX_BYTES = 2 * 1024 * 1024;
|
|
110
|
+
const GMAIL_FILE_ATTACHMENT_TOOLS = new Set([
|
|
111
|
+
"gmail-create-draft",
|
|
112
|
+
"gmail-send-email",
|
|
113
|
+
]);
|
|
114
|
+
const PDF_MAGIC = Object.freeze([0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
115
|
+
class IntegrationLocalAttachmentError extends Error {
|
|
116
|
+
constructor() {
|
|
117
|
+
super("integration_local_attachment_rejected");
|
|
118
|
+
this.name = "IntegrationLocalAttachmentError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function localAttachmentRefusal() {
|
|
122
|
+
return envelope({
|
|
123
|
+
ok: false,
|
|
124
|
+
outcome: "refused",
|
|
125
|
+
attribution: "configuration",
|
|
126
|
+
error: "integration_local_attachment_rejected",
|
|
127
|
+
guidance: "Attach a regular PDF under the Agent's writable media directory or /tmp; local PDFs are limited to 2 MiB total.",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function withinRoot(root, candidate) {
|
|
131
|
+
const relative = path.relative(root, candidate);
|
|
132
|
+
return (relative === "" ||
|
|
133
|
+
(relative !== ".." &&
|
|
134
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
135
|
+
!path.isAbsolute(relative)));
|
|
136
|
+
}
|
|
137
|
+
async function allowedLocalAttachmentRoots(env) {
|
|
138
|
+
const candidates = [env.HERMES_WRITE_SAFE_ROOT?.trim(), os.tmpdir()].filter((value) => Boolean(value && path.isAbsolute(value)));
|
|
139
|
+
const roots = await Promise.all([...new Set(candidates)].map(async (candidate) => {
|
|
140
|
+
try {
|
|
141
|
+
return await realpath(candidate);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
return roots.filter((value) => Boolean(value));
|
|
148
|
+
}
|
|
149
|
+
async function localPdfDataUri(localPath, filename, remainingBytes, env) {
|
|
150
|
+
if (!path.isAbsolute(localPath) ||
|
|
151
|
+
filename.length < 1 ||
|
|
152
|
+
filename.length > 160 ||
|
|
153
|
+
path.basename(filename) !== filename ||
|
|
154
|
+
!filename.toLowerCase().endsWith(".pdf") ||
|
|
155
|
+
/[\u0000-\u001f\u007f]/.test(filename)) {
|
|
156
|
+
throw new IntegrationLocalAttachmentError();
|
|
157
|
+
}
|
|
158
|
+
const [entry, resolved, roots] = await Promise.all([
|
|
159
|
+
lstat(localPath),
|
|
160
|
+
realpath(localPath),
|
|
161
|
+
allowedLocalAttachmentRoots(env),
|
|
162
|
+
]).catch(() => {
|
|
163
|
+
throw new IntegrationLocalAttachmentError();
|
|
164
|
+
});
|
|
165
|
+
if (entry.isSymbolicLink() ||
|
|
166
|
+
!roots.some((root) => withinRoot(root, resolved))) {
|
|
167
|
+
throw new IntegrationLocalAttachmentError();
|
|
168
|
+
}
|
|
169
|
+
const handle = await open(resolved, "r").catch(() => {
|
|
170
|
+
throw new IntegrationLocalAttachmentError();
|
|
171
|
+
});
|
|
172
|
+
let bytes;
|
|
173
|
+
try {
|
|
174
|
+
const opened = await handle.stat();
|
|
175
|
+
if (!entry.isFile() ||
|
|
176
|
+
!opened.isFile() ||
|
|
177
|
+
opened.dev !== entry.dev ||
|
|
178
|
+
opened.ino !== entry.ino ||
|
|
179
|
+
opened.size < PDF_MAGIC.length ||
|
|
180
|
+
opened.size > remainingBytes) {
|
|
181
|
+
throw new IntegrationLocalAttachmentError();
|
|
182
|
+
}
|
|
183
|
+
bytes = new Uint8Array(new ArrayBuffer(opened.size));
|
|
184
|
+
let offset = 0;
|
|
185
|
+
while (offset < opened.size) {
|
|
186
|
+
const chunk = await handle.read(bytes, offset, opened.size - offset, offset);
|
|
187
|
+
if (chunk.bytesRead < 1)
|
|
188
|
+
throw new IntegrationLocalAttachmentError();
|
|
189
|
+
offset += chunk.bytesRead;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof IntegrationLocalAttachmentError)
|
|
194
|
+
throw error;
|
|
195
|
+
throw new IntegrationLocalAttachmentError();
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await handle.close().catch(() => undefined);
|
|
199
|
+
}
|
|
200
|
+
if (!PDF_MAGIC.every((byte, index) => bytes[index] === byte)) {
|
|
201
|
+
throw new IntegrationLocalAttachmentError();
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
dataUri: `data:application/pdf;base64,${Buffer.from(bytes).toString("base64")}`,
|
|
205
|
+
bytes: bytes.byteLength,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Materialize only the two live Gmail actions whose file-ref contract is known.
|
|
210
|
+
* Remote URLs/data URIs remain byte-identical. Endpoint-run effects are already
|
|
211
|
+
* authorized against their original argument hash, so they may not introduce a
|
|
212
|
+
* local path that would change after authorization.
|
|
213
|
+
*/
|
|
214
|
+
export async function materializeIntegrationLocalPdfAttachments(toolName, args, options) {
|
|
215
|
+
if (!GMAIL_FILE_ATTACHMENT_TOOLS.has(toolName))
|
|
216
|
+
return args;
|
|
217
|
+
const attachments = args.attachments;
|
|
218
|
+
if (!Array.isArray(attachments) || attachments.length === 0)
|
|
219
|
+
return args;
|
|
220
|
+
const filenames = args.attachmentFilenames;
|
|
221
|
+
if (!Array.isArray(filenames) || filenames.length !== attachments.length) {
|
|
222
|
+
throw new IntegrationLocalAttachmentError();
|
|
223
|
+
}
|
|
224
|
+
let remainingBytes = INTEGRATION_LOCAL_PDF_MAX_BYTES;
|
|
225
|
+
const materialized = [];
|
|
226
|
+
for (let index = 0; index < attachments.length; index += 1) {
|
|
227
|
+
const attachment = attachments[index];
|
|
228
|
+
const filename = filenames[index];
|
|
229
|
+
if (typeof attachment !== "string" || typeof filename !== "string") {
|
|
230
|
+
throw new IntegrationLocalAttachmentError();
|
|
231
|
+
}
|
|
232
|
+
if (/^(?:https?:|data:)/i.test(attachment)) {
|
|
233
|
+
materialized.push(attachment);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (!options.allowLocal)
|
|
237
|
+
throw new IntegrationLocalAttachmentError();
|
|
238
|
+
const converted = await localPdfDataUri(attachment, filename, remainingBytes, options.env ?? process.env);
|
|
239
|
+
materialized.push(converted.dataUri);
|
|
240
|
+
remainingBytes -= converted.bytes;
|
|
241
|
+
}
|
|
242
|
+
return { ...args, attachments: materialized };
|
|
243
|
+
}
|
|
95
244
|
/**
|
|
96
245
|
* Mirror the home-workspace consistency invariant owned by
|
|
97
246
|
* `readMcpAgentServiceContext`. Customer profiles prove home with the customer
|
|
@@ -604,17 +753,20 @@ export async function integrationsCallTool(input = {}, actor, agentEffectId) {
|
|
|
604
753
|
return home.envelope;
|
|
605
754
|
try {
|
|
606
755
|
const endpointContext = isEndpointIntegrationContext(actor);
|
|
756
|
+
const callArguments = args
|
|
757
|
+
? await materializeIntegrationLocalPdfAttachments(tool.toolName, args, { allowLocal: !endpointContext })
|
|
758
|
+
: null;
|
|
607
759
|
const body = endpointContext
|
|
608
760
|
? {
|
|
609
761
|
...selector.selector,
|
|
610
762
|
toolName: tool.toolName,
|
|
611
|
-
...(
|
|
763
|
+
...(callArguments ? { arguments: callArguments } : {}),
|
|
612
764
|
...trustedIntegrationContextBody(actor, "DISPATCH"),
|
|
613
765
|
}
|
|
614
766
|
: {
|
|
615
767
|
...selector.selector,
|
|
616
768
|
toolName: tool.toolName,
|
|
617
|
-
...(
|
|
769
|
+
...(callArguments ? { arguments: callArguments } : {}),
|
|
618
770
|
...(typeof input.approvalId === "string" && input.approvalId
|
|
619
771
|
? { approvalId: input.approvalId }
|
|
620
772
|
: {}),
|
|
@@ -628,6 +780,9 @@ export async function integrationsCallTool(input = {}, actor, agentEffectId) {
|
|
|
628
780
|
return normalizeRouteResult(response);
|
|
629
781
|
}
|
|
630
782
|
catch (error) {
|
|
783
|
+
if (error instanceof IntegrationLocalAttachmentError) {
|
|
784
|
+
return localAttachmentRefusal();
|
|
785
|
+
}
|
|
631
786
|
return toolError("call_tool", error);
|
|
632
787
|
}
|
|
633
788
|
}
|