@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.
@@ -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: "invalid_agent_service_context",
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 (process.env.SELLABLE_AGENT_RUNTIME === "1" &&
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: "invalid_agent_service_context",
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
  ],
@@ -1,4 +1,4 @@
1
- import { getConfiguredCredentialIntent, writeNewConfig } from "../auth.js";
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 (process.env.SELLABLE_AGENT_RUNTIME === "1" &&
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.922",
3
+ "version": "0.1.923-wip.managedauth.20260911.1",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",