@sellable/mcp 0.1.922 → 0.1.923-wip.managedauth.20260911.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/dist/agent-service-auth.d.ts +2 -2
- package/dist/agent-service-auth.js +3 -2
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +50 -3
- package/dist/managed-agent-auth.d.ts +20 -0
- package/dist/managed-agent-auth.js +72 -0
- package/dist/server.js +4 -1
- package/dist/tools/cli-login.js +2 -4
- package/package.json +1 -1
|
@@ -57,7 +57,7 @@ export type McpCredentialResolution = {
|
|
|
57
57
|
} | {
|
|
58
58
|
mode: "invalid";
|
|
59
59
|
principal: null;
|
|
60
|
-
code: "invalid_agent_service_context";
|
|
60
|
+
code: "invalid_agent_service_context" | "managed_agent_auth_unavailable";
|
|
61
61
|
};
|
|
62
62
|
export declare function resolveMcpCredentialIntent(input: {
|
|
63
63
|
env?: McpAgentEnvironment;
|
|
@@ -104,7 +104,7 @@ export declare function createMcpCredentialBoundary<T extends {
|
|
|
104
104
|
authorization: {
|
|
105
105
|
allowed: boolean;
|
|
106
106
|
decision: string;
|
|
107
|
-
code: "invalid_agent_service_context";
|
|
107
|
+
code: "invalid_agent_service_context" | "managed_agent_auth_unavailable";
|
|
108
108
|
};
|
|
109
109
|
context?: undefined;
|
|
110
110
|
} | {
|
|
@@ -182,11 +182,12 @@ export function resolveMcpCredentialIntent(input) {
|
|
|
182
182
|
code: "invalid_agent_service_context",
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
|
-
catch {
|
|
185
|
+
catch (error) {
|
|
186
186
|
return {
|
|
187
187
|
mode: "invalid",
|
|
188
188
|
principal: null,
|
|
189
|
-
code: "
|
|
189
|
+
code: error instanceof Error && "code" in error && error.code === "managed_agent_auth_unavailable"
|
|
190
|
+
? "managed_agent_auth_unavailable" : "invalid_agent_service_context",
|
|
190
191
|
};
|
|
191
192
|
}
|
|
192
193
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { ManagedAgentAuthenticationError } from "./managed-agent-auth.js";
|
|
1
2
|
export interface SellableEnvConfig {
|
|
2
3
|
token: string;
|
|
3
4
|
activeWorkspaceId?: string;
|
|
@@ -35,6 +36,8 @@ export type SkillState = {
|
|
|
35
36
|
engage?: EngageSkillState;
|
|
36
37
|
};
|
|
37
38
|
export declare const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
39
|
+
/** Runtime identity must not depend on the mutable file it is protecting. */
|
|
40
|
+
export declare function isManagedAgentRuntime(): boolean;
|
|
38
41
|
/**
|
|
39
42
|
* Set which config file to use. Call before getConfig().
|
|
40
43
|
* - "sellable.json" (default) - production
|
package/dist/auth.js
CHANGED
|
@@ -4,12 +4,21 @@ import * as os from "os";
|
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { readMcpAgentServiceContext } from "./agent-service-auth.js";
|
|
6
6
|
import { getAgentScope } from "./agent-scope.js";
|
|
7
|
+
import { ManagedAgentAuthenticationError, readProtectedManagedAuth } from "./managed-agent-auth.js";
|
|
8
|
+
export { ManagedAgentAuthenticationError } from "./managed-agent-auth.js";
|
|
7
9
|
export const DEFAULT_API_URL = "https://app.sellable.dev";
|
|
8
10
|
const WORKSPACE_LOCK_ENV = "SELLABLE_LOCK_WORKSPACE_ID";
|
|
9
11
|
const REQUIRE_WORKSPACE_LOCK_ENV = "SELLABLE_REQUIRE_WORKSPACE_LOCK";
|
|
10
12
|
const AGENT_SERVICE_CREDENTIAL_FILE_ENV = "SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE";
|
|
11
13
|
const AGENT_RUNTIME_SECRET_ROOT_ENV = "SELLABLE_AGENT_RUNTIME_SECRET_ROOT";
|
|
12
14
|
const AGENT_PROFILE_ID_ENV = "SELLABLE_AGENT_PROFILE_ID";
|
|
15
|
+
/** Runtime identity must not depend on the mutable file it is protecting. */
|
|
16
|
+
export function isManagedAgentRuntime() {
|
|
17
|
+
return (process.env.SELLABLE_AGENT_RUNTIME === "1" ||
|
|
18
|
+
["ADMIN", "CUSTOMER"].includes(process.env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND ?? "") ||
|
|
19
|
+
Boolean(process.env[AGENT_SERVICE_CREDENTIAL_FILE_ENV]?.trim()) ||
|
|
20
|
+
Boolean(process.env.SELLABLE_AGENT_MANAGED_AUTH_PATH?.trim()));
|
|
21
|
+
}
|
|
13
22
|
// No caching — re-read config on every call so token/workspace changes
|
|
14
23
|
// made by the LLM (editing sellable.json) take effect immediately
|
|
15
24
|
// without requiring an MCP server restart.
|
|
@@ -201,6 +210,36 @@ export function resolveWorkspaceIdForRequest(requestedWorkspaceId, action = "Sel
|
|
|
201
210
|
return lockedWorkspaceId;
|
|
202
211
|
}
|
|
203
212
|
export function getConfig() {
|
|
213
|
+
const managed = readProtectedManagedAuth();
|
|
214
|
+
if (managed) {
|
|
215
|
+
// Mutable config contains preferences only. A stale copy, missing file, or
|
|
216
|
+
// invalid JSON must not replace the service token, API origin, or identity.
|
|
217
|
+
let preferences = {};
|
|
218
|
+
try {
|
|
219
|
+
const raw = JSON.parse(fs.readFileSync(getConfigPath(), "utf8"));
|
|
220
|
+
const selected = raw?.activeEnv && raw?.environments
|
|
221
|
+
? raw.environments[raw.activeEnv] : raw;
|
|
222
|
+
if (selected && typeof selected === "object")
|
|
223
|
+
preferences = selected;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// The supervisor binding is sufficient to keep the home workspace usable.
|
|
227
|
+
}
|
|
228
|
+
const crossWorkspace = getAgentScope() === "CROSS_WORKSPACE";
|
|
229
|
+
return {
|
|
230
|
+
token: managed.token,
|
|
231
|
+
apiUrl: managed.apiUrl,
|
|
232
|
+
workspaceId: managed.agent.workspaceId,
|
|
233
|
+
activeWorkspaceId: crossWorkspace
|
|
234
|
+
? normalizeWorkspaceId(typeof preferences.activeWorkspaceId === "string" ? preferences.activeWorkspaceId : null) || managed.agent.workspaceId
|
|
235
|
+
: managed.agent.workspaceId,
|
|
236
|
+
...(crossWorkspace && typeof preferences.activeWorkspaceName === "string"
|
|
237
|
+
? { activeWorkspaceName: preferences.activeWorkspaceName } : {}),
|
|
238
|
+
...(preferences.skillState && typeof preferences.skillState === "object"
|
|
239
|
+
? { skillState: preferences.skillState } : {}),
|
|
240
|
+
credentialKind: "agent_service",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
204
243
|
// Config cache removed 2026-05-04: we always re-read from disk so
|
|
205
244
|
// workspace switches (set_active_workspace) take effect immediately
|
|
206
245
|
// for ALL downstream tools (list_senders, fetch_*, etc.) without
|
|
@@ -209,6 +248,8 @@ export function getConfig() {
|
|
|
209
248
|
const configPathCandidates = getConfigPathCandidates();
|
|
210
249
|
const configPath = getConfigPath();
|
|
211
250
|
if (!fs.existsSync(configPath)) {
|
|
251
|
+
if (isManagedAgentRuntime())
|
|
252
|
+
throw new ManagedAgentAuthenticationError();
|
|
212
253
|
throw new Error("Sellable not configured.\n\n" +
|
|
213
254
|
"Run a Sellable workflow to start first-run login, for example /sellable:create-campaign in Claude Code or $sellable:create-campaign in Codex.\n\n" +
|
|
214
255
|
"If the browser login page shows a manual fallback command, paste it in your terminal:\n" +
|
|
@@ -250,6 +291,10 @@ export function getConfig() {
|
|
|
250
291
|
throw new Error(`Missing 'token' in ${configFileName}`);
|
|
251
292
|
}
|
|
252
293
|
if (config.token.startsWith("skt_")) {
|
|
294
|
+
// The 2026-09-11 sales cron overwrote Admin's config with a legacy
|
|
295
|
+
// personal token using cp, bypassing writeNewConfig's login guard.
|
|
296
|
+
if (isManagedAgentRuntime())
|
|
297
|
+
throw new ManagedAgentAuthenticationError();
|
|
253
298
|
config.credentialKind = "human";
|
|
254
299
|
}
|
|
255
300
|
else if (config.token.startsWith("sat_")) {
|
|
@@ -280,6 +325,8 @@ export function getConfig() {
|
|
|
280
325
|
}
|
|
281
326
|
catch (error) {
|
|
282
327
|
if (error instanceof SyntaxError) {
|
|
328
|
+
if (isManagedAgentRuntime())
|
|
329
|
+
throw new ManagedAgentAuthenticationError();
|
|
283
330
|
throw new Error(`Invalid JSON in ${configPath}: ${error.message}`);
|
|
284
331
|
}
|
|
285
332
|
throw error;
|
|
@@ -287,6 +334,8 @@ export function getConfig() {
|
|
|
287
334
|
}
|
|
288
335
|
/** Reads only the credential class; never returns or validates credential bytes. */
|
|
289
336
|
export function getConfiguredCredentialIntent() {
|
|
337
|
+
if (isManagedAgentRuntime())
|
|
338
|
+
return "agent_service";
|
|
290
339
|
const configPath = getConfigPath();
|
|
291
340
|
if (!fs.existsSync(configPath))
|
|
292
341
|
return "unconfigured";
|
|
@@ -404,9 +453,7 @@ function writeRawConfigFile(configPath, raw) {
|
|
|
404
453
|
* new config without an MCP server restart.
|
|
405
454
|
*/
|
|
406
455
|
export function writeNewConfig(opts) {
|
|
407
|
-
if (
|
|
408
|
-
(process.env[AGENT_SERVICE_CREDENTIAL_FILE_ENV]?.trim() ||
|
|
409
|
-
getConfiguredCredentialIntent() === "agent_service")) {
|
|
456
|
+
if (isManagedAgentRuntime()) {
|
|
410
457
|
throw new Error("Managed Agent service credentials cannot be replaced by human login");
|
|
411
458
|
}
|
|
412
459
|
resolveWorkspaceIdForRequest(opts.activeWorkspaceId, "write Sellable auth config");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type McpAgentServiceContext } from "./agent-service-auth.js";
|
|
2
|
+
export interface ProtectedManagedAuth {
|
|
3
|
+
schemaVersion: "sellable-managed-auth/v1";
|
|
4
|
+
profileId: string;
|
|
5
|
+
runtimeId: string;
|
|
6
|
+
runtimeGeneration: number;
|
|
7
|
+
agentKind: "ADMIN" | "CUSTOMER";
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
token: string;
|
|
10
|
+
agent: Omit<McpAgentServiceContext, "kind" | "credentialId">;
|
|
11
|
+
}
|
|
12
|
+
export declare class ManagedAgentAuthenticationError extends Error {
|
|
13
|
+
readonly code = "managed_agent_auth_unavailable";
|
|
14
|
+
constructor();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The root supervisor owns this binding and every ancestor below /run.
|
|
18
|
+
* Profile config is deliberately not an input to credential or origin selection.
|
|
19
|
+
*/
|
|
20
|
+
export declare function readProtectedManagedAuth(env?: NodeJS.ProcessEnv): ProtectedManagedAuth | null;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readMcpAgentServiceContext } from "./agent-service-auth.js";
|
|
3
|
+
const AUTH_ROOT = "/run/sellable-managed-auth";
|
|
4
|
+
const MAX_BINDING_BYTES = 32768;
|
|
5
|
+
export class ManagedAgentAuthenticationError extends Error {
|
|
6
|
+
code = "managed_agent_auth_unavailable";
|
|
7
|
+
constructor() {
|
|
8
|
+
super("managed_agent_auth_unavailable: The runtime supervisor must restore " +
|
|
9
|
+
"managed authentication. Do not copy a personal config or start browser login.");
|
|
10
|
+
this.name = "ManagedAgentAuthenticationError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The root supervisor owns this binding and every ancestor below /run.
|
|
15
|
+
* Profile config is deliberately not an input to credential or origin selection.
|
|
16
|
+
*/
|
|
17
|
+
export function readProtectedManagedAuth(env = process.env) {
|
|
18
|
+
const path = env.SELLABLE_AGENT_MANAGED_AUTH_PATH?.trim();
|
|
19
|
+
if (!path)
|
|
20
|
+
return null;
|
|
21
|
+
let fd;
|
|
22
|
+
try {
|
|
23
|
+
const match = path.match(/^\/run\/sellable-managed-auth\/([A-Za-z0-9_-]{1,128})\/identity\.json$/);
|
|
24
|
+
if (!match)
|
|
25
|
+
throw new ManagedAgentAuthenticationError();
|
|
26
|
+
for (const directory of ["/run", AUTH_ROOT, `${AUTH_ROOT}/${match[1]}`]) {
|
|
27
|
+
const entry = lstatSync(directory);
|
|
28
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || entry.uid !== 0 || (entry.mode & 0o022) !== 0) {
|
|
29
|
+
throw new ManagedAgentAuthenticationError();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
33
|
+
const entry = fstatSync(fd);
|
|
34
|
+
if (!entry.isFile() || entry.uid !== 0 || (entry.mode & 0o777) !== 0o440
|
|
35
|
+
|| entry.size < 1 || entry.size > MAX_BINDING_BYTES) {
|
|
36
|
+
throw new ManagedAgentAuthenticationError();
|
|
37
|
+
}
|
|
38
|
+
const value = JSON.parse(readFileSync(fd, "utf8"));
|
|
39
|
+
if (value?.schemaVersion !== "sellable-managed-auth/v1" || value.profileId !== match[1]
|
|
40
|
+
|| !/^fly-runtime_[a-f0-9]{32}$/.test(value.runtimeId)
|
|
41
|
+
|| !Number.isSafeInteger(value.runtimeGeneration) || value.runtimeGeneration < 1
|
|
42
|
+
|| !["ADMIN", "CUSTOMER"].includes(value.agentKind)
|
|
43
|
+
|| typeof value.token !== "string" || !value.token.startsWith("sat_")) {
|
|
44
|
+
throw new ManagedAgentAuthenticationError();
|
|
45
|
+
}
|
|
46
|
+
const url = new URL(value.apiUrl);
|
|
47
|
+
const testHttp = env.SELLABLE_AGENT_ALLOW_TEST_OIDC === "1" && url.protocol === "http:"
|
|
48
|
+
&& ["127.0.0.1", "localhost", "host.docker.internal"].includes(url.hostname);
|
|
49
|
+
if ((url.protocol !== "https:" && !testHttp) || url.origin !== value.apiUrl || url.username || url.password) {
|
|
50
|
+
throw new ManagedAgentAuthenticationError();
|
|
51
|
+
}
|
|
52
|
+
const context = readMcpAgentServiceContext(env, value.token);
|
|
53
|
+
if (!context || !value.agent || value.agent.workspaceId !== context.workspaceId
|
|
54
|
+
|| value.agent.agentId !== context.agentId || value.agent.generation !== context.generation
|
|
55
|
+
|| value.agent.policyHash !== context.policyHash || !Array.isArray(value.agent.selectedChannelIds)
|
|
56
|
+
|| JSON.stringify([...value.agent.selectedChannelIds].sort()) !== JSON.stringify(context.selectedChannelIds)
|
|
57
|
+
|| String(value.runtimeGeneration) !== env.SELLABLE_AGENT_RUNTIME_GENERATION
|
|
58
|
+
|| (env.SELLABLE_RUNTIME_ID && value.runtimeId !== env.SELLABLE_RUNTIME_ID)
|
|
59
|
+
|| (env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND && value.agentKind !== env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND)) {
|
|
60
|
+
throw new ManagedAgentAuthenticationError();
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Never include JSON bytes, credentials, or attacker-controlled parse errors.
|
|
66
|
+
throw new ManagedAgentAuthenticationError();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (fd !== undefined)
|
|
70
|
+
closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -196,8 +196,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
196
196
|
type: "text",
|
|
197
197
|
text: JSON.stringify({
|
|
198
198
|
ok: false,
|
|
199
|
-
error:
|
|
199
|
+
error: boundary.authorization.code,
|
|
200
200
|
decision: "DENY",
|
|
201
|
+
...(boundary.authorization.code === "managed_agent_auth_unavailable"
|
|
202
|
+
? { message: "Managed authentication is unavailable. The runtime supervisor must restore it; do not copy personal credentials, start browser login, or retry a Calendar mutation automatically." }
|
|
203
|
+
: {}),
|
|
201
204
|
}),
|
|
202
205
|
},
|
|
203
206
|
],
|
package/dist/tools/cli-login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isManagedAgentRuntime, writeNewConfig } from "../auth.js";
|
|
2
2
|
/**
|
|
3
3
|
* Sellable CLI login tools.
|
|
4
4
|
*
|
|
@@ -59,9 +59,7 @@ function resolveApiUrl() {
|
|
|
59
59
|
"https://app.sellable.dev");
|
|
60
60
|
}
|
|
61
61
|
function managedAgentAlreadyAuthenticated() {
|
|
62
|
-
return (
|
|
63
|
-
(Boolean(process.env.SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE?.trim()) ||
|
|
64
|
-
getConfiguredCredentialIntent() === "agent_service"));
|
|
62
|
+
return isManagedAgentRuntime();
|
|
65
63
|
}
|
|
66
64
|
function managedAgentLoginRefusal() {
|
|
67
65
|
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.");
|
package/package.json
CHANGED