@sellable/mcp 0.1.619-wip.121.2 → 0.1.619

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.
@@ -0,0 +1,358 @@
1
+ export const DEFAULT_AGENT_TOOL_CATALOGUE = [
2
+ {
3
+ tool: "get_company_info",
4
+ version: "v1",
5
+ decision: "AUTO",
6
+ method: "GET",
7
+ route: "/api/v3/reply-management/company-info",
8
+ handler: "getCompanyInfoMarkdown",
9
+ workspaceBinding: "workspaceId",
10
+ },
11
+ {
12
+ tool: "set_company_info",
13
+ version: "v1",
14
+ decision: "REQUESTER_CONFIRM",
15
+ method: "PUT",
16
+ route: "/api/v3/reply-management/company-info",
17
+ handler: "saveCompanyInfoMarkdown",
18
+ workspaceBinding: "workspaceId",
19
+ },
20
+ ];
21
+ export function compileAgentToolPolicy(entries = DEFAULT_AGENT_TOOL_CATALOGUE) {
22
+ const compiledEntries = [...entries]
23
+ .map((entry) => ({ ...entry }))
24
+ .sort((left, right) => left.tool.localeCompare(right.tool));
25
+ const toolNames = new Set(compiledEntries.map((entry) => entry.tool));
26
+ if (toolNames.size !== compiledEntries.length) {
27
+ throw new Error("Agent tool policy contains duplicate tool names");
28
+ }
29
+ return {
30
+ policyHash: portableSha256(JSON.stringify(canonicalize(compiledEntries))),
31
+ entries: Object.freeze(compiledEntries.map((entry) => Object.freeze(entry))),
32
+ };
33
+ }
34
+ export function decideAgentTool(compiled, tool) {
35
+ const entry = compiled.entries.find((candidate) => candidate.tool === tool);
36
+ if (entry)
37
+ return { ...entry, allowed: entry.decision !== "DENY" };
38
+ return {
39
+ tool,
40
+ version: "unknown",
41
+ decision: "DENY",
42
+ method: "GET",
43
+ route: "",
44
+ handler: "",
45
+ workspaceBinding: "",
46
+ allowed: false,
47
+ };
48
+ }
49
+ export function listToolsForAgent(compiled, definitions) {
50
+ const allowed = new Set(compiled.entries
51
+ .filter((entry) => entry.decision !== "DENY")
52
+ .map((entry) => entry.tool));
53
+ return definitions.filter((definition) => allowed.has(definition.name));
54
+ }
55
+ function canonicalize(value) {
56
+ if (Array.isArray(value))
57
+ return value.map(canonicalize);
58
+ if (value && typeof value === "object") {
59
+ return Object.fromEntries(Object.entries(value)
60
+ .sort(([left], [right]) => left.localeCompare(right))
61
+ .map(([key, item]) => [key, canonicalize(item)]));
62
+ }
63
+ return value;
64
+ }
65
+ const SHA256_CONSTANTS = [
66
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b,
67
+ 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01,
68
+ 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7,
69
+ 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
70
+ 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152,
71
+ 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
72
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
73
+ 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
74
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
75
+ 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08,
76
+ 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f,
77
+ 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
78
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
79
+ ];
80
+ function rotateRight(value, bits) {
81
+ return (value >>> bits) | (value << (32 - bits));
82
+ }
83
+ export function portableSha256(value) {
84
+ const bytes = new TextEncoder().encode(value);
85
+ const bitLength = bytes.length * 8;
86
+ const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
87
+ const padded = new Uint8Array(paddedLength);
88
+ padded.set(bytes);
89
+ padded[bytes.length] = 0x80;
90
+ const view = new DataView(padded.buffer);
91
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000));
92
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
93
+ const hash = [
94
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f,
95
+ 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
96
+ ];
97
+ const words = new Uint32Array(64);
98
+ for (let offset = 0; offset < paddedLength; offset += 64) {
99
+ for (let index = 0; index < 16; index += 1) {
100
+ words[index] = view.getUint32(offset + index * 4);
101
+ }
102
+ for (let index = 16; index < 64; index += 1) {
103
+ const left = words[index - 15];
104
+ const right = words[index - 2];
105
+ const sigma0 = rotateRight(left, 7) ^ rotateRight(left, 18) ^ (left >>> 3);
106
+ const sigma1 = rotateRight(right, 17) ^ rotateRight(right, 19) ^ (right >>> 10);
107
+ words[index] =
108
+ (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
109
+ }
110
+ let [a, b, c, d, e, f, g, h] = hash;
111
+ for (let index = 0; index < 64; index += 1) {
112
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
113
+ const choice = (e & f) ^ (~e & g);
114
+ const temp1 = (h + sum1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
115
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
116
+ const majority = (a & b) ^ (a & c) ^ (b & c);
117
+ const temp2 = (sum0 + majority) >>> 0;
118
+ h = g;
119
+ g = f;
120
+ f = e;
121
+ e = (d + temp1) >>> 0;
122
+ d = c;
123
+ c = b;
124
+ b = a;
125
+ a = (temp1 + temp2) >>> 0;
126
+ }
127
+ hash[0] = (hash[0] + a) >>> 0;
128
+ hash[1] = (hash[1] + b) >>> 0;
129
+ hash[2] = (hash[2] + c) >>> 0;
130
+ hash[3] = (hash[3] + d) >>> 0;
131
+ hash[4] = (hash[4] + e) >>> 0;
132
+ hash[5] = (hash[5] + f) >>> 0;
133
+ hash[6] = (hash[6] + g) >>> 0;
134
+ hash[7] = (hash[7] + h) >>> 0;
135
+ }
136
+ return hash.map((word) => word.toString(16).padStart(8, "0")).join("");
137
+ }
138
+ export function hashApprovalArguments(args) {
139
+ return portableSha256(JSON.stringify(canonicalize(args)));
140
+ }
141
+ export function canonicalAgentToolArguments(tool, args) {
142
+ if (tool !== "set_company_info" || !args || typeof args !== "object") {
143
+ return args;
144
+ }
145
+ const input = args;
146
+ const markdown = typeof input.companyInfoMarkdown === "string"
147
+ ? input.companyInfoMarkdown.trim()
148
+ ? input.companyInfoMarkdown
149
+ : null
150
+ : input.companyInfoMarkdown === null
151
+ ? null
152
+ : null;
153
+ return input.forceOverwrite === true
154
+ ? { companyInfoMarkdown: markdown, forceOverwrite: true }
155
+ : {
156
+ companyInfoMarkdown: markdown,
157
+ expectedUpdatedAt: typeof input.expectedUpdatedAt === "string" ||
158
+ input.expectedUpdatedAt === null
159
+ ? input.expectedUpdatedAt
160
+ : null,
161
+ };
162
+ }
163
+ export function hashAgentToolArguments(tool, args) {
164
+ return hashApprovalArguments(canonicalAgentToolArguments(tool, args));
165
+ }
166
+ export function deriveAgentApprovalRequestId(input) {
167
+ return `saa_${hashApprovalArguments({
168
+ workspaceId: input.workspaceId,
169
+ agentId: input.agentId,
170
+ requesterId: input.requesterId,
171
+ channelId: input.channelId,
172
+ providerRequestId: input.providerRequestId,
173
+ tool: input.tool,
174
+ toolVersion: input.toolVersion,
175
+ argumentHash: hashAgentToolArguments(input.tool, input.arguments),
176
+ policyHash: input.policyHash,
177
+ credentialGeneration: input.credentialGeneration,
178
+ decision: input.decision,
179
+ })}`;
180
+ }
181
+ export function validateApprovalBinding(input) {
182
+ const { approval, expected } = input;
183
+ if (!approval)
184
+ return { ok: false, code: "approval_required" };
185
+ if (approval.consumedAt)
186
+ return { ok: false, code: "approval_consumed" };
187
+ if (approval.revokedAt)
188
+ return { ok: false, code: "approval_revoked" };
189
+ if (approval.expiresAt.getTime() <= (input.now ?? new Date()).getTime()) {
190
+ return { ok: false, code: "approval_expired" };
191
+ }
192
+ for (const key of Object.keys(expected)) {
193
+ if (approval[key] !== expected[key]) {
194
+ return { ok: false, code: `approval_${String(key)}_mismatch` };
195
+ }
196
+ }
197
+ return { ok: true, code: "approved" };
198
+ }
199
+ export function createAgentMcpAuthorizationHandlers(input) {
200
+ const policy = input.policy ?? compileAgentToolPolicy();
201
+ return {
202
+ listTools(context) {
203
+ if (!context.principal)
204
+ return input.allTools;
205
+ if (context.principal.policyHash !== policy.policyHash ||
206
+ !Number.isSafeInteger(context.principal.generation) ||
207
+ context.principal.generation < 1) {
208
+ return [];
209
+ }
210
+ return listToolsForAgent(policy, input.allTools);
211
+ },
212
+ async authorizeCall(call) {
213
+ const principal = call.context.principal;
214
+ if (!principal) {
215
+ return { allowed: false, decision: "DENY", code: "agent_principal_required" };
216
+ }
217
+ if (principal.policyHash !== policy.policyHash) {
218
+ return { allowed: false, decision: "DENY", code: "policy_hash_mismatch" };
219
+ }
220
+ if (!Number.isSafeInteger(principal.generation) || principal.generation < 1) {
221
+ return { allowed: false, decision: "DENY", code: "credential_generation_invalid" };
222
+ }
223
+ const decision = decideAgentTool(policy, call.name);
224
+ if (!decision.allowed) {
225
+ return { allowed: false, decision: "DENY", code: "tool_denied" };
226
+ }
227
+ const policyEntry = {
228
+ tool: decision.tool,
229
+ version: decision.version,
230
+ decision: decision.decision,
231
+ method: decision.method,
232
+ route: decision.route,
233
+ handler: decision.handler,
234
+ workspaceBinding: decision.workspaceBinding,
235
+ };
236
+ if (decision.decision === "AUTO") {
237
+ return { allowed: true, decision: "AUTO", policy: policyEntry };
238
+ }
239
+ if (decision.decision !== "REQUESTER_CONFIRM" &&
240
+ decision.decision !== "WORKSPACE_APPROVER") {
241
+ return { allowed: false, decision: "DENY", code: "tool_denied" };
242
+ }
243
+ const approvalDecision = decision.decision;
244
+ if (!input.approvalPort) {
245
+ return {
246
+ allowed: false,
247
+ decision: approvalDecision,
248
+ code: "approval_port_unavailable",
249
+ };
250
+ }
251
+ const requesterId = call.context.requesterId?.trim();
252
+ const channelId = call.context.channelId?.trim();
253
+ const providerRequestId = call.context.providerRequestId?.trim();
254
+ const approvalId = call.context.approvalId?.trim();
255
+ if (!requesterId || !channelId || !providerRequestId) {
256
+ return {
257
+ allowed: false,
258
+ decision: approvalDecision,
259
+ code: "approval_context_required",
260
+ };
261
+ }
262
+ const expectedApprovalId = deriveAgentApprovalRequestId({
263
+ workspaceId: principal.workspaceId,
264
+ agentId: principal.agentId,
265
+ requesterId,
266
+ channelId,
267
+ providerRequestId,
268
+ tool: decision.tool,
269
+ toolVersion: decision.version,
270
+ arguments: call.args,
271
+ policyHash: principal.policyHash,
272
+ credentialGeneration: principal.generation,
273
+ decision: approvalDecision,
274
+ });
275
+ if (!approvalId) {
276
+ if (!input.approvalPort.requestApproval) {
277
+ return {
278
+ allowed: false,
279
+ decision: approvalDecision,
280
+ code: "approval_port_unavailable",
281
+ };
282
+ }
283
+ const approval = await input.approvalPort.requestApproval({
284
+ requesterId,
285
+ channelId,
286
+ providerRequestId,
287
+ tool: decision.tool,
288
+ arguments: call.args,
289
+ });
290
+ if (approval.approvalId !== expectedApprovalId ||
291
+ approval.workspaceId !== principal.workspaceId ||
292
+ approval.agentId !== principal.agentId ||
293
+ approval.requesterId !== requesterId ||
294
+ approval.channelId !== channelId ||
295
+ approval.providerRequestId !== providerRequestId ||
296
+ approval.tool !== decision.tool ||
297
+ approval.toolVersion !== decision.version ||
298
+ approval.decision !== approvalDecision) {
299
+ return {
300
+ allowed: false,
301
+ decision: approvalDecision,
302
+ code: "approval_mismatch",
303
+ };
304
+ }
305
+ return {
306
+ allowed: false,
307
+ decision: approvalDecision,
308
+ code: "approval_required",
309
+ approval,
310
+ };
311
+ }
312
+ if (approvalId !== expectedApprovalId) {
313
+ return {
314
+ allowed: false,
315
+ decision: approvalDecision,
316
+ code: "approval_mismatch",
317
+ };
318
+ }
319
+ const consumed = await input.approvalPort.consumeApprovalAndStartEffect({
320
+ approvalId,
321
+ requesterId,
322
+ channelId,
323
+ providerRequestId,
324
+ argumentHash: hashAgentToolArguments(decision.tool, call.args),
325
+ workspaceId: principal.workspaceId,
326
+ agentId: principal.agentId,
327
+ tool: decision.tool,
328
+ toolVersion: decision.version,
329
+ policyHash: principal.policyHash,
330
+ credentialGeneration: principal.generation,
331
+ decision: approvalDecision,
332
+ now: input.now?.() ?? new Date(),
333
+ arguments: call.args,
334
+ });
335
+ if (consumed.status === "AUTHORIZED") {
336
+ return {
337
+ allowed: true,
338
+ decision: approvalDecision,
339
+ policy: policyEntry,
340
+ effectId: consumed.effectId,
341
+ };
342
+ }
343
+ const codeByStatus = {
344
+ MISSING: "approval_required",
345
+ MISMATCH: "approval_mismatch",
346
+ EXPIRED: "approval_expired",
347
+ REVOKED: "approval_revoked",
348
+ CONSUMED: "approval_consumed",
349
+ };
350
+ return {
351
+ allowed: false,
352
+ decision: approvalDecision,
353
+ code: codeByStatus[consumed.status],
354
+ };
355
+ },
356
+ policy,
357
+ };
358
+ }
package/dist/api.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface DownloadToFileResult {
10
10
  }
11
11
  export interface ApiRequestOptions {
12
12
  workspaceId?: string | null;
13
+ agentEffectId?: string;
13
14
  }
14
15
  export declare class SellableApiError extends Error {
15
16
  status: number;
package/dist/api.js CHANGED
@@ -54,6 +54,9 @@ export class SellableApi {
54
54
  "Content-Type": "application/json",
55
55
  Authorization: `Bearer ${config.token}`,
56
56
  ...(workspaceId ? { "x-workspaceid": workspaceId } : {}),
57
+ ...(options?.agentEffectId
58
+ ? { "x-sellable-agent-effect-id": options.agentEffectId }
59
+ : {}),
57
60
  },
58
61
  ...(body && { body: JSON.stringify(body) }),
59
62
  });
package/dist/auth.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface SellableMultiEnvConfig {
14
14
  export type SellableConfig = SellableEnvConfig & {
15
15
  /** Name of the active environment (e.g. "local", "prod"). */
16
16
  activeEnvName?: string;
17
+ /** Credential class controls MCP discovery/invocation and API semantics. */
18
+ credentialKind?: "human" | "agent_service";
17
19
  };
18
20
  export type EngageWorkspaceState = {
19
21
  lastSenderId?: string;
@@ -47,6 +49,8 @@ export declare function assertWorkspaceLockSatisfied(action?: string): void;
47
49
  export declare function getEffectiveConfiguredWorkspaceId(config: Pick<SellableConfig, "activeWorkspaceId" | "workspaceId">): string | null;
48
50
  export declare function resolveWorkspaceIdForRequest(requestedWorkspaceId: string | null | undefined, action?: string): string | null;
49
51
  export declare function getConfig(): SellableConfig;
52
+ /** Reads only the credential class; never returns or validates credential bytes. */
53
+ export declare function getConfiguredCredentialIntent(): "unconfigured" | "human" | "agent_service" | "invalid";
50
54
  export declare function updateActiveWorkspace(params: {
51
55
  workspaceId: string;
52
56
  workspaceName?: string | null;
package/dist/auth.js CHANGED
@@ -1,9 +1,14 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
+ import { createHash } from "node:crypto";
5
+ import { readMcpAgentServiceContext } from "./agent-service-auth.js";
4
6
  const DEFAULT_API_URL = "https://app.sellable.dev";
5
7
  const WORKSPACE_LOCK_ENV = "SELLABLE_LOCK_WORKSPACE_ID";
6
8
  const REQUIRE_WORKSPACE_LOCK_ENV = "SELLABLE_REQUIRE_WORKSPACE_LOCK";
9
+ const AGENT_SERVICE_CREDENTIAL_FILE_ENV = "SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE";
10
+ const AGENT_RUNTIME_SECRET_ROOT_ENV = "SELLABLE_AGENT_RUNTIME_SECRET_ROOT";
11
+ const AGENT_PROFILE_ID_ENV = "SELLABLE_AGENT_PROFILE_ID";
7
12
  // No caching — re-read config on every call so token/workspace changes
8
13
  // made by the LLM (editing sellable.json) take effect immediately
9
14
  // without requiring an MCP server restart.
@@ -87,6 +92,64 @@ function normalizeConfig(raw) {
87
92
  apiUrl: raw.apiUrl || DEFAULT_API_URL,
88
93
  };
89
94
  }
95
+ function readConfinedAgentServiceCredential(raw, env = process.env) {
96
+ const credentialPathValue = env[AGENT_SERVICE_CREDENTIAL_FILE_ENV]?.trim();
97
+ if (!credentialPathValue)
98
+ return null;
99
+ const runtimeRootValue = env[AGENT_RUNTIME_SECRET_ROOT_ENV]?.trim();
100
+ const profileId = env[AGENT_PROFILE_ID_ENV]?.trim();
101
+ if (!runtimeRootValue ||
102
+ !path.isAbsolute(runtimeRootValue) ||
103
+ !path.isAbsolute(credentialPathValue) ||
104
+ !profileId ||
105
+ !/^[A-Za-z0-9_-]{1,128}$/.test(profileId)) {
106
+ throw new Error("Invalid Agent service credential file binding");
107
+ }
108
+ const rootLink = fs.lstatSync(runtimeRootValue);
109
+ if (rootLink.isSymbolicLink() ||
110
+ !rootLink.isDirectory() ||
111
+ (rootLink.mode & 0o077) !== 0) {
112
+ throw new Error("Invalid Agent runtime secret root");
113
+ }
114
+ const lexicalExpectedPath = path.join(path.resolve(runtimeRootValue), profileId, "service-credential");
115
+ if (path.resolve(credentialPathValue) !== lexicalExpectedPath) {
116
+ throw new Error("Invalid Agent service credential file binding");
117
+ }
118
+ const runtimeRoot = fs.realpathSync(runtimeRootValue);
119
+ const expectedPath = path.join(runtimeRoot, profileId, "service-credential");
120
+ const parent = path.dirname(expectedPath);
121
+ const parentLink = fs.lstatSync(parent);
122
+ const credentialLink = fs.lstatSync(expectedPath);
123
+ const currentUid = process.getuid?.();
124
+ if (parentLink.isSymbolicLink() ||
125
+ !parentLink.isDirectory() ||
126
+ (parentLink.mode & 0o077) !== 0 ||
127
+ credentialLink.isSymbolicLink() ||
128
+ !credentialLink.isFile() ||
129
+ (credentialLink.mode & 0o777) !== 0o600 ||
130
+ credentialLink.size < 8 ||
131
+ credentialLink.size > 4096 ||
132
+ (currentUid !== undefined && credentialLink.uid !== currentUid)) {
133
+ throw new Error("Invalid Agent service credential file");
134
+ }
135
+ if (fs.realpathSync(parent) !== parent || fs.realpathSync(expectedPath) !== expectedPath) {
136
+ throw new Error("Invalid Agent service credential file confinement");
137
+ }
138
+ const token = fs.readFileSync(expectedPath, "utf8").trim();
139
+ const descriptor = raw.serviceCredential;
140
+ const generation = Number(env.SELLABLE_AGENT_CREDENTIAL_GENERATION ?? "");
141
+ const fingerprint = createHash("sha256").update(token).digest("hex").slice(0, 16);
142
+ if (!/^sat_[^_\s]+_\S+$/.test(token) ||
143
+ typeof raw.token === "string" ||
144
+ !descriptor ||
145
+ descriptor.fingerprint !== fingerprint ||
146
+ descriptor.generation !== generation ||
147
+ typeof descriptor.reference !== "string" ||
148
+ !/^secret:\/\/agent\/service\/g[1-9][0-9]*$/.test(descriptor.reference)) {
149
+ throw new Error("Invalid Agent service credential descriptor");
150
+ }
151
+ return token;
152
+ }
90
153
  export function getLockedWorkspaceId() {
91
154
  const value = process.env[WORKSPACE_LOCK_ENV]?.trim();
92
155
  return value || null;
@@ -147,6 +210,7 @@ export function getConfig() {
147
210
  const raw = JSON.parse(content);
148
211
  // Resolve config: support multi-env format or legacy flat format
149
212
  let config;
213
+ let selectedRaw;
150
214
  if (raw.activeEnv && raw.environments) {
151
215
  const envName = raw.activeEnv;
152
216
  const envConfig = raw.environments[envName];
@@ -154,6 +218,7 @@ export function getConfig() {
154
218
  const available = Object.keys(raw.environments).join(", ");
155
219
  throw new Error(`Unknown environment '${envName}' in ${configFileName}. Available: ${available}`);
156
220
  }
221
+ selectedRaw = envConfig;
157
222
  config = {
158
223
  ...normalizeConfig(envConfig),
159
224
  activeEnvName: envName,
@@ -161,13 +226,35 @@ export function getConfig() {
161
226
  }
162
227
  else {
163
228
  // Legacy flat format: { token, workspaceId?, apiUrl? }
229
+ selectedRaw = raw;
164
230
  config = normalizeConfig(raw);
165
231
  }
166
- if (!config.token) {
232
+ const confinedAgentCredential = readConfinedAgentServiceCredential(selectedRaw);
233
+ if (confinedAgentCredential) {
234
+ config.token = confinedAgentCredential;
235
+ config.credentialKind = "agent_service";
236
+ }
237
+ else if (!config.token) {
167
238
  throw new Error(`Missing 'token' in ${configFileName}`);
168
239
  }
169
- if (!config.token.startsWith("skt_")) {
170
- throw new Error("Invalid token format - must start with 'skt_'");
240
+ if (config.token.startsWith("skt_")) {
241
+ config.credentialKind = "human";
242
+ }
243
+ else if (config.token.startsWith("sat_")) {
244
+ const agent = readMcpAgentServiceContext(process.env, config.token);
245
+ if (!agent)
246
+ throw new Error("Invalid Agent service context");
247
+ const configuredWorkspace = normalizeWorkspaceId(config.activeWorkspaceId) ||
248
+ normalizeWorkspaceId(config.workspaceId);
249
+ if (configuredWorkspace && configuredWorkspace !== agent.workspaceId) {
250
+ throw new Error("Agent service credential workspace binding mismatch");
251
+ }
252
+ config.activeWorkspaceId = agent.workspaceId;
253
+ config.workspaceId = agent.workspaceId;
254
+ config.credentialKind = "agent_service";
255
+ }
256
+ else {
257
+ throw new Error("Invalid token format - expected a Sellable credential");
171
258
  }
172
259
  return config;
173
260
  }
@@ -178,7 +265,38 @@ export function getConfig() {
178
265
  throw error;
179
266
  }
180
267
  }
268
+ /** Reads only the credential class; never returns or validates credential bytes. */
269
+ export function getConfiguredCredentialIntent() {
270
+ const configPath = getConfigPath();
271
+ if (!fs.existsSync(configPath))
272
+ return "unconfigured";
273
+ try {
274
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
275
+ const selected = raw?.activeEnv && raw?.environments
276
+ ? raw.environments[raw.activeEnv]
277
+ : raw;
278
+ const token = selected?.token;
279
+ if (process.env[AGENT_SERVICE_CREDENTIAL_FILE_ENV]?.trim()) {
280
+ return token == null && selected?.serviceCredential
281
+ ? "agent_service"
282
+ : "invalid";
283
+ }
284
+ if (typeof token !== "string")
285
+ return "invalid";
286
+ if (token.startsWith("sat_"))
287
+ return "agent_service";
288
+ if (token.startsWith("skt_"))
289
+ return "human";
290
+ return "invalid";
291
+ }
292
+ catch {
293
+ return "invalid";
294
+ }
295
+ }
181
296
  export function updateActiveWorkspace(params) {
297
+ if (getConfig().credentialKind === "agent_service") {
298
+ throw new Error("Agent service profiles cannot switch workspaces");
299
+ }
182
300
  resolveWorkspaceIdForRequest(params.workspaceId, "set_active_workspace");
183
301
  const configPath = getConfigPath();
184
302
  const configDir = path.dirname(configPath);