@true-and-useful/janee 0.12.0 → 0.13.0
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/README.md +2 -0
- package/dist/cli/commands/capability.d.ts +12 -10
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +77 -40
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/config.d.ts +7 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +135 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/diagnose.d.ts +7 -0
- package/dist/cli/commands/diagnose.d.ts.map +1 -0
- package/dist/cli/commands/diagnose.js +133 -0
- package/dist/cli/commands/diagnose.js.map +1 -0
- package/dist/cli/commands/doctor-bundle.d.ts +6 -0
- package/dist/cli/commands/doctor-bundle.d.ts.map +1 -0
- package/dist/cli/commands/doctor-bundle.js +108 -0
- package/dist/cli/commands/doctor-bundle.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +163 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/service-edit.d.ts +16 -0
- package/dist/cli/commands/service-edit.d.ts.map +1 -0
- package/dist/cli/commands/service-edit.js +187 -0
- package/dist/cli/commands/service-edit.js.map +1 -0
- package/dist/cli/commands/whoami.d.ts +5 -0
- package/dist/cli/commands/whoami.d.ts.map +1 -0
- package/dist/cli/commands/whoami.js +91 -0
- package/dist/cli/commands/whoami.js.map +1 -0
- package/dist/cli/config-yaml.d.ts +12 -14
- package/dist/cli/config-yaml.d.ts.map +1 -1
- package/dist/cli/config-yaml.js +209 -86
- package/dist/cli/config-yaml.js.map +1 -1
- package/dist/cli/index.js +81 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/authority.d.ts +5 -3
- package/dist/core/authority.d.ts.map +1 -1
- package/dist/core/authority.js +76 -43
- package/dist/core/authority.js.map +1 -1
- package/dist/core/health.d.ts +24 -1
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +138 -26
- package/dist/core/health.js.map +1 -1
- package/dist/core/mcp-server.d.ts +31 -11
- package/dist/core/mcp-server.d.ts.map +1 -1
- package/dist/core/mcp-server.js +580 -238
- package/dist/core/mcp-server.js.map +1 -1
- package/package.json +1 -1
package/dist/core/mcp-server.js
CHANGED
|
@@ -40,6 +40,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
40
40
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
41
|
};
|
|
42
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.DenialError = void 0;
|
|
44
|
+
exports.explainAccessDenial = explainAccessDenial;
|
|
43
45
|
exports.createMCPServer = createMCPServer;
|
|
44
46
|
exports.makeAPIRequest = makeAPIRequest;
|
|
45
47
|
exports.captureClientInfo = captureClientInfo;
|
|
@@ -59,8 +61,17 @@ const exec_js_1 = require("./exec.js");
|
|
|
59
61
|
const health_js_1 = require("./health.js");
|
|
60
62
|
const rules_js_1 = require("./rules.js");
|
|
61
63
|
// Read version from package.json
|
|
62
|
-
const packageJsonPath = (0, path_1.join)(__dirname,
|
|
63
|
-
const pkgVersion = JSON.parse((0, fs_1.readFileSync)(packageJsonPath,
|
|
64
|
+
const packageJsonPath = (0, path_1.join)(__dirname, "../../package.json");
|
|
65
|
+
const pkgVersion = JSON.parse((0, fs_1.readFileSync)(packageJsonPath, "utf8")).version || "0.0.0";
|
|
66
|
+
class DenialError extends Error {
|
|
67
|
+
denial;
|
|
68
|
+
constructor(message, denial) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'DenialError';
|
|
71
|
+
this.denial = denial;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.DenialError = DenialError;
|
|
64
75
|
/**
|
|
65
76
|
* Check whether an agent can access a capability.
|
|
66
77
|
* Checks capability-level allowedAgents first, then falls back to
|
|
@@ -74,11 +85,40 @@ function canAccessCapability(agentId, cap, service, defaultAccessPolicy) {
|
|
|
74
85
|
if (cap.allowedAgents && cap.allowedAgents.length > 0) {
|
|
75
86
|
return cap.allowedAgents.includes(agentId);
|
|
76
87
|
}
|
|
77
|
-
if (defaultAccessPolicy ===
|
|
88
|
+
if (defaultAccessPolicy === "restricted") {
|
|
78
89
|
return false;
|
|
79
90
|
}
|
|
80
91
|
return (0, agent_scope_js_1.canAgentAccess)(agentId, service?.ownership);
|
|
81
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Returns the specific reason access was denied, or null if access is allowed.
|
|
95
|
+
*/
|
|
96
|
+
function explainAccessDenial(agentId, cap, service, defaultAccessPolicy) {
|
|
97
|
+
if (!agentId)
|
|
98
|
+
return null;
|
|
99
|
+
if (cap.allowedAgents && cap.allowedAgents.length > 0) {
|
|
100
|
+
if (!cap.allowedAgents.includes(agentId)) {
|
|
101
|
+
return {
|
|
102
|
+
reason: 'AGENT_NOT_ALLOWED',
|
|
103
|
+
detail: `Agent "${agentId}" is not in allowedAgents [${cap.allowedAgents.join(', ')}]`
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (defaultAccessPolicy === 'restricted') {
|
|
109
|
+
return {
|
|
110
|
+
reason: 'DEFAULT_ACCESS_RESTRICTED',
|
|
111
|
+
detail: `defaultAccess is "restricted" and capability has no allowedAgents list`
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (!(0, agent_scope_js_1.canAgentAccess)(agentId, service?.ownership)) {
|
|
115
|
+
return {
|
|
116
|
+
reason: 'OWNERSHIP_DENIED',
|
|
117
|
+
detail: `Agent "${agentId}" is not listed in service ownership for "${service?.baseUrl || 'unknown'}"`
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
82
122
|
/**
|
|
83
123
|
* Parse TTL string to seconds
|
|
84
124
|
*/
|
|
@@ -92,7 +132,7 @@ function parseTTL(ttl) {
|
|
|
92
132
|
s: 1,
|
|
93
133
|
m: 60,
|
|
94
134
|
h: 3600,
|
|
95
|
-
d: 86400
|
|
135
|
+
d: 86400,
|
|
96
136
|
};
|
|
97
137
|
return value * multipliers[unit];
|
|
98
138
|
}
|
|
@@ -100,7 +140,7 @@ function parseTTL(ttl) {
|
|
|
100
140
|
* Create and start MCP server
|
|
101
141
|
*/
|
|
102
142
|
function createMCPServer(options) {
|
|
103
|
-
const { sessionManager, auditLogger, defaultAccess, onExecute, onExecCommand, onReloadConfig, onPersistOwnership, onForwardToolCall } = options;
|
|
143
|
+
const { sessionManager, auditLogger, defaultAccess, onExecute, onExecCommand, onReloadConfig, onPersistOwnership, onForwardToolCall, } = options;
|
|
104
144
|
// Store as mutable to support hot-reloading
|
|
105
145
|
let capabilities = options.capabilities;
|
|
106
146
|
let services = options.services;
|
|
@@ -108,12 +148,12 @@ function createMCPServer(options) {
|
|
|
108
148
|
// Populated by captureClientInfo() after connecting a transport.
|
|
109
149
|
const clientSessions = new Map();
|
|
110
150
|
const server = new index_js_1.Server({
|
|
111
|
-
name:
|
|
112
|
-
version: pkgVersion
|
|
151
|
+
name: "janee",
|
|
152
|
+
version: pkgVersion,
|
|
113
153
|
}, {
|
|
114
154
|
capabilities: {
|
|
115
|
-
tools: {}
|
|
116
|
-
}
|
|
155
|
+
tools: {},
|
|
156
|
+
},
|
|
117
157
|
});
|
|
118
158
|
/**
|
|
119
159
|
* Resolve agent identity from the MCP session.
|
|
@@ -123,141 +163,192 @@ function createMCPServer(options) {
|
|
|
123
163
|
* Falls back to args.agentId for legacy scenarios.
|
|
124
164
|
*/
|
|
125
165
|
function resolveAgentFromRequest(extra, args) {
|
|
126
|
-
const sessionKey = extra?.sessionId ||
|
|
127
|
-
const clientName = clientSessions.get(sessionKey) || clientSessions.get(
|
|
128
|
-
return (0, agent_scope_js_1.resolveAgentIdentity)({
|
|
166
|
+
const sessionKey = extra?.sessionId || "__default__";
|
|
167
|
+
const clientName = clientSessions.get(sessionKey) || clientSessions.get("__default__");
|
|
168
|
+
return (0, agent_scope_js_1.resolveAgentIdentity)({
|
|
169
|
+
agentId: extra?.sessionId,
|
|
170
|
+
metadata: { transportAgentHint: clientName },
|
|
171
|
+
}, args?.agentId);
|
|
129
172
|
}
|
|
130
173
|
// Tool: list_services
|
|
131
174
|
const listServicesTool = {
|
|
132
|
-
name:
|
|
133
|
-
description:
|
|
175
|
+
name: "list_services",
|
|
176
|
+
description: "List available API capabilities managed by Janee",
|
|
134
177
|
inputSchema: {
|
|
135
|
-
type:
|
|
178
|
+
type: "object",
|
|
136
179
|
properties: {},
|
|
137
|
-
required: []
|
|
138
|
-
}
|
|
180
|
+
required: [],
|
|
181
|
+
},
|
|
139
182
|
};
|
|
140
183
|
// Tool: execute
|
|
141
184
|
const executeTool = {
|
|
142
|
-
name:
|
|
143
|
-
description:
|
|
185
|
+
name: "execute",
|
|
186
|
+
description: "Execute an API request through Janee proxy",
|
|
144
187
|
inputSchema: {
|
|
145
|
-
type:
|
|
188
|
+
type: "object",
|
|
146
189
|
properties: {
|
|
147
190
|
capability: {
|
|
148
|
-
type:
|
|
149
|
-
description:
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Capability name to use (from list_services)",
|
|
150
193
|
},
|
|
151
194
|
method: {
|
|
152
|
-
type:
|
|
153
|
-
enum: [
|
|
154
|
-
description:
|
|
195
|
+
type: "string",
|
|
196
|
+
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
197
|
+
description: "HTTP method",
|
|
155
198
|
},
|
|
156
199
|
path: {
|
|
157
|
-
type:
|
|
158
|
-
description:
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "API path (e.g., /v1/customers)",
|
|
159
202
|
},
|
|
160
203
|
body: {
|
|
161
|
-
type:
|
|
162
|
-
description:
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Request body (JSON string, optional)",
|
|
163
206
|
},
|
|
164
207
|
headers: {
|
|
165
|
-
type:
|
|
166
|
-
description:
|
|
167
|
-
additionalProperties: { type:
|
|
208
|
+
type: "object",
|
|
209
|
+
description: "Additional headers (optional)",
|
|
210
|
+
additionalProperties: { type: "string" },
|
|
168
211
|
},
|
|
169
212
|
reason: {
|
|
170
|
-
type:
|
|
171
|
-
description:
|
|
172
|
-
}
|
|
213
|
+
type: "string",
|
|
214
|
+
description: "Reason for this request (required for some capabilities)",
|
|
215
|
+
},
|
|
173
216
|
},
|
|
174
|
-
required: [
|
|
175
|
-
}
|
|
217
|
+
required: ["capability", "method", "path"],
|
|
218
|
+
},
|
|
176
219
|
};
|
|
177
220
|
// Tool: reload_config
|
|
178
221
|
const reloadConfigTool = {
|
|
179
|
-
name:
|
|
180
|
-
description:
|
|
222
|
+
name: "reload_config",
|
|
223
|
+
description: "Reload Janee configuration from disk without restarting the server. Use after adding new services or capabilities.",
|
|
181
224
|
inputSchema: {
|
|
182
|
-
type:
|
|
225
|
+
type: "object",
|
|
183
226
|
properties: {},
|
|
184
|
-
required: []
|
|
185
|
-
}
|
|
227
|
+
required: [],
|
|
228
|
+
},
|
|
186
229
|
};
|
|
187
230
|
// Tool: janee_exec (RFC 0001 - Secure CLI Execution)
|
|
188
231
|
const execTool = {
|
|
189
|
-
name:
|
|
190
|
-
description:
|
|
232
|
+
name: "janee_exec",
|
|
233
|
+
description: "Execute a CLI command with credentials injected via environment variables. The agent never sees the actual credential — Janee injects it and scrubs output.",
|
|
191
234
|
inputSchema: {
|
|
192
|
-
type:
|
|
235
|
+
type: "object",
|
|
193
236
|
properties: {
|
|
194
237
|
capability: {
|
|
195
|
-
type:
|
|
196
|
-
description:
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "Capability name (must be exec mode, from list_services)",
|
|
197
240
|
},
|
|
198
241
|
command: {
|
|
199
|
-
type:
|
|
200
|
-
items: { type:
|
|
201
|
-
description: 'Command and arguments as array, e.g. ["gh", "issue", "list"]'
|
|
242
|
+
type: "array",
|
|
243
|
+
items: { type: "string" },
|
|
244
|
+
description: 'Command and arguments as array, e.g. ["gh", "issue", "list"]',
|
|
202
245
|
},
|
|
203
246
|
cwd: {
|
|
204
|
-
type:
|
|
205
|
-
description:
|
|
247
|
+
type: "string",
|
|
248
|
+
description: "Working directory for the command (defaults to process cwd)",
|
|
206
249
|
},
|
|
207
250
|
stdin: {
|
|
208
|
-
type:
|
|
209
|
-
description:
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Optional stdin input to pipe to the command",
|
|
210
253
|
},
|
|
211
254
|
reason: {
|
|
212
|
-
type:
|
|
213
|
-
description:
|
|
214
|
-
}
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "Reason for this execution (required for some capabilities)",
|
|
257
|
+
},
|
|
215
258
|
},
|
|
216
|
-
required: [
|
|
217
|
-
}
|
|
259
|
+
required: ["capability", "command"],
|
|
260
|
+
},
|
|
218
261
|
};
|
|
219
262
|
// Tool: manage_credential (agent-scoped credential access control)
|
|
220
263
|
const manageCredentialTool = {
|
|
221
|
-
name:
|
|
222
|
-
description:
|
|
264
|
+
name: "manage_credential",
|
|
265
|
+
description: "View or manage access policies for agent-scoped credentials. Agents can check who has access, grant access to other agents, or revoke access.",
|
|
223
266
|
inputSchema: {
|
|
224
|
-
type:
|
|
267
|
+
type: "object",
|
|
225
268
|
properties: {
|
|
226
269
|
action: {
|
|
227
|
-
type:
|
|
228
|
-
enum: [
|
|
229
|
-
description:
|
|
270
|
+
type: "string",
|
|
271
|
+
enum: ["view", "grant", "revoke"],
|
|
272
|
+
description: "Action to perform: view ownership info, grant access to another agent, or revoke access",
|
|
230
273
|
},
|
|
231
274
|
service: {
|
|
232
|
-
type:
|
|
233
|
-
description:
|
|
275
|
+
type: "string",
|
|
276
|
+
description: "Service name to manage",
|
|
234
277
|
},
|
|
235
278
|
targetAgentId: {
|
|
236
|
-
type:
|
|
237
|
-
description:
|
|
238
|
-
}
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "Agent ID to grant/revoke access for (required for grant/revoke actions)",
|
|
281
|
+
},
|
|
239
282
|
},
|
|
240
|
-
required: [
|
|
241
|
-
}
|
|
283
|
+
required: ["action", "service"],
|
|
284
|
+
},
|
|
242
285
|
};
|
|
243
286
|
// Tool: test_service
|
|
244
287
|
const testServiceTool = {
|
|
245
|
-
name:
|
|
246
|
-
description:
|
|
288
|
+
name: "test_service",
|
|
289
|
+
description: "Test connectivity and authentication for a configured service. Verifies that Janee can reach the service and that credentials are valid.",
|
|
247
290
|
inputSchema: {
|
|
248
|
-
type:
|
|
291
|
+
type: "object",
|
|
249
292
|
properties: {
|
|
250
293
|
service: {
|
|
294
|
+
type: "string",
|
|
295
|
+
description: "Service name to test (from list_services). Omit to test all services.",
|
|
296
|
+
},
|
|
297
|
+
timeout: {
|
|
298
|
+
type: "number",
|
|
299
|
+
description: "Timeout in milliseconds for the health check (default: 10000).",
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
required: [],
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
// Tool: whoami — lets agents discover their resolved identity
|
|
306
|
+
const whoamiTool = {
|
|
307
|
+
name: 'whoami',
|
|
308
|
+
description: 'Show your resolved agent identity as Janee sees it, which capabilities you can access, and the server access policy. Useful for understanding allowedAgents restrictions.',
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: 'object',
|
|
311
|
+
properties: {},
|
|
312
|
+
required: []
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
// Tool: explain_access — full policy evaluation trace
|
|
316
|
+
const explainAccessTool = {
|
|
317
|
+
name: 'explain_access',
|
|
318
|
+
description: 'Trace exactly why a given agent can or cannot access a capability. Returns step-by-step policy evaluation (capability exists, mode, allowedAgents, defaultAccess, ownership, rules). Use for debugging access issues.',
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
agent: {
|
|
251
323
|
type: 'string',
|
|
252
|
-
description: '
|
|
324
|
+
description: 'Agent ID to evaluate access for. Defaults to the calling agent.'
|
|
325
|
+
},
|
|
326
|
+
capability: {
|
|
327
|
+
type: 'string',
|
|
328
|
+
description: 'Capability name to check access for.'
|
|
329
|
+
},
|
|
330
|
+
method: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description: 'HTTP method for rules evaluation (optional, only applies to proxy capabilities).'
|
|
333
|
+
},
|
|
334
|
+
path: {
|
|
335
|
+
type: 'string',
|
|
336
|
+
description: 'Request path for rules evaluation (optional, only applies to proxy capabilities).'
|
|
253
337
|
}
|
|
254
338
|
},
|
|
255
|
-
required: []
|
|
339
|
+
required: ['capability']
|
|
256
340
|
}
|
|
257
341
|
};
|
|
258
342
|
// Register tools
|
|
259
343
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
260
|
-
const tools = [
|
|
344
|
+
const tools = [
|
|
345
|
+
listServicesTool,
|
|
346
|
+
executeTool,
|
|
347
|
+
manageCredentialTool,
|
|
348
|
+
testServiceTool,
|
|
349
|
+
whoamiTool,
|
|
350
|
+
explainAccessTool,
|
|
351
|
+
];
|
|
261
352
|
if (onExecCommand && !options.hideExecTool) {
|
|
262
353
|
tools.push(execTool);
|
|
263
354
|
}
|
|
@@ -270,39 +361,40 @@ function createMCPServer(options) {
|
|
|
270
361
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
|
|
271
362
|
const { name, arguments: args } = request.params;
|
|
272
363
|
try {
|
|
273
|
-
// Runner proxy: forward non-exec tools to the Authority
|
|
274
|
-
if (onForwardToolCall && name !==
|
|
364
|
+
// Runner proxy: forward all non-exec tools to the Authority
|
|
365
|
+
if (onForwardToolCall && name !== "janee_exec") {
|
|
275
366
|
const forwardAgentId = resolveAgentFromRequest(extra, args);
|
|
276
367
|
const result = await onForwardToolCall(name, (args || {}), forwardAgentId);
|
|
277
368
|
return result;
|
|
278
369
|
}
|
|
279
370
|
switch (name) {
|
|
280
|
-
case
|
|
371
|
+
case "list_services": {
|
|
281
372
|
const listAgentId = resolveAgentFromRequest(extra, args);
|
|
282
373
|
return {
|
|
283
|
-
content: [
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
374
|
+
content: [
|
|
375
|
+
{
|
|
376
|
+
type: "text",
|
|
377
|
+
text: JSON.stringify(capabilities.map((cap) => ({
|
|
287
378
|
accessible: canAccessCapability(listAgentId, cap, services.get(cap.service), defaultAccess),
|
|
288
379
|
name: cap.name,
|
|
289
380
|
service: cap.service,
|
|
290
|
-
mode: cap.mode ||
|
|
381
|
+
mode: cap.mode || "proxy",
|
|
291
382
|
ttl: cap.ttl,
|
|
292
383
|
autoApprove: cap.autoApprove,
|
|
293
384
|
requiresReason: cap.requiresReason,
|
|
294
385
|
rules: cap.rules,
|
|
295
|
-
...(cap.mode ===
|
|
386
|
+
...(cap.mode === "exec" && {
|
|
296
387
|
allowCommands: cap.allowCommands,
|
|
297
388
|
env: cap.env ? Object.keys(cap.env) : undefined,
|
|
298
|
-
})
|
|
299
|
-
})), null, 2)
|
|
300
|
-
}
|
|
389
|
+
}),
|
|
390
|
+
})), null, 2),
|
|
391
|
+
},
|
|
392
|
+
],
|
|
301
393
|
};
|
|
302
394
|
}
|
|
303
|
-
case
|
|
395
|
+
case "reload_config": {
|
|
304
396
|
if (!onReloadConfig) {
|
|
305
|
-
throw new Error(
|
|
397
|
+
throw new Error("Config reload not supported");
|
|
306
398
|
}
|
|
307
399
|
try {
|
|
308
400
|
const result = onReloadConfig();
|
|
@@ -311,63 +403,93 @@ function createMCPServer(options) {
|
|
|
311
403
|
capabilities = result.capabilities;
|
|
312
404
|
services = result.services;
|
|
313
405
|
return {
|
|
314
|
-
content: [
|
|
315
|
-
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
316
409
|
text: JSON.stringify({
|
|
317
410
|
success: true,
|
|
318
|
-
message:
|
|
411
|
+
message: "Configuration reloaded successfully",
|
|
319
412
|
services: services.size,
|
|
320
413
|
capabilities: capabilities.length,
|
|
321
414
|
changes: {
|
|
322
415
|
services: services.size - prevServiceCount,
|
|
323
|
-
capabilities: capabilities.length - prevCapCount
|
|
324
|
-
}
|
|
325
|
-
}, null, 2)
|
|
326
|
-
}
|
|
416
|
+
capabilities: capabilities.length - prevCapCount,
|
|
417
|
+
},
|
|
418
|
+
}, null, 2),
|
|
419
|
+
},
|
|
420
|
+
],
|
|
327
421
|
};
|
|
328
422
|
}
|
|
329
423
|
catch (error) {
|
|
330
|
-
throw new Error(`Failed to reload config: ${error instanceof Error ? error.message :
|
|
424
|
+
throw new Error(`Failed to reload config: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
331
425
|
}
|
|
332
426
|
}
|
|
333
|
-
case
|
|
427
|
+
case "execute": {
|
|
334
428
|
const { capability, method, path, body, headers, reason } = args;
|
|
335
429
|
// Validate required arguments
|
|
336
430
|
if (!capability) {
|
|
337
|
-
throw new Error(
|
|
431
|
+
throw new Error("Missing required argument: capability");
|
|
338
432
|
}
|
|
339
433
|
if (!method) {
|
|
340
|
-
throw new Error(
|
|
434
|
+
throw new Error("Missing required argument: method (GET, POST, PUT, DELETE, etc.)");
|
|
341
435
|
}
|
|
342
436
|
if (!path) {
|
|
343
|
-
throw new Error(
|
|
437
|
+
throw new Error("Missing required argument: path");
|
|
344
438
|
}
|
|
345
439
|
// Find capability
|
|
346
|
-
const cap = capabilities.find(c => c.name === capability);
|
|
440
|
+
const cap = capabilities.find((c) => c.name === capability);
|
|
347
441
|
if (!cap) {
|
|
348
|
-
throw new
|
|
442
|
+
throw new DenialError(`Unknown capability: ${capability}`, {
|
|
443
|
+
reasonCode: 'CAPABILITY_NOT_FOUND',
|
|
444
|
+
capability,
|
|
445
|
+
nextStep: `Run 'janee cap list' to see available capabilities, or add one with 'janee cap add'.`
|
|
446
|
+
});
|
|
349
447
|
}
|
|
350
448
|
// Reject exec-mode capabilities — they should use janee_exec instead
|
|
351
|
-
if (cap.mode ===
|
|
352
|
-
throw new
|
|
449
|
+
if (cap.mode === "exec") {
|
|
450
|
+
throw new DenialError(`Capability "${capability}" is an exec-mode capability. Use the 'janee_exec' tool instead.`, {
|
|
451
|
+
reasonCode: "MODE_MISMATCH",
|
|
452
|
+
capability,
|
|
453
|
+
nextStep: `Use the 'janee_exec' tool for exec-mode capabilities.`,
|
|
454
|
+
});
|
|
353
455
|
}
|
|
354
456
|
// Check if reason required
|
|
355
457
|
if (cap.requiresReason && !reason) {
|
|
356
|
-
throw new
|
|
458
|
+
throw new DenialError(`Capability "${capability}" requires a reason`, {
|
|
459
|
+
reasonCode: 'REASON_REQUIRED',
|
|
460
|
+
capability,
|
|
461
|
+
nextStep: `Include a 'reason' argument explaining why you need this access.`
|
|
462
|
+
});
|
|
357
463
|
}
|
|
358
464
|
// Check rules (path-based policies)
|
|
359
465
|
const ruleCheck = (0, rules_js_1.checkRules)(cap.rules, method, path);
|
|
360
466
|
if (!ruleCheck.allowed) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
467
|
+
auditLogger.logDenied(cap.service, method, path, ruleCheck.reason || "Request denied by policy", reason);
|
|
468
|
+
throw new DenialError(ruleCheck.reason || "Request denied by policy", {
|
|
469
|
+
reasonCode: "RULE_DENY",
|
|
470
|
+
capability,
|
|
471
|
+
agentId: resolveAgentFromRequest(extra, args),
|
|
472
|
+
evaluatedPolicy: `rules for ${method} ${path}`,
|
|
473
|
+
nextStep: `Check capability rules with 'janee cap list --json' — the path/method may be explicitly denied.`,
|
|
474
|
+
});
|
|
364
475
|
}
|
|
365
476
|
// Check agent-scoped access (capability-level allowedAgents, then service-level ownership)
|
|
366
477
|
const executeAgentId = resolveAgentFromRequest(extra, args);
|
|
367
478
|
const executeSvc = services.get(cap.service);
|
|
368
479
|
if (!canAccessCapability(executeAgentId, cap, executeSvc, defaultAccess)) {
|
|
369
|
-
|
|
370
|
-
|
|
480
|
+
const denialDetail = explainAccessDenial(executeAgentId, cap, executeSvc, defaultAccess);
|
|
481
|
+
auditLogger.logDenied(cap.service, method, path, "Agent does not have access to this capability", reason);
|
|
482
|
+
throw new DenialError(`Access denied: capability "${capability}" is not accessible to this agent`, {
|
|
483
|
+
reasonCode: denialDetail?.reason || "AGENT_NOT_ALLOWED",
|
|
484
|
+
capability,
|
|
485
|
+
agentId: executeAgentId,
|
|
486
|
+
evaluatedPolicy: denialDetail?.detail,
|
|
487
|
+
nextStep: denialDetail?.reason === "AGENT_NOT_ALLOWED"
|
|
488
|
+
? `Add this agent to allowedAgents: 'janee cap edit ${capability} --allowed-agents ${executeAgentId}'`
|
|
489
|
+
: denialDetail?.reason === "DEFAULT_ACCESS_RESTRICTED"
|
|
490
|
+
? `Either add allowedAgents to the capability or change defaultAccess to 'open'.`
|
|
491
|
+
: `Check service ownership settings for the backing service.`,
|
|
492
|
+
});
|
|
371
493
|
}
|
|
372
494
|
// Get or create session
|
|
373
495
|
const ttlSeconds = parseTTL(cap.ttl);
|
|
@@ -378,34 +500,38 @@ function createMCPServer(options) {
|
|
|
378
500
|
path,
|
|
379
501
|
method,
|
|
380
502
|
headers: headers || {},
|
|
381
|
-
body
|
|
503
|
+
body,
|
|
382
504
|
};
|
|
383
505
|
// Execute
|
|
384
506
|
const response = await onExecute(session, apiReq);
|
|
385
507
|
return {
|
|
386
|
-
content: [
|
|
387
|
-
|
|
508
|
+
content: [
|
|
509
|
+
{
|
|
510
|
+
type: "text",
|
|
388
511
|
text: JSON.stringify({
|
|
389
512
|
status: response.statusCode,
|
|
390
|
-
body: response.body
|
|
391
|
-
}, null, 2)
|
|
392
|
-
}
|
|
513
|
+
body: response.body,
|
|
514
|
+
}, null, 2),
|
|
515
|
+
},
|
|
516
|
+
],
|
|
393
517
|
};
|
|
394
518
|
}
|
|
395
|
-
case
|
|
519
|
+
case "janee_exec": {
|
|
396
520
|
if (!onExecCommand) {
|
|
397
|
-
throw new Error(
|
|
521
|
+
throw new Error("CLI execution not supported in this configuration");
|
|
398
522
|
}
|
|
399
|
-
const { capability: execCapName, command: rawExecCommand, cwd: execCwd, stdin: execStdin, reason: execReason } = args;
|
|
523
|
+
const { capability: execCapName, command: rawExecCommand, cwd: execCwd, stdin: execStdin, reason: execReason, } = args;
|
|
400
524
|
if (!execCapName) {
|
|
401
|
-
throw new Error(
|
|
525
|
+
throw new Error("Missing required argument: capability");
|
|
402
526
|
}
|
|
403
|
-
if (!rawExecCommand ||
|
|
404
|
-
|
|
527
|
+
if (!rawExecCommand ||
|
|
528
|
+
(Array.isArray(rawExecCommand) && rawExecCommand.length === 0) ||
|
|
529
|
+
(typeof rawExecCommand === "string" && rawExecCommand.trim() === "")) {
|
|
530
|
+
throw new Error("Missing required argument: command");
|
|
405
531
|
}
|
|
406
532
|
const execCommand = Array.isArray(rawExecCommand)
|
|
407
533
|
? rawExecCommand
|
|
408
|
-
: typeof rawExecCommand ===
|
|
534
|
+
: typeof rawExecCommand === "string"
|
|
409
535
|
? rawExecCommand.trim().split(/\s+/)
|
|
410
536
|
: [];
|
|
411
537
|
let execCap;
|
|
@@ -413,32 +539,67 @@ function createMCPServer(options) {
|
|
|
413
539
|
if (onForwardToolCall) {
|
|
414
540
|
// Runner mode: Authority handles validation and credential injection.
|
|
415
541
|
// Build a minimal capability stub so onExecCommand has the name.
|
|
416
|
-
execCap = {
|
|
542
|
+
execCap = {
|
|
543
|
+
name: execCapName,
|
|
544
|
+
service: "",
|
|
545
|
+
ttl: "1h",
|
|
546
|
+
mode: "exec",
|
|
547
|
+
workDir: execCwd,
|
|
548
|
+
};
|
|
417
549
|
execSession = { agentId: resolveAgentFromRequest(extra, args) };
|
|
418
550
|
}
|
|
419
551
|
else {
|
|
420
552
|
// Standalone mode: validate locally
|
|
421
|
-
const foundCap = capabilities.find(c => c.name === execCapName);
|
|
553
|
+
const foundCap = capabilities.find((c) => c.name === execCapName);
|
|
422
554
|
if (!foundCap) {
|
|
423
|
-
throw new
|
|
555
|
+
throw new DenialError(`Unknown capability: ${execCapName}`, {
|
|
556
|
+
reasonCode: 'CAPABILITY_NOT_FOUND',
|
|
557
|
+
capability: execCapName,
|
|
558
|
+
nextStep: `Run 'janee cap list' to see available capabilities, or add one with 'janee cap add'.`
|
|
559
|
+
});
|
|
424
560
|
}
|
|
425
561
|
execCap = execCwd ? { ...foundCap, workDir: execCwd } : foundCap;
|
|
426
|
-
if (execCap.mode !==
|
|
427
|
-
throw new
|
|
562
|
+
if (execCap.mode !== "exec") {
|
|
563
|
+
throw new DenialError(`Capability "${execCapName}" is not an exec-mode capability. Use the 'execute' tool for API proxy capabilities.`, {
|
|
564
|
+
reasonCode: "MODE_MISMATCH",
|
|
565
|
+
capability: execCapName,
|
|
566
|
+
nextStep: `Use the 'execute' tool for proxy-mode capabilities.`,
|
|
567
|
+
});
|
|
428
568
|
}
|
|
429
569
|
const execAgentId = resolveAgentFromRequest(extra, args);
|
|
430
570
|
const execSvc = services.get(execCap.service);
|
|
431
571
|
if (!canAccessCapability(execAgentId, execCap, execSvc, defaultAccess)) {
|
|
432
|
-
|
|
433
|
-
|
|
572
|
+
const execDenialDetail = explainAccessDenial(execAgentId, execCap, execSvc, defaultAccess);
|
|
573
|
+
auditLogger.logDenied(execCap.service, "EXEC", execCommand.join(" "), "Agent does not have access to this capability", execReason);
|
|
574
|
+
throw new DenialError(`Access denied: capability "${execCapName}" is not accessible to this agent`, {
|
|
575
|
+
reasonCode: execDenialDetail?.reason || "AGENT_NOT_ALLOWED",
|
|
576
|
+
capability: execCapName,
|
|
577
|
+
agentId: execAgentId,
|
|
578
|
+
evaluatedPolicy: execDenialDetail?.detail,
|
|
579
|
+
nextStep: execDenialDetail?.reason === "AGENT_NOT_ALLOWED"
|
|
580
|
+
? `Add this agent to allowedAgents: 'janee cap edit ${execCapName} --allowed-agents ${execAgentId}'`
|
|
581
|
+
: execDenialDetail?.reason === "DEFAULT_ACCESS_RESTRICTED"
|
|
582
|
+
? `Either add allowedAgents to the capability or change defaultAccess to 'open'.`
|
|
583
|
+
: `Check service ownership settings for the backing service.`,
|
|
584
|
+
});
|
|
434
585
|
}
|
|
435
586
|
if (execCap.requiresReason && !execReason) {
|
|
436
|
-
throw new
|
|
587
|
+
throw new DenialError(`Capability "${execCapName}" requires a reason`, {
|
|
588
|
+
reasonCode: 'REASON_REQUIRED',
|
|
589
|
+
capability: execCapName,
|
|
590
|
+
nextStep: `Include a 'reason' argument explaining why you need this access.`
|
|
591
|
+
});
|
|
437
592
|
}
|
|
438
593
|
const cmdValidation = (0, exec_js_1.validateCommand)(execCommand, execCap.allowCommands || []);
|
|
439
594
|
if (!cmdValidation.allowed) {
|
|
440
|
-
auditLogger.logDenied(execCap.service,
|
|
441
|
-
throw new
|
|
595
|
+
auditLogger.logDenied(execCap.service, "EXEC", execCommand.join(" "), cmdValidation.reason || "Command not allowed", execReason);
|
|
596
|
+
throw new DenialError(cmdValidation.reason || "Command not allowed", {
|
|
597
|
+
reasonCode: "COMMAND_NOT_ALLOWED",
|
|
598
|
+
capability: execCapName,
|
|
599
|
+
agentId: execAgentId,
|
|
600
|
+
evaluatedPolicy: `allowCommands: [${(execCap.allowCommands || []).join(", ")}]`,
|
|
601
|
+
nextStep: `Update allowed commands: 'janee cap edit ${execCapName} --allow-commands "new-pattern"'`,
|
|
602
|
+
});
|
|
442
603
|
}
|
|
443
604
|
const execTtlSeconds = parseTTL(execCap.ttl);
|
|
444
605
|
execSession = sessionManager.createSession(execCap.name, execCap.service, execTtlSeconds, { reason: execReason });
|
|
@@ -447,107 +608,120 @@ function createMCPServer(options) {
|
|
|
447
608
|
// Log to audit
|
|
448
609
|
auditLogger.log({
|
|
449
610
|
service: execCap.service,
|
|
450
|
-
path: execCommand.join(
|
|
451
|
-
method:
|
|
452
|
-
headers: {
|
|
611
|
+
path: execCommand.join(" "),
|
|
612
|
+
method: "EXEC",
|
|
613
|
+
headers: { "x-janee-reason": execReason || "" },
|
|
453
614
|
}, {
|
|
454
615
|
statusCode: execResult.exitCode === 0 ? 200 : 500,
|
|
455
616
|
headers: {},
|
|
456
617
|
body: execResult.stdout,
|
|
457
618
|
}, execResult.executionTimeMs);
|
|
458
619
|
return {
|
|
459
|
-
content: [
|
|
460
|
-
|
|
620
|
+
content: [
|
|
621
|
+
{
|
|
622
|
+
type: "text",
|
|
461
623
|
text: JSON.stringify({
|
|
462
624
|
exitCode: execResult.exitCode,
|
|
463
625
|
stdout: execResult.stdout,
|
|
464
626
|
stderr: execResult.stderr,
|
|
465
627
|
executionTimeMs: execResult.executionTimeMs,
|
|
466
|
-
executionTarget:
|
|
467
|
-
}, null, 2)
|
|
468
|
-
}
|
|
628
|
+
executionTarget: "runner",
|
|
629
|
+
}, null, 2),
|
|
630
|
+
},
|
|
631
|
+
],
|
|
469
632
|
};
|
|
470
633
|
}
|
|
471
|
-
case
|
|
472
|
-
const { action: credAction, service: credService, targetAgentId: credTarget } = args;
|
|
634
|
+
case "manage_credential": {
|
|
635
|
+
const { action: credAction, service: credService, targetAgentId: credTarget, } = args;
|
|
473
636
|
const credAgentId = resolveAgentFromRequest(extra, args);
|
|
474
637
|
if (!credService) {
|
|
475
|
-
throw new Error(
|
|
638
|
+
throw new Error("Missing required argument: service");
|
|
476
639
|
}
|
|
477
640
|
const svc = services.get(credService);
|
|
478
641
|
if (!svc) {
|
|
479
642
|
throw new Error(`Unknown service: ${credService}`);
|
|
480
643
|
}
|
|
481
|
-
if (credAction ===
|
|
644
|
+
if (credAction === "view") {
|
|
482
645
|
return {
|
|
483
|
-
content: [
|
|
484
|
-
|
|
646
|
+
content: [
|
|
647
|
+
{
|
|
648
|
+
type: "text",
|
|
485
649
|
text: JSON.stringify({
|
|
486
650
|
service: credService,
|
|
487
|
-
ownership: svc.ownership || {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
651
|
+
ownership: svc.ownership || {
|
|
652
|
+
accessPolicy: "all-agents",
|
|
653
|
+
note: "No ownership metadata (legacy credential)",
|
|
654
|
+
},
|
|
655
|
+
yourAccess: (0, agent_scope_js_1.canAgentAccess)(credAgentId, svc.ownership),
|
|
656
|
+
}, null, 2),
|
|
657
|
+
},
|
|
658
|
+
],
|
|
491
659
|
};
|
|
492
660
|
}
|
|
493
661
|
// Grant/revoke require ownership verification
|
|
494
662
|
if (!credAgentId) {
|
|
495
|
-
throw new Error(
|
|
663
|
+
throw new Error("agentId is required for grant/revoke actions");
|
|
496
664
|
}
|
|
497
665
|
if (!svc.ownership) {
|
|
498
|
-
throw new Error(
|
|
666
|
+
throw new Error("Cannot manage access for legacy credentials without ownership metadata. Re-add the service to enable scoping.");
|
|
499
667
|
}
|
|
500
668
|
if (svc.ownership.createdBy !== credAgentId) {
|
|
501
|
-
throw new Error(
|
|
669
|
+
throw new Error("Only the credential owner can grant or revoke access");
|
|
502
670
|
}
|
|
503
|
-
if (credAction ===
|
|
671
|
+
if (credAction === "grant") {
|
|
504
672
|
if (!credTarget) {
|
|
505
|
-
throw new Error(
|
|
673
|
+
throw new Error("targetAgentId is required for grant action");
|
|
506
674
|
}
|
|
507
|
-
const { grantAccess } = await Promise.resolve().then(() => __importStar(require(
|
|
675
|
+
const { grantAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
508
676
|
svc.ownership = grantAccess(svc.ownership, credTarget);
|
|
509
677
|
// Persist ownership change to config storage
|
|
510
678
|
if (onPersistOwnership) {
|
|
511
679
|
onPersistOwnership(credService, svc.ownership);
|
|
512
680
|
}
|
|
513
681
|
return {
|
|
514
|
-
content: [
|
|
515
|
-
|
|
682
|
+
content: [
|
|
683
|
+
{
|
|
684
|
+
type: "text",
|
|
516
685
|
text: JSON.stringify({
|
|
517
686
|
success: true,
|
|
518
687
|
message: `Granted access to ${credTarget}`,
|
|
519
688
|
ownership: svc.ownership,
|
|
520
|
-
persisted: !!onPersistOwnership
|
|
521
|
-
}, null, 2)
|
|
522
|
-
}
|
|
689
|
+
persisted: !!onPersistOwnership,
|
|
690
|
+
}, null, 2),
|
|
691
|
+
},
|
|
692
|
+
],
|
|
523
693
|
};
|
|
524
694
|
}
|
|
525
|
-
if (credAction ===
|
|
695
|
+
if (credAction === "revoke") {
|
|
526
696
|
if (!credTarget) {
|
|
527
|
-
throw new Error(
|
|
697
|
+
throw new Error("targetAgentId is required for revoke action");
|
|
528
698
|
}
|
|
529
|
-
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require(
|
|
699
|
+
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
530
700
|
svc.ownership = revokeAccess(svc.ownership, credTarget);
|
|
531
701
|
// Persist ownership change to config storage
|
|
532
702
|
if (onPersistOwnership) {
|
|
533
703
|
onPersistOwnership(credService, svc.ownership);
|
|
534
704
|
}
|
|
535
705
|
return {
|
|
536
|
-
content: [
|
|
537
|
-
|
|
706
|
+
content: [
|
|
707
|
+
{
|
|
708
|
+
type: "text",
|
|
538
709
|
text: JSON.stringify({
|
|
539
710
|
success: true,
|
|
540
711
|
message: `Revoked access from ${credTarget}`,
|
|
541
712
|
ownership: svc.ownership,
|
|
542
|
-
persisted: !!onPersistOwnership
|
|
543
|
-
}, null, 2)
|
|
544
|
-
}
|
|
713
|
+
persisted: !!onPersistOwnership,
|
|
714
|
+
}, null, 2),
|
|
715
|
+
},
|
|
716
|
+
],
|
|
545
717
|
};
|
|
546
718
|
}
|
|
547
719
|
throw new Error(`Unknown action: ${credAction}. Use 'view', 'grant', or 'revoke'.`);
|
|
548
720
|
}
|
|
549
|
-
case
|
|
550
|
-
const { service: testSvcName } = (args ||
|
|
721
|
+
case "test_service": {
|
|
722
|
+
const { service: testSvcName, timeout: testTimeout } = (args ||
|
|
723
|
+
{});
|
|
724
|
+
const testOpts = testTimeout ? { timeout: testTimeout } : {};
|
|
551
725
|
let targets;
|
|
552
726
|
if (testSvcName) {
|
|
553
727
|
const svc = services.get(testSvcName);
|
|
@@ -560,13 +734,148 @@ function createMCPServer(options) {
|
|
|
560
734
|
targets = Array.from(services.entries());
|
|
561
735
|
}
|
|
562
736
|
if (targets.length === 0) {
|
|
563
|
-
throw new Error(
|
|
737
|
+
throw new Error("No services configured");
|
|
564
738
|
}
|
|
565
|
-
const results = await Promise.all(targets.map(([name, config]) => (0, health_js_1.testServiceConnection)(name, config)));
|
|
739
|
+
const results = await Promise.all(targets.map(([name, config]) => (0, health_js_1.testServiceConnection)(name, config, testOpts)));
|
|
740
|
+
return {
|
|
741
|
+
content: [
|
|
742
|
+
{
|
|
743
|
+
type: "text",
|
|
744
|
+
text: JSON.stringify(results.length === 1 ? results[0] : results, null, 2),
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
case 'explain_access': {
|
|
750
|
+
const { agent: explainAgent, capability: explainCapName, method: explainMethod, path: explainPath } = args;
|
|
751
|
+
const targetAgentId = explainAgent || resolveAgentFromRequest(extra, args);
|
|
752
|
+
const trace = [];
|
|
753
|
+
const explainCap = capabilities.find(c => c.name === explainCapName);
|
|
754
|
+
if (!explainCap) {
|
|
755
|
+
trace.push({ check: 'capability_exists', result: 'fail', detail: `Capability "${explainCapName}" not found` });
|
|
756
|
+
return {
|
|
757
|
+
content: [{
|
|
758
|
+
type: 'text',
|
|
759
|
+
text: JSON.stringify({
|
|
760
|
+
agent: targetAgentId ?? null,
|
|
761
|
+
capability: explainCapName,
|
|
762
|
+
allowed: false,
|
|
763
|
+
trace,
|
|
764
|
+
nextStep: `Run 'janee cap list' to see available capabilities.`
|
|
765
|
+
}, null, 2)
|
|
766
|
+
}]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
trace.push({ check: 'capability_exists', result: 'pass', detail: `Capability "${explainCapName}" exists (service: ${explainCap.service})` });
|
|
770
|
+
// Mode check
|
|
771
|
+
if (explainMethod && explainCap.mode === 'exec') {
|
|
772
|
+
trace.push({ check: 'mode', result: 'fail', detail: `Capability is exec-mode but method/path were provided (use janee_exec)` });
|
|
773
|
+
}
|
|
774
|
+
else if (!explainMethod && explainCap.mode !== 'exec') {
|
|
775
|
+
trace.push({ check: 'mode', result: 'pass', detail: `Capability mode: ${explainCap.mode || 'proxy'}` });
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
trace.push({ check: 'mode', result: 'pass', detail: `Capability mode: ${explainCap.mode || 'proxy'}` });
|
|
779
|
+
}
|
|
780
|
+
// allowedAgents
|
|
781
|
+
if (explainCap.allowedAgents && explainCap.allowedAgents.length > 0) {
|
|
782
|
+
if (!targetAgentId) {
|
|
783
|
+
trace.push({ check: 'allowed_agents', result: 'pass', detail: `No agent ID (admin/CLI) — bypasses allowedAgents` });
|
|
784
|
+
}
|
|
785
|
+
else if (explainCap.allowedAgents.includes(targetAgentId)) {
|
|
786
|
+
trace.push({ check: 'allowed_agents', result: 'pass', detail: `Agent "${targetAgentId}" is in allowedAgents [${explainCap.allowedAgents.join(', ')}]` });
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
trace.push({ check: 'allowed_agents', result: 'fail', detail: `Agent "${targetAgentId}" is NOT in allowedAgents [${explainCap.allowedAgents.join(', ')}]` });
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
trace.push({ check: 'allowed_agents', result: 'skip', detail: `No allowedAgents restriction on this capability` });
|
|
794
|
+
}
|
|
795
|
+
// defaultAccess
|
|
796
|
+
if (targetAgentId && (!explainCap.allowedAgents || explainCap.allowedAgents.length === 0)) {
|
|
797
|
+
if (defaultAccess === 'restricted') {
|
|
798
|
+
trace.push({ check: 'default_access', result: 'fail', detail: `defaultAccess is "restricted" and no allowedAgents list — agent blocked` });
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
trace.push({ check: 'default_access', result: 'pass', detail: `defaultAccess is "${defaultAccess ?? 'open'}" — agent allowed` });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
trace.push({ check: 'default_access', result: 'skip', detail: targetAgentId ? `allowedAgents list takes precedence` : `No agent ID (admin/CLI)` });
|
|
806
|
+
}
|
|
807
|
+
// Ownership
|
|
808
|
+
const explainSvc = services.get(explainCap.service);
|
|
809
|
+
if (targetAgentId && explainSvc?.ownership) {
|
|
810
|
+
if ((0, agent_scope_js_1.canAgentAccess)(targetAgentId, explainSvc.ownership)) {
|
|
811
|
+
trace.push({ check: 'ownership', result: 'pass', detail: `Agent can access service (ownership: ${JSON.stringify(explainSvc.ownership)})` });
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
trace.push({ check: 'ownership', result: 'fail', detail: `Agent cannot access service (ownership: ${JSON.stringify(explainSvc.ownership)})` });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
trace.push({ check: 'ownership', result: 'skip', detail: explainSvc?.ownership ? `No agent ID (admin/CLI)` : `No ownership restrictions on service` });
|
|
819
|
+
}
|
|
820
|
+
// Rules check (only if method/path provided)
|
|
821
|
+
if (explainMethod && explainPath && explainCap.mode !== 'exec') {
|
|
822
|
+
const ruleResult = (0, rules_js_1.checkRules)(explainCap.rules, explainMethod, explainPath);
|
|
823
|
+
if (ruleResult.allowed) {
|
|
824
|
+
trace.push({ check: 'rules', result: 'pass', detail: `${explainMethod} ${explainPath} is allowed by rules` });
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
trace.push({ check: 'rules', result: 'fail', detail: ruleResult.reason || `${explainMethod} ${explainPath} is denied by rules` });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
else if (explainCap.mode === 'exec') {
|
|
831
|
+
trace.push({ check: 'rules', result: 'skip', detail: `Exec-mode capabilities use allowCommands, not path rules` });
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
trace.push({ check: 'rules', result: 'skip', detail: `No method/path provided for rules evaluation` });
|
|
835
|
+
}
|
|
836
|
+
// Command validation for exec mode
|
|
837
|
+
if (explainCap.mode === 'exec') {
|
|
838
|
+
trace.push({ check: 'allow_commands', result: 'skip', detail: `allowCommands: [${(explainCap.allowCommands || []).join(', ')}] — provide a specific command to validate` });
|
|
839
|
+
}
|
|
840
|
+
const hasFail = trace.some(t => t.result === 'fail');
|
|
841
|
+
const firstFail = trace.find(t => t.result === 'fail');
|
|
842
|
+
return {
|
|
843
|
+
content: [{
|
|
844
|
+
type: 'text',
|
|
845
|
+
text: JSON.stringify({
|
|
846
|
+
agent: targetAgentId ?? null,
|
|
847
|
+
capability: explainCapName,
|
|
848
|
+
allowed: !hasFail,
|
|
849
|
+
trace,
|
|
850
|
+
...(hasFail && firstFail ? { nextStep: firstFail.detail } : {})
|
|
851
|
+
}, null, 2)
|
|
852
|
+
}]
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
case 'whoami': {
|
|
856
|
+
const whoamiAgentId = resolveAgentFromRequest(extra, args);
|
|
857
|
+
const accessibleCaps = capabilities
|
|
858
|
+
.filter(cap => canAccessCapability(whoamiAgentId, cap, services.get(cap.service), defaultAccess))
|
|
859
|
+
.map(cap => cap.name);
|
|
860
|
+
const deniedCaps = capabilities
|
|
861
|
+
.filter(cap => !canAccessCapability(whoamiAgentId, cap, services.get(cap.service), defaultAccess))
|
|
862
|
+
.map(cap => cap.name);
|
|
566
863
|
return {
|
|
567
864
|
content: [{
|
|
568
865
|
type: 'text',
|
|
569
|
-
text: JSON.stringify(
|
|
866
|
+
text: JSON.stringify({
|
|
867
|
+
agentId: whoamiAgentId ?? null,
|
|
868
|
+
identitySource: whoamiAgentId
|
|
869
|
+
? ((extra?.sessionId && clientSessions.has(extra.sessionId)) || clientSessions.has('__default__')
|
|
870
|
+
? 'transport (clientInfo.name)'
|
|
871
|
+
: 'client-asserted (untrusted)')
|
|
872
|
+
: 'none',
|
|
873
|
+
defaultAccessPolicy: defaultAccess ?? 'open',
|
|
874
|
+
capabilities: {
|
|
875
|
+
accessible: accessibleCaps,
|
|
876
|
+
denied: deniedCaps,
|
|
877
|
+
},
|
|
878
|
+
}, null, 2)
|
|
570
879
|
}]
|
|
571
880
|
};
|
|
572
881
|
}
|
|
@@ -575,14 +884,20 @@ function createMCPServer(options) {
|
|
|
575
884
|
}
|
|
576
885
|
}
|
|
577
886
|
catch (error) {
|
|
887
|
+
const payload = {
|
|
888
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
889
|
+
};
|
|
890
|
+
if (error instanceof DenialError) {
|
|
891
|
+
payload.denial = error.denial;
|
|
892
|
+
}
|
|
578
893
|
return {
|
|
579
|
-
content: [
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
isError: true
|
|
894
|
+
content: [
|
|
895
|
+
{
|
|
896
|
+
type: "text",
|
|
897
|
+
text: JSON.stringify(payload, null, 2),
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
isError: true,
|
|
586
901
|
};
|
|
587
902
|
}
|
|
588
903
|
});
|
|
@@ -600,31 +915,31 @@ function createMCPServer(options) {
|
|
|
600
915
|
*/
|
|
601
916
|
function makeAPIRequest(targetUrl, request) {
|
|
602
917
|
return new Promise((resolve, reject) => {
|
|
603
|
-
const client = targetUrl.protocol ===
|
|
918
|
+
const client = targetUrl.protocol === "https:" ? https_1.default : http_1.default;
|
|
604
919
|
const options = {
|
|
605
920
|
hostname: targetUrl.hostname,
|
|
606
921
|
port: targetUrl.port,
|
|
607
922
|
path: targetUrl.pathname + targetUrl.search,
|
|
608
923
|
method: request.method,
|
|
609
924
|
headers: {
|
|
610
|
-
|
|
611
|
-
...request.headers
|
|
612
|
-
}
|
|
925
|
+
"User-Agent": "janee/" + pkgVersion,
|
|
926
|
+
...request.headers,
|
|
927
|
+
},
|
|
613
928
|
};
|
|
614
929
|
const req = client.request(options, (res) => {
|
|
615
|
-
let body =
|
|
616
|
-
res.on(
|
|
930
|
+
let body = "";
|
|
931
|
+
res.on("data", (chunk) => {
|
|
617
932
|
body += chunk;
|
|
618
933
|
});
|
|
619
|
-
res.on(
|
|
934
|
+
res.on("end", () => {
|
|
620
935
|
resolve({
|
|
621
936
|
statusCode: res.statusCode || 500,
|
|
622
937
|
headers: res.headers,
|
|
623
|
-
body
|
|
938
|
+
body,
|
|
624
939
|
});
|
|
625
940
|
});
|
|
626
941
|
});
|
|
627
|
-
req.on(
|
|
942
|
+
req.on("error", (error) => {
|
|
628
943
|
reject(error);
|
|
629
944
|
});
|
|
630
945
|
if (request.body) {
|
|
@@ -641,8 +956,8 @@ function makeAPIRequest(targetUrl, request) {
|
|
|
641
956
|
function captureClientInfo(transport, clientSessions) {
|
|
642
957
|
const original = transport.onmessage;
|
|
643
958
|
transport.onmessage = (message, extra) => {
|
|
644
|
-
if (message?.method ===
|
|
645
|
-
const key = extra?.sessionId ||
|
|
959
|
+
if (message?.method === "initialize" && message?.params?.clientInfo?.name) {
|
|
960
|
+
const key = extra?.sessionId || "__default__";
|
|
646
961
|
clientSessions.set(key, message.params.clientInfo.name);
|
|
647
962
|
}
|
|
648
963
|
return original?.call(transport, message, extra);
|
|
@@ -657,7 +972,7 @@ async function startMCPServer(serverOptions) {
|
|
|
657
972
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
658
973
|
await server.connect(transport);
|
|
659
974
|
captureClientInfo(transport, clientSessions);
|
|
660
|
-
console.error(
|
|
975
|
+
console.error("Janee MCP server started (stdio)");
|
|
661
976
|
return mcpResult;
|
|
662
977
|
}
|
|
663
978
|
/**
|
|
@@ -679,83 +994,103 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
679
994
|
const sessions = new Map();
|
|
680
995
|
// Authority REST endpoints -- active when runnerKey is provided
|
|
681
996
|
if (httpOptions.runnerKey && httpOptions.authorityHooks) {
|
|
682
|
-
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require(
|
|
997
|
+
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require("crypto")));
|
|
683
998
|
const runnerKey = httpOptions.runnerKey;
|
|
684
999
|
const hooks = httpOptions.authorityHooks;
|
|
685
1000
|
const authMiddleware = (req, res, next) => {
|
|
686
|
-
const provided = req.header(
|
|
687
|
-
if (!provided ||
|
|
1001
|
+
const provided = req.header("x-janee-runner-key");
|
|
1002
|
+
if (!provided ||
|
|
1003
|
+
provided.length !== runnerKey.length ||
|
|
688
1004
|
!timingSafeEqual(Buffer.from(provided), Buffer.from(runnerKey))) {
|
|
689
|
-
res.status(401).json({ error:
|
|
1005
|
+
res.status(401).json({ error: "Unauthorized runner request" });
|
|
690
1006
|
return;
|
|
691
1007
|
}
|
|
692
1008
|
next();
|
|
693
1009
|
};
|
|
694
|
-
app.get(
|
|
695
|
-
res.status(200).json({ ok: true, mode:
|
|
1010
|
+
app.get("/v1/health", (_req, res) => {
|
|
1011
|
+
res.status(200).json({ ok: true, mode: "authority" });
|
|
696
1012
|
});
|
|
697
|
-
app.post(
|
|
1013
|
+
app.post("/v1/exec/authorize", authMiddleware, async (req, res) => {
|
|
698
1014
|
try {
|
|
699
1015
|
const body = req.body;
|
|
700
|
-
if (!body?.runner?.runnerId ||
|
|
701
|
-
|
|
1016
|
+
if (!body?.runner?.runnerId ||
|
|
1017
|
+
!Array.isArray(body?.command) ||
|
|
1018
|
+
body.command.length === 0 ||
|
|
1019
|
+
!body.capabilityId) {
|
|
1020
|
+
res.status(400).json({ error: "Invalid authorize request" });
|
|
702
1021
|
return;
|
|
703
1022
|
}
|
|
704
1023
|
const response = await hooks.authorizeExec(body);
|
|
705
1024
|
res.status(200).json(response);
|
|
706
1025
|
}
|
|
707
1026
|
catch (error) {
|
|
708
|
-
res
|
|
1027
|
+
res
|
|
1028
|
+
.status(403)
|
|
1029
|
+
.json({
|
|
1030
|
+
error: error instanceof Error ? error.message : "Authorization failed",
|
|
1031
|
+
});
|
|
709
1032
|
}
|
|
710
1033
|
});
|
|
711
|
-
app.post(
|
|
1034
|
+
app.post("/v1/exec/complete", authMiddleware, async (req, res) => {
|
|
712
1035
|
try {
|
|
713
1036
|
if (!req.body?.grantId) {
|
|
714
|
-
res.status(400).json({ error:
|
|
1037
|
+
res.status(400).json({ error: "grantId is required" });
|
|
715
1038
|
return;
|
|
716
1039
|
}
|
|
717
1040
|
await hooks.completeExec(req.body);
|
|
718
1041
|
res.status(200).json({ ok: true });
|
|
719
1042
|
}
|
|
720
1043
|
catch (error) {
|
|
721
|
-
res
|
|
1044
|
+
res
|
|
1045
|
+
.status(500)
|
|
1046
|
+
.json({
|
|
1047
|
+
error: error instanceof Error ? error.message : "completion failed",
|
|
1048
|
+
});
|
|
722
1049
|
}
|
|
723
1050
|
});
|
|
724
1051
|
if (hooks.testService) {
|
|
725
|
-
app.post(
|
|
1052
|
+
app.post("/v1/test", authMiddleware, async (req, res) => {
|
|
726
1053
|
try {
|
|
727
|
-
const result = await hooks.testService(req.body?.service
|
|
1054
|
+
const result = await hooks.testService(req.body?.service, {
|
|
1055
|
+
timeout: req.body?.timeout,
|
|
1056
|
+
});
|
|
728
1057
|
res.status(200).json(result);
|
|
729
1058
|
}
|
|
730
1059
|
catch (error) {
|
|
731
|
-
res
|
|
1060
|
+
res
|
|
1061
|
+
.status(500)
|
|
1062
|
+
.json({
|
|
1063
|
+
error: error instanceof Error ? error.message : "Test failed",
|
|
1064
|
+
});
|
|
732
1065
|
}
|
|
733
1066
|
});
|
|
734
1067
|
}
|
|
735
1068
|
}
|
|
736
1069
|
// Sweep idle sessions every 60 seconds
|
|
737
|
-
const idleSweepInterval = idleTimeoutMs > 0
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1070
|
+
const idleSweepInterval = idleTimeoutMs > 0
|
|
1071
|
+
? setInterval(async () => {
|
|
1072
|
+
const now = Date.now();
|
|
1073
|
+
for (const [sid, session] of sessions.entries()) {
|
|
1074
|
+
if (now - session.lastActivityAt > idleTimeoutMs) {
|
|
1075
|
+
console.error(`Closing idle session ${sid} (inactive for ${Math.round((now - session.lastActivityAt) / 1000)}s)`);
|
|
1076
|
+
sessions.delete(sid);
|
|
1077
|
+
try {
|
|
1078
|
+
await session.transport.close?.();
|
|
1079
|
+
await session.server.close();
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
// best-effort cleanup
|
|
1083
|
+
}
|
|
749
1084
|
}
|
|
750
1085
|
}
|
|
751
|
-
}
|
|
752
|
-
|
|
1086
|
+
}, Math.min(idleTimeoutMs, 60_000))
|
|
1087
|
+
: undefined;
|
|
753
1088
|
if (idleSweepInterval) {
|
|
754
1089
|
idleSweepInterval.unref?.(); // Don't prevent Node from exiting
|
|
755
1090
|
}
|
|
756
|
-
app.all(
|
|
1091
|
+
app.all("/mcp", async (req, res) => {
|
|
757
1092
|
try {
|
|
758
|
-
const sessionId = req.headers[
|
|
1093
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
759
1094
|
if (sessionId && sessions.has(sessionId)) {
|
|
760
1095
|
const session = sessions.get(sessionId);
|
|
761
1096
|
session.lastActivityAt = Date.now();
|
|
@@ -768,7 +1103,11 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
768
1103
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
769
1104
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
770
1105
|
onsessioninitialized: (sid) => {
|
|
771
|
-
sessions.set(sid, {
|
|
1106
|
+
sessions.set(sid, {
|
|
1107
|
+
transport,
|
|
1108
|
+
server,
|
|
1109
|
+
lastActivityAt: Date.now(),
|
|
1110
|
+
});
|
|
772
1111
|
if (clientName)
|
|
773
1112
|
clientSessions.set(sid, clientName);
|
|
774
1113
|
},
|
|
@@ -788,25 +1127,28 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
788
1127
|
}
|
|
789
1128
|
else if (sessionId) {
|
|
790
1129
|
res.status(404).json({
|
|
791
|
-
jsonrpc:
|
|
792
|
-
error: { code: -32001, message:
|
|
1130
|
+
jsonrpc: "2.0",
|
|
1131
|
+
error: { code: -32001, message: "Session not found" },
|
|
793
1132
|
id: null,
|
|
794
1133
|
});
|
|
795
1134
|
}
|
|
796
1135
|
else {
|
|
797
1136
|
res.status(400).json({
|
|
798
|
-
jsonrpc:
|
|
799
|
-
error: {
|
|
1137
|
+
jsonrpc: "2.0",
|
|
1138
|
+
error: {
|
|
1139
|
+
code: -32600,
|
|
1140
|
+
message: "Bad Request: Missing session ID or not an initialize request",
|
|
1141
|
+
},
|
|
800
1142
|
id: null,
|
|
801
1143
|
});
|
|
802
1144
|
}
|
|
803
1145
|
}
|
|
804
1146
|
catch (error) {
|
|
805
|
-
console.error(
|
|
1147
|
+
console.error("Error handling MCP request:", error);
|
|
806
1148
|
if (!res.headersSent) {
|
|
807
1149
|
res.status(500).json({
|
|
808
|
-
jsonrpc:
|
|
809
|
-
error: { code: -32603, message:
|
|
1150
|
+
jsonrpc: "2.0",
|
|
1151
|
+
error: { code: -32603, message: "Internal server error" },
|
|
810
1152
|
id: null,
|
|
811
1153
|
});
|
|
812
1154
|
}
|