@sellable/mcp 0.1.901 → 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.
|
@@ -16,7 +16,7 @@ export type WaitForCliLoginInput = {
|
|
|
16
16
|
/** Test-only: override poll interval. Not exposed in inputSchema. */
|
|
17
17
|
pollIntervalMs?: number;
|
|
18
18
|
};
|
|
19
|
-
type CliLoginErrorType = "rate_limited" | "send_failed" | "expired" | "already_consumed" | "timeout" | "tool_timeout_guard" | "network_error" | "not_implemented";
|
|
19
|
+
type CliLoginErrorType = "rate_limited" | "send_failed" | "expired" | "already_consumed" | "timeout" | "tool_timeout_guard" | "network_error" | "managed_agent_service_credential" | "not_implemented";
|
|
20
20
|
type CliLoginErrorPayload = {
|
|
21
21
|
ok: false;
|
|
22
22
|
error: {
|
package/dist/tools/cli-login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { writeNewConfig } from "../auth.js";
|
|
1
|
+
import { getConfiguredCredentialIntent, writeNewConfig } from "../auth.js";
|
|
2
2
|
/**
|
|
3
3
|
* Sellable CLI login tools.
|
|
4
4
|
*
|
|
@@ -58,6 +58,13 @@ function resolveApiUrl() {
|
|
|
58
58
|
process.env.SELLABLE_BASE_URL ??
|
|
59
59
|
"https://app.sellable.dev");
|
|
60
60
|
}
|
|
61
|
+
function managedAgentAlreadyAuthenticated() {
|
|
62
|
+
return (process.env.SELLABLE_AGENT_RUNTIME === "1" &&
|
|
63
|
+
getConfiguredCredentialIntent() === "agent_service");
|
|
64
|
+
}
|
|
65
|
+
function managedAgentLoginRefusal() {
|
|
66
|
+
return errorPayload("managed_agent_service_credential", "This managed Agent is already authenticated.", "Use the connected integration directly. A browser login would replace the Agent's scoped service credential and is not allowed.");
|
|
67
|
+
}
|
|
61
68
|
export const startCliLoginToolDef = {
|
|
62
69
|
name: "start_cli_login",
|
|
63
70
|
description: "Start the CLI login handoff. Triggers a magic-link email to the supplied email address. " +
|
|
@@ -97,6 +104,9 @@ export const waitForCliLoginToolDef = {
|
|
|
97
104
|
},
|
|
98
105
|
};
|
|
99
106
|
export async function handleStartCliLogin(args) {
|
|
107
|
+
if (managedAgentAlreadyAuthenticated()) {
|
|
108
|
+
return managedAgentLoginRefusal();
|
|
109
|
+
}
|
|
100
110
|
const apiUrl = resolveApiUrl();
|
|
101
111
|
let res;
|
|
102
112
|
try {
|
|
@@ -174,6 +184,9 @@ export async function handleWaitForCliLogin(args) {
|
|
|
174
184
|
continue;
|
|
175
185
|
}
|
|
176
186
|
if (body.status === "ready") {
|
|
187
|
+
if (managedAgentAlreadyAuthenticated()) {
|
|
188
|
+
return managedAgentLoginRefusal();
|
|
189
|
+
}
|
|
177
190
|
const { configPath } = writeNewConfig({
|
|
178
191
|
token: body.token,
|
|
179
192
|
activeWorkspaceId: body.activeWorkspaceId,
|
|
@@ -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
|
|
@@ -364,6 +513,9 @@ export function toolError(action, error) {
|
|
|
364
513
|
catch {
|
|
365
514
|
// Plain-text body; fall through to the generic shape below.
|
|
366
515
|
}
|
|
516
|
+
const structuredRouteRefusal = typeof forwarded.error === "string" ||
|
|
517
|
+
typeof forwarded.outcome === "string" ||
|
|
518
|
+
typeof forwarded.attribution === "string";
|
|
367
519
|
return envelope({
|
|
368
520
|
ok: false,
|
|
369
521
|
outcome: typeof forwarded.outcome === "string" ? forwarded.outcome : "refused",
|
|
@@ -377,7 +529,9 @@ export function toolError(action, error) {
|
|
|
377
529
|
: `integration_${action}_failed`,
|
|
378
530
|
guidance: typeof forwarded.guidance === "string"
|
|
379
531
|
? forwarded.guidance
|
|
380
|
-
:
|
|
532
|
+
: structuredRouteRefusal
|
|
533
|
+
? null
|
|
534
|
+
: (error.guidance ?? null),
|
|
381
535
|
result: forwarded.detail ?? null,
|
|
382
536
|
});
|
|
383
537
|
}
|
|
@@ -599,17 +753,20 @@ export async function integrationsCallTool(input = {}, actor, agentEffectId) {
|
|
|
599
753
|
return home.envelope;
|
|
600
754
|
try {
|
|
601
755
|
const endpointContext = isEndpointIntegrationContext(actor);
|
|
756
|
+
const callArguments = args
|
|
757
|
+
? await materializeIntegrationLocalPdfAttachments(tool.toolName, args, { allowLocal: !endpointContext })
|
|
758
|
+
: null;
|
|
602
759
|
const body = endpointContext
|
|
603
760
|
? {
|
|
604
761
|
...selector.selector,
|
|
605
762
|
toolName: tool.toolName,
|
|
606
|
-
...(
|
|
763
|
+
...(callArguments ? { arguments: callArguments } : {}),
|
|
607
764
|
...trustedIntegrationContextBody(actor, "DISPATCH"),
|
|
608
765
|
}
|
|
609
766
|
: {
|
|
610
767
|
...selector.selector,
|
|
611
768
|
toolName: tool.toolName,
|
|
612
|
-
...(
|
|
769
|
+
...(callArguments ? { arguments: callArguments } : {}),
|
|
613
770
|
...(typeof input.approvalId === "string" && input.approvalId
|
|
614
771
|
? { approvalId: input.approvalId }
|
|
615
772
|
: {}),
|
|
@@ -623,6 +780,9 @@ export async function integrationsCallTool(input = {}, actor, agentEffectId) {
|
|
|
623
780
|
return normalizeRouteResult(response);
|
|
624
781
|
}
|
|
625
782
|
catch (error) {
|
|
783
|
+
if (error instanceof IntegrationLocalAttachmentError) {
|
|
784
|
+
return localAttachmentRefusal();
|
|
785
|
+
}
|
|
626
786
|
return toolError("call_tool", error);
|
|
627
787
|
}
|
|
628
788
|
}
|