@true-and-useful/janee 0.12.0 → 0.13.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/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 +8 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +164 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/serve-mcp.d.ts.map +1 -1
- package/dist/cli/commands/serve-mcp.js +5 -0
- package/dist/cli/commands/serve-mcp.js.map +1 -1
- 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 +36 -4
- package/dist/core/mcp-server.d.ts.map +1 -1
- package/dist/core/mcp-server.js +608 -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,186 +163,251 @@ 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: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'Agent ID to evaluate access for. Defaults to the calling agent.'
|
|
325
|
+
},
|
|
326
|
+
capability: {
|
|
251
327
|
type: 'string',
|
|
252
|
-
description: '
|
|
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
|
};
|
|
342
|
+
// Tool: doctor — runner self-diagnostics (only available in runner mode)
|
|
343
|
+
const doctorTool = {
|
|
344
|
+
name: "doctor",
|
|
345
|
+
description: "Run runner-to-authority diagnostics. Checks authority reachability, authentication, tool forwarding, and identity parity. Only available when running in runner mode.",
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: "object",
|
|
348
|
+
properties: {},
|
|
349
|
+
required: [],
|
|
350
|
+
},
|
|
351
|
+
};
|
|
258
352
|
// Register tools
|
|
259
353
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
260
|
-
const tools = [
|
|
354
|
+
const tools = [
|
|
355
|
+
listServicesTool,
|
|
356
|
+
executeTool,
|
|
357
|
+
manageCredentialTool,
|
|
358
|
+
testServiceTool,
|
|
359
|
+
whoamiTool,
|
|
360
|
+
explainAccessTool,
|
|
361
|
+
];
|
|
261
362
|
if (onExecCommand && !options.hideExecTool) {
|
|
262
363
|
tools.push(execTool);
|
|
263
364
|
}
|
|
264
365
|
if (onReloadConfig) {
|
|
265
366
|
tools.push(reloadConfigTool);
|
|
266
367
|
}
|
|
368
|
+
if (options.onDoctorRunner) {
|
|
369
|
+
tools.push(doctorTool);
|
|
370
|
+
}
|
|
267
371
|
return { tools };
|
|
268
372
|
});
|
|
269
373
|
// Handle tool calls
|
|
270
374
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
|
|
271
375
|
const { name, arguments: args } = request.params;
|
|
272
376
|
try {
|
|
273
|
-
// Runner proxy: forward non-exec tools to the Authority
|
|
274
|
-
if (onForwardToolCall && name !==
|
|
377
|
+
// Runner proxy: forward all non-exec/non-doctor tools to the Authority
|
|
378
|
+
if (onForwardToolCall && name !== "janee_exec" && name !== "doctor") {
|
|
275
379
|
const forwardAgentId = resolveAgentFromRequest(extra, args);
|
|
276
380
|
const result = await onForwardToolCall(name, (args || {}), forwardAgentId);
|
|
277
381
|
return result;
|
|
278
382
|
}
|
|
279
383
|
switch (name) {
|
|
280
|
-
case
|
|
384
|
+
case "list_services": {
|
|
281
385
|
const listAgentId = resolveAgentFromRequest(extra, args);
|
|
282
386
|
return {
|
|
283
|
-
content: [
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
387
|
+
content: [
|
|
388
|
+
{
|
|
389
|
+
type: "text",
|
|
390
|
+
text: JSON.stringify(capabilities.map((cap) => ({
|
|
287
391
|
accessible: canAccessCapability(listAgentId, cap, services.get(cap.service), defaultAccess),
|
|
288
392
|
name: cap.name,
|
|
289
393
|
service: cap.service,
|
|
290
|
-
mode: cap.mode ||
|
|
394
|
+
mode: cap.mode || "proxy",
|
|
291
395
|
ttl: cap.ttl,
|
|
292
396
|
autoApprove: cap.autoApprove,
|
|
293
397
|
requiresReason: cap.requiresReason,
|
|
294
398
|
rules: cap.rules,
|
|
295
|
-
...(cap.mode ===
|
|
399
|
+
...(cap.mode === "exec" && {
|
|
296
400
|
allowCommands: cap.allowCommands,
|
|
297
401
|
env: cap.env ? Object.keys(cap.env) : undefined,
|
|
298
|
-
})
|
|
299
|
-
})), null, 2)
|
|
300
|
-
}
|
|
402
|
+
}),
|
|
403
|
+
})), null, 2),
|
|
404
|
+
},
|
|
405
|
+
],
|
|
301
406
|
};
|
|
302
407
|
}
|
|
303
|
-
case
|
|
408
|
+
case "reload_config": {
|
|
304
409
|
if (!onReloadConfig) {
|
|
305
|
-
throw new Error(
|
|
410
|
+
throw new Error("Config reload not supported");
|
|
306
411
|
}
|
|
307
412
|
try {
|
|
308
413
|
const result = onReloadConfig();
|
|
@@ -311,63 +416,93 @@ function createMCPServer(options) {
|
|
|
311
416
|
capabilities = result.capabilities;
|
|
312
417
|
services = result.services;
|
|
313
418
|
return {
|
|
314
|
-
content: [
|
|
315
|
-
|
|
419
|
+
content: [
|
|
420
|
+
{
|
|
421
|
+
type: "text",
|
|
316
422
|
text: JSON.stringify({
|
|
317
423
|
success: true,
|
|
318
|
-
message:
|
|
424
|
+
message: "Configuration reloaded successfully",
|
|
319
425
|
services: services.size,
|
|
320
426
|
capabilities: capabilities.length,
|
|
321
427
|
changes: {
|
|
322
428
|
services: services.size - prevServiceCount,
|
|
323
|
-
capabilities: capabilities.length - prevCapCount
|
|
324
|
-
}
|
|
325
|
-
}, null, 2)
|
|
326
|
-
}
|
|
429
|
+
capabilities: capabilities.length - prevCapCount,
|
|
430
|
+
},
|
|
431
|
+
}, null, 2),
|
|
432
|
+
},
|
|
433
|
+
],
|
|
327
434
|
};
|
|
328
435
|
}
|
|
329
436
|
catch (error) {
|
|
330
|
-
throw new Error(`Failed to reload config: ${error instanceof Error ? error.message :
|
|
437
|
+
throw new Error(`Failed to reload config: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
331
438
|
}
|
|
332
439
|
}
|
|
333
|
-
case
|
|
440
|
+
case "execute": {
|
|
334
441
|
const { capability, method, path, body, headers, reason } = args;
|
|
335
442
|
// Validate required arguments
|
|
336
443
|
if (!capability) {
|
|
337
|
-
throw new Error(
|
|
444
|
+
throw new Error("Missing required argument: capability");
|
|
338
445
|
}
|
|
339
446
|
if (!method) {
|
|
340
|
-
throw new Error(
|
|
447
|
+
throw new Error("Missing required argument: method (GET, POST, PUT, DELETE, etc.)");
|
|
341
448
|
}
|
|
342
449
|
if (!path) {
|
|
343
|
-
throw new Error(
|
|
450
|
+
throw new Error("Missing required argument: path");
|
|
344
451
|
}
|
|
345
452
|
// Find capability
|
|
346
|
-
const cap = capabilities.find(c => c.name === capability);
|
|
453
|
+
const cap = capabilities.find((c) => c.name === capability);
|
|
347
454
|
if (!cap) {
|
|
348
|
-
throw new
|
|
455
|
+
throw new DenialError(`Unknown capability: ${capability}`, {
|
|
456
|
+
reasonCode: 'CAPABILITY_NOT_FOUND',
|
|
457
|
+
capability,
|
|
458
|
+
nextStep: `Run 'janee cap list' to see available capabilities, or add one with 'janee cap add'.`
|
|
459
|
+
});
|
|
349
460
|
}
|
|
350
461
|
// Reject exec-mode capabilities — they should use janee_exec instead
|
|
351
|
-
if (cap.mode ===
|
|
352
|
-
throw new
|
|
462
|
+
if (cap.mode === "exec") {
|
|
463
|
+
throw new DenialError(`Capability "${capability}" is an exec-mode capability. Use the 'janee_exec' tool instead.`, {
|
|
464
|
+
reasonCode: "MODE_MISMATCH",
|
|
465
|
+
capability,
|
|
466
|
+
nextStep: `Use the 'janee_exec' tool for exec-mode capabilities.`,
|
|
467
|
+
});
|
|
353
468
|
}
|
|
354
469
|
// Check if reason required
|
|
355
470
|
if (cap.requiresReason && !reason) {
|
|
356
|
-
throw new
|
|
471
|
+
throw new DenialError(`Capability "${capability}" requires a reason`, {
|
|
472
|
+
reasonCode: 'REASON_REQUIRED',
|
|
473
|
+
capability,
|
|
474
|
+
nextStep: `Include a 'reason' argument explaining why you need this access.`
|
|
475
|
+
});
|
|
357
476
|
}
|
|
358
477
|
// Check rules (path-based policies)
|
|
359
478
|
const ruleCheck = (0, rules_js_1.checkRules)(cap.rules, method, path);
|
|
360
479
|
if (!ruleCheck.allowed) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
480
|
+
auditLogger.logDenied(cap.service, method, path, ruleCheck.reason || "Request denied by policy", reason);
|
|
481
|
+
throw new DenialError(ruleCheck.reason || "Request denied by policy", {
|
|
482
|
+
reasonCode: "RULE_DENY",
|
|
483
|
+
capability,
|
|
484
|
+
agentId: resolveAgentFromRequest(extra, args),
|
|
485
|
+
evaluatedPolicy: `rules for ${method} ${path}`,
|
|
486
|
+
nextStep: `Check capability rules with 'janee cap list --json' — the path/method may be explicitly denied.`,
|
|
487
|
+
});
|
|
364
488
|
}
|
|
365
489
|
// Check agent-scoped access (capability-level allowedAgents, then service-level ownership)
|
|
366
490
|
const executeAgentId = resolveAgentFromRequest(extra, args);
|
|
367
491
|
const executeSvc = services.get(cap.service);
|
|
368
492
|
if (!canAccessCapability(executeAgentId, cap, executeSvc, defaultAccess)) {
|
|
369
|
-
|
|
370
|
-
|
|
493
|
+
const denialDetail = explainAccessDenial(executeAgentId, cap, executeSvc, defaultAccess);
|
|
494
|
+
auditLogger.logDenied(cap.service, method, path, "Agent does not have access to this capability", reason);
|
|
495
|
+
throw new DenialError(`Access denied: capability "${capability}" is not accessible to this agent`, {
|
|
496
|
+
reasonCode: denialDetail?.reason || "AGENT_NOT_ALLOWED",
|
|
497
|
+
capability,
|
|
498
|
+
agentId: executeAgentId,
|
|
499
|
+
evaluatedPolicy: denialDetail?.detail,
|
|
500
|
+
nextStep: denialDetail?.reason === "AGENT_NOT_ALLOWED"
|
|
501
|
+
? `Add this agent to allowedAgents: 'janee cap edit ${capability} --allowed-agents ${executeAgentId}'`
|
|
502
|
+
: denialDetail?.reason === "DEFAULT_ACCESS_RESTRICTED"
|
|
503
|
+
? `Either add allowedAgents to the capability or change defaultAccess to 'open'.`
|
|
504
|
+
: `Check service ownership settings for the backing service.`,
|
|
505
|
+
});
|
|
371
506
|
}
|
|
372
507
|
// Get or create session
|
|
373
508
|
const ttlSeconds = parseTTL(cap.ttl);
|
|
@@ -378,34 +513,38 @@ function createMCPServer(options) {
|
|
|
378
513
|
path,
|
|
379
514
|
method,
|
|
380
515
|
headers: headers || {},
|
|
381
|
-
body
|
|
516
|
+
body,
|
|
382
517
|
};
|
|
383
518
|
// Execute
|
|
384
519
|
const response = await onExecute(session, apiReq);
|
|
385
520
|
return {
|
|
386
|
-
content: [
|
|
387
|
-
|
|
521
|
+
content: [
|
|
522
|
+
{
|
|
523
|
+
type: "text",
|
|
388
524
|
text: JSON.stringify({
|
|
389
525
|
status: response.statusCode,
|
|
390
|
-
body: response.body
|
|
391
|
-
}, null, 2)
|
|
392
|
-
}
|
|
526
|
+
body: response.body,
|
|
527
|
+
}, null, 2),
|
|
528
|
+
},
|
|
529
|
+
],
|
|
393
530
|
};
|
|
394
531
|
}
|
|
395
|
-
case
|
|
532
|
+
case "janee_exec": {
|
|
396
533
|
if (!onExecCommand) {
|
|
397
|
-
throw new Error(
|
|
534
|
+
throw new Error("CLI execution not supported in this configuration");
|
|
398
535
|
}
|
|
399
|
-
const { capability: execCapName, command: rawExecCommand, cwd: execCwd, stdin: execStdin, reason: execReason } = args;
|
|
536
|
+
const { capability: execCapName, command: rawExecCommand, cwd: execCwd, stdin: execStdin, reason: execReason, } = args;
|
|
400
537
|
if (!execCapName) {
|
|
401
|
-
throw new Error(
|
|
538
|
+
throw new Error("Missing required argument: capability");
|
|
402
539
|
}
|
|
403
|
-
if (!rawExecCommand ||
|
|
404
|
-
|
|
540
|
+
if (!rawExecCommand ||
|
|
541
|
+
(Array.isArray(rawExecCommand) && rawExecCommand.length === 0) ||
|
|
542
|
+
(typeof rawExecCommand === "string" && rawExecCommand.trim() === "")) {
|
|
543
|
+
throw new Error("Missing required argument: command");
|
|
405
544
|
}
|
|
406
545
|
const execCommand = Array.isArray(rawExecCommand)
|
|
407
546
|
? rawExecCommand
|
|
408
|
-
: typeof rawExecCommand ===
|
|
547
|
+
: typeof rawExecCommand === "string"
|
|
409
548
|
? rawExecCommand.trim().split(/\s+/)
|
|
410
549
|
: [];
|
|
411
550
|
let execCap;
|
|
@@ -413,32 +552,67 @@ function createMCPServer(options) {
|
|
|
413
552
|
if (onForwardToolCall) {
|
|
414
553
|
// Runner mode: Authority handles validation and credential injection.
|
|
415
554
|
// Build a minimal capability stub so onExecCommand has the name.
|
|
416
|
-
execCap = {
|
|
555
|
+
execCap = {
|
|
556
|
+
name: execCapName,
|
|
557
|
+
service: "",
|
|
558
|
+
ttl: "1h",
|
|
559
|
+
mode: "exec",
|
|
560
|
+
workDir: execCwd,
|
|
561
|
+
};
|
|
417
562
|
execSession = { agentId: resolveAgentFromRequest(extra, args) };
|
|
418
563
|
}
|
|
419
564
|
else {
|
|
420
565
|
// Standalone mode: validate locally
|
|
421
|
-
const foundCap = capabilities.find(c => c.name === execCapName);
|
|
566
|
+
const foundCap = capabilities.find((c) => c.name === execCapName);
|
|
422
567
|
if (!foundCap) {
|
|
423
|
-
throw new
|
|
568
|
+
throw new DenialError(`Unknown capability: ${execCapName}`, {
|
|
569
|
+
reasonCode: 'CAPABILITY_NOT_FOUND',
|
|
570
|
+
capability: execCapName,
|
|
571
|
+
nextStep: `Run 'janee cap list' to see available capabilities, or add one with 'janee cap add'.`
|
|
572
|
+
});
|
|
424
573
|
}
|
|
425
574
|
execCap = execCwd ? { ...foundCap, workDir: execCwd } : foundCap;
|
|
426
|
-
if (execCap.mode !==
|
|
427
|
-
throw new
|
|
575
|
+
if (execCap.mode !== "exec") {
|
|
576
|
+
throw new DenialError(`Capability "${execCapName}" is not an exec-mode capability. Use the 'execute' tool for API proxy capabilities.`, {
|
|
577
|
+
reasonCode: "MODE_MISMATCH",
|
|
578
|
+
capability: execCapName,
|
|
579
|
+
nextStep: `Use the 'execute' tool for proxy-mode capabilities.`,
|
|
580
|
+
});
|
|
428
581
|
}
|
|
429
582
|
const execAgentId = resolveAgentFromRequest(extra, args);
|
|
430
583
|
const execSvc = services.get(execCap.service);
|
|
431
584
|
if (!canAccessCapability(execAgentId, execCap, execSvc, defaultAccess)) {
|
|
432
|
-
|
|
433
|
-
|
|
585
|
+
const execDenialDetail = explainAccessDenial(execAgentId, execCap, execSvc, defaultAccess);
|
|
586
|
+
auditLogger.logDenied(execCap.service, "EXEC", execCommand.join(" "), "Agent does not have access to this capability", execReason);
|
|
587
|
+
throw new DenialError(`Access denied: capability "${execCapName}" is not accessible to this agent`, {
|
|
588
|
+
reasonCode: execDenialDetail?.reason || "AGENT_NOT_ALLOWED",
|
|
589
|
+
capability: execCapName,
|
|
590
|
+
agentId: execAgentId,
|
|
591
|
+
evaluatedPolicy: execDenialDetail?.detail,
|
|
592
|
+
nextStep: execDenialDetail?.reason === "AGENT_NOT_ALLOWED"
|
|
593
|
+
? `Add this agent to allowedAgents: 'janee cap edit ${execCapName} --allowed-agents ${execAgentId}'`
|
|
594
|
+
: execDenialDetail?.reason === "DEFAULT_ACCESS_RESTRICTED"
|
|
595
|
+
? `Either add allowedAgents to the capability or change defaultAccess to 'open'.`
|
|
596
|
+
: `Check service ownership settings for the backing service.`,
|
|
597
|
+
});
|
|
434
598
|
}
|
|
435
599
|
if (execCap.requiresReason && !execReason) {
|
|
436
|
-
throw new
|
|
600
|
+
throw new DenialError(`Capability "${execCapName}" requires a reason`, {
|
|
601
|
+
reasonCode: 'REASON_REQUIRED',
|
|
602
|
+
capability: execCapName,
|
|
603
|
+
nextStep: `Include a 'reason' argument explaining why you need this access.`
|
|
604
|
+
});
|
|
437
605
|
}
|
|
438
606
|
const cmdValidation = (0, exec_js_1.validateCommand)(execCommand, execCap.allowCommands || []);
|
|
439
607
|
if (!cmdValidation.allowed) {
|
|
440
|
-
auditLogger.logDenied(execCap.service,
|
|
441
|
-
throw new
|
|
608
|
+
auditLogger.logDenied(execCap.service, "EXEC", execCommand.join(" "), cmdValidation.reason || "Command not allowed", execReason);
|
|
609
|
+
throw new DenialError(cmdValidation.reason || "Command not allowed", {
|
|
610
|
+
reasonCode: "COMMAND_NOT_ALLOWED",
|
|
611
|
+
capability: execCapName,
|
|
612
|
+
agentId: execAgentId,
|
|
613
|
+
evaluatedPolicy: `allowCommands: [${(execCap.allowCommands || []).join(", ")}]`,
|
|
614
|
+
nextStep: `Update allowed commands: 'janee cap edit ${execCapName} --allow-commands "new-pattern"'`,
|
|
615
|
+
});
|
|
442
616
|
}
|
|
443
617
|
const execTtlSeconds = parseTTL(execCap.ttl);
|
|
444
618
|
execSession = sessionManager.createSession(execCap.name, execCap.service, execTtlSeconds, { reason: execReason });
|
|
@@ -447,107 +621,120 @@ function createMCPServer(options) {
|
|
|
447
621
|
// Log to audit
|
|
448
622
|
auditLogger.log({
|
|
449
623
|
service: execCap.service,
|
|
450
|
-
path: execCommand.join(
|
|
451
|
-
method:
|
|
452
|
-
headers: {
|
|
624
|
+
path: execCommand.join(" "),
|
|
625
|
+
method: "EXEC",
|
|
626
|
+
headers: { "x-janee-reason": execReason || "" },
|
|
453
627
|
}, {
|
|
454
628
|
statusCode: execResult.exitCode === 0 ? 200 : 500,
|
|
455
629
|
headers: {},
|
|
456
630
|
body: execResult.stdout,
|
|
457
631
|
}, execResult.executionTimeMs);
|
|
458
632
|
return {
|
|
459
|
-
content: [
|
|
460
|
-
|
|
633
|
+
content: [
|
|
634
|
+
{
|
|
635
|
+
type: "text",
|
|
461
636
|
text: JSON.stringify({
|
|
462
637
|
exitCode: execResult.exitCode,
|
|
463
638
|
stdout: execResult.stdout,
|
|
464
639
|
stderr: execResult.stderr,
|
|
465
640
|
executionTimeMs: execResult.executionTimeMs,
|
|
466
|
-
executionTarget:
|
|
467
|
-
}, null, 2)
|
|
468
|
-
}
|
|
641
|
+
executionTarget: "runner",
|
|
642
|
+
}, null, 2),
|
|
643
|
+
},
|
|
644
|
+
],
|
|
469
645
|
};
|
|
470
646
|
}
|
|
471
|
-
case
|
|
472
|
-
const { action: credAction, service: credService, targetAgentId: credTarget } = args;
|
|
647
|
+
case "manage_credential": {
|
|
648
|
+
const { action: credAction, service: credService, targetAgentId: credTarget, } = args;
|
|
473
649
|
const credAgentId = resolveAgentFromRequest(extra, args);
|
|
474
650
|
if (!credService) {
|
|
475
|
-
throw new Error(
|
|
651
|
+
throw new Error("Missing required argument: service");
|
|
476
652
|
}
|
|
477
653
|
const svc = services.get(credService);
|
|
478
654
|
if (!svc) {
|
|
479
655
|
throw new Error(`Unknown service: ${credService}`);
|
|
480
656
|
}
|
|
481
|
-
if (credAction ===
|
|
657
|
+
if (credAction === "view") {
|
|
482
658
|
return {
|
|
483
|
-
content: [
|
|
484
|
-
|
|
659
|
+
content: [
|
|
660
|
+
{
|
|
661
|
+
type: "text",
|
|
485
662
|
text: JSON.stringify({
|
|
486
663
|
service: credService,
|
|
487
|
-
ownership: svc.ownership || {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
664
|
+
ownership: svc.ownership || {
|
|
665
|
+
accessPolicy: "all-agents",
|
|
666
|
+
note: "No ownership metadata (legacy credential)",
|
|
667
|
+
},
|
|
668
|
+
yourAccess: (0, agent_scope_js_1.canAgentAccess)(credAgentId, svc.ownership),
|
|
669
|
+
}, null, 2),
|
|
670
|
+
},
|
|
671
|
+
],
|
|
491
672
|
};
|
|
492
673
|
}
|
|
493
674
|
// Grant/revoke require ownership verification
|
|
494
675
|
if (!credAgentId) {
|
|
495
|
-
throw new Error(
|
|
676
|
+
throw new Error("agentId is required for grant/revoke actions");
|
|
496
677
|
}
|
|
497
678
|
if (!svc.ownership) {
|
|
498
|
-
throw new Error(
|
|
679
|
+
throw new Error("Cannot manage access for legacy credentials without ownership metadata. Re-add the service to enable scoping.");
|
|
499
680
|
}
|
|
500
681
|
if (svc.ownership.createdBy !== credAgentId) {
|
|
501
|
-
throw new Error(
|
|
682
|
+
throw new Error("Only the credential owner can grant or revoke access");
|
|
502
683
|
}
|
|
503
|
-
if (credAction ===
|
|
684
|
+
if (credAction === "grant") {
|
|
504
685
|
if (!credTarget) {
|
|
505
|
-
throw new Error(
|
|
686
|
+
throw new Error("targetAgentId is required for grant action");
|
|
506
687
|
}
|
|
507
|
-
const { grantAccess } = await Promise.resolve().then(() => __importStar(require(
|
|
688
|
+
const { grantAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
508
689
|
svc.ownership = grantAccess(svc.ownership, credTarget);
|
|
509
690
|
// Persist ownership change to config storage
|
|
510
691
|
if (onPersistOwnership) {
|
|
511
692
|
onPersistOwnership(credService, svc.ownership);
|
|
512
693
|
}
|
|
513
694
|
return {
|
|
514
|
-
content: [
|
|
515
|
-
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
697
|
+
type: "text",
|
|
516
698
|
text: JSON.stringify({
|
|
517
699
|
success: true,
|
|
518
700
|
message: `Granted access to ${credTarget}`,
|
|
519
701
|
ownership: svc.ownership,
|
|
520
|
-
persisted: !!onPersistOwnership
|
|
521
|
-
}, null, 2)
|
|
522
|
-
}
|
|
702
|
+
persisted: !!onPersistOwnership,
|
|
703
|
+
}, null, 2),
|
|
704
|
+
},
|
|
705
|
+
],
|
|
523
706
|
};
|
|
524
707
|
}
|
|
525
|
-
if (credAction ===
|
|
708
|
+
if (credAction === "revoke") {
|
|
526
709
|
if (!credTarget) {
|
|
527
|
-
throw new Error(
|
|
710
|
+
throw new Error("targetAgentId is required for revoke action");
|
|
528
711
|
}
|
|
529
|
-
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require(
|
|
712
|
+
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
530
713
|
svc.ownership = revokeAccess(svc.ownership, credTarget);
|
|
531
714
|
// Persist ownership change to config storage
|
|
532
715
|
if (onPersistOwnership) {
|
|
533
716
|
onPersistOwnership(credService, svc.ownership);
|
|
534
717
|
}
|
|
535
718
|
return {
|
|
536
|
-
content: [
|
|
537
|
-
|
|
719
|
+
content: [
|
|
720
|
+
{
|
|
721
|
+
type: "text",
|
|
538
722
|
text: JSON.stringify({
|
|
539
723
|
success: true,
|
|
540
724
|
message: `Revoked access from ${credTarget}`,
|
|
541
725
|
ownership: svc.ownership,
|
|
542
|
-
persisted: !!onPersistOwnership
|
|
543
|
-
}, null, 2)
|
|
544
|
-
}
|
|
726
|
+
persisted: !!onPersistOwnership,
|
|
727
|
+
}, null, 2),
|
|
728
|
+
},
|
|
729
|
+
],
|
|
545
730
|
};
|
|
546
731
|
}
|
|
547
732
|
throw new Error(`Unknown action: ${credAction}. Use 'view', 'grant', or 'revoke'.`);
|
|
548
733
|
}
|
|
549
|
-
case
|
|
550
|
-
const { service: testSvcName } = (args ||
|
|
734
|
+
case "test_service": {
|
|
735
|
+
const { service: testSvcName, timeout: testTimeout } = (args ||
|
|
736
|
+
{});
|
|
737
|
+
const testOpts = testTimeout ? { timeout: testTimeout } : {};
|
|
551
738
|
let targets;
|
|
552
739
|
if (testSvcName) {
|
|
553
740
|
const svc = services.get(testSvcName);
|
|
@@ -560,29 +747,185 @@ function createMCPServer(options) {
|
|
|
560
747
|
targets = Array.from(services.entries());
|
|
561
748
|
}
|
|
562
749
|
if (targets.length === 0) {
|
|
563
|
-
throw new Error(
|
|
750
|
+
throw new Error("No services configured");
|
|
751
|
+
}
|
|
752
|
+
const results = await Promise.all(targets.map(([name, config]) => (0, health_js_1.testServiceConnection)(name, config, testOpts)));
|
|
753
|
+
return {
|
|
754
|
+
content: [
|
|
755
|
+
{
|
|
756
|
+
type: "text",
|
|
757
|
+
text: JSON.stringify(results.length === 1 ? results[0] : results, null, 2),
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
case 'explain_access': {
|
|
763
|
+
const { agent: explainAgent, capability: explainCapName, method: explainMethod, path: explainPath } = args;
|
|
764
|
+
const targetAgentId = explainAgent || resolveAgentFromRequest(extra, args);
|
|
765
|
+
const trace = [];
|
|
766
|
+
const explainCap = capabilities.find(c => c.name === explainCapName);
|
|
767
|
+
if (!explainCap) {
|
|
768
|
+
trace.push({ check: 'capability_exists', result: 'fail', detail: `Capability "${explainCapName}" not found` });
|
|
769
|
+
return {
|
|
770
|
+
content: [{
|
|
771
|
+
type: 'text',
|
|
772
|
+
text: JSON.stringify({
|
|
773
|
+
agent: targetAgentId ?? null,
|
|
774
|
+
capability: explainCapName,
|
|
775
|
+
allowed: false,
|
|
776
|
+
trace,
|
|
777
|
+
nextStep: `Run 'janee cap list' to see available capabilities.`
|
|
778
|
+
}, null, 2)
|
|
779
|
+
}]
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
trace.push({ check: 'capability_exists', result: 'pass', detail: `Capability "${explainCapName}" exists (service: ${explainCap.service})` });
|
|
783
|
+
// Mode check
|
|
784
|
+
if (explainMethod && explainCap.mode === 'exec') {
|
|
785
|
+
trace.push({ check: 'mode', result: 'fail', detail: `Capability is exec-mode but method/path were provided (use janee_exec)` });
|
|
786
|
+
}
|
|
787
|
+
else if (!explainMethod && explainCap.mode !== 'exec') {
|
|
788
|
+
trace.push({ check: 'mode', result: 'pass', detail: `Capability mode: ${explainCap.mode || 'proxy'}` });
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
trace.push({ check: 'mode', result: 'pass', detail: `Capability mode: ${explainCap.mode || 'proxy'}` });
|
|
792
|
+
}
|
|
793
|
+
// allowedAgents
|
|
794
|
+
if (explainCap.allowedAgents && explainCap.allowedAgents.length > 0) {
|
|
795
|
+
if (!targetAgentId) {
|
|
796
|
+
trace.push({ check: 'allowed_agents', result: 'pass', detail: `No agent ID (admin/CLI) — bypasses allowedAgents` });
|
|
797
|
+
}
|
|
798
|
+
else if (explainCap.allowedAgents.includes(targetAgentId)) {
|
|
799
|
+
trace.push({ check: 'allowed_agents', result: 'pass', detail: `Agent "${targetAgentId}" is in allowedAgents [${explainCap.allowedAgents.join(', ')}]` });
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
trace.push({ check: 'allowed_agents', result: 'fail', detail: `Agent "${targetAgentId}" is NOT in allowedAgents [${explainCap.allowedAgents.join(', ')}]` });
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
trace.push({ check: 'allowed_agents', result: 'skip', detail: `No allowedAgents restriction on this capability` });
|
|
807
|
+
}
|
|
808
|
+
// defaultAccess
|
|
809
|
+
if (targetAgentId && (!explainCap.allowedAgents || explainCap.allowedAgents.length === 0)) {
|
|
810
|
+
if (defaultAccess === 'restricted') {
|
|
811
|
+
trace.push({ check: 'default_access', result: 'fail', detail: `defaultAccess is "restricted" and no allowedAgents list — agent blocked` });
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
trace.push({ check: 'default_access', result: 'pass', detail: `defaultAccess is "${defaultAccess ?? 'open'}" — agent allowed` });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
trace.push({ check: 'default_access', result: 'skip', detail: targetAgentId ? `allowedAgents list takes precedence` : `No agent ID (admin/CLI)` });
|
|
819
|
+
}
|
|
820
|
+
// Ownership
|
|
821
|
+
const explainSvc = services.get(explainCap.service);
|
|
822
|
+
if (targetAgentId && explainSvc?.ownership) {
|
|
823
|
+
if ((0, agent_scope_js_1.canAgentAccess)(targetAgentId, explainSvc.ownership)) {
|
|
824
|
+
trace.push({ check: 'ownership', result: 'pass', detail: `Agent can access service (ownership: ${JSON.stringify(explainSvc.ownership)})` });
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
trace.push({ check: 'ownership', result: 'fail', detail: `Agent cannot access service (ownership: ${JSON.stringify(explainSvc.ownership)})` });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
trace.push({ check: 'ownership', result: 'skip', detail: explainSvc?.ownership ? `No agent ID (admin/CLI)` : `No ownership restrictions on service` });
|
|
832
|
+
}
|
|
833
|
+
// Rules check (only if method/path provided)
|
|
834
|
+
if (explainMethod && explainPath && explainCap.mode !== 'exec') {
|
|
835
|
+
const ruleResult = (0, rules_js_1.checkRules)(explainCap.rules, explainMethod, explainPath);
|
|
836
|
+
if (ruleResult.allowed) {
|
|
837
|
+
trace.push({ check: 'rules', result: 'pass', detail: `${explainMethod} ${explainPath} is allowed by rules` });
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
trace.push({ check: 'rules', result: 'fail', detail: ruleResult.reason || `${explainMethod} ${explainPath} is denied by rules` });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
else if (explainCap.mode === 'exec') {
|
|
844
|
+
trace.push({ check: 'rules', result: 'skip', detail: `Exec-mode capabilities use allowCommands, not path rules` });
|
|
564
845
|
}
|
|
565
|
-
|
|
846
|
+
else {
|
|
847
|
+
trace.push({ check: 'rules', result: 'skip', detail: `No method/path provided for rules evaluation` });
|
|
848
|
+
}
|
|
849
|
+
// Command validation for exec mode
|
|
850
|
+
if (explainCap.mode === 'exec') {
|
|
851
|
+
trace.push({ check: 'allow_commands', result: 'skip', detail: `allowCommands: [${(explainCap.allowCommands || []).join(', ')}] — provide a specific command to validate` });
|
|
852
|
+
}
|
|
853
|
+
const hasFail = trace.some(t => t.result === 'fail');
|
|
854
|
+
const firstFail = trace.find(t => t.result === 'fail');
|
|
566
855
|
return {
|
|
567
856
|
content: [{
|
|
568
857
|
type: 'text',
|
|
569
|
-
text: JSON.stringify(
|
|
858
|
+
text: JSON.stringify({
|
|
859
|
+
agent: targetAgentId ?? null,
|
|
860
|
+
capability: explainCapName,
|
|
861
|
+
allowed: !hasFail,
|
|
862
|
+
trace,
|
|
863
|
+
...(hasFail && firstFail ? { nextStep: firstFail.detail } : {})
|
|
864
|
+
}, null, 2)
|
|
570
865
|
}]
|
|
571
866
|
};
|
|
572
867
|
}
|
|
868
|
+
case 'whoami': {
|
|
869
|
+
const whoamiAgentId = resolveAgentFromRequest(extra, args);
|
|
870
|
+
const accessibleCaps = capabilities
|
|
871
|
+
.filter(cap => canAccessCapability(whoamiAgentId, cap, services.get(cap.service), defaultAccess))
|
|
872
|
+
.map(cap => cap.name);
|
|
873
|
+
const deniedCaps = capabilities
|
|
874
|
+
.filter(cap => !canAccessCapability(whoamiAgentId, cap, services.get(cap.service), defaultAccess))
|
|
875
|
+
.map(cap => cap.name);
|
|
876
|
+
return {
|
|
877
|
+
content: [{
|
|
878
|
+
type: 'text',
|
|
879
|
+
text: JSON.stringify({
|
|
880
|
+
agentId: whoamiAgentId ?? null,
|
|
881
|
+
identitySource: whoamiAgentId
|
|
882
|
+
? ((extra?.sessionId && clientSessions.has(extra.sessionId)) || clientSessions.has('__default__')
|
|
883
|
+
? 'transport (clientInfo.name)'
|
|
884
|
+
: 'client-asserted (untrusted)')
|
|
885
|
+
: 'none',
|
|
886
|
+
defaultAccessPolicy: defaultAccess ?? 'open',
|
|
887
|
+
capabilities: {
|
|
888
|
+
accessible: accessibleCaps,
|
|
889
|
+
denied: deniedCaps,
|
|
890
|
+
},
|
|
891
|
+
}, null, 2)
|
|
892
|
+
}]
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
case "doctor": {
|
|
896
|
+
if (!options.onDoctorRunner) {
|
|
897
|
+
throw new Error("Doctor diagnostics only available in runner mode.");
|
|
898
|
+
}
|
|
899
|
+
const doctorAgentId = resolveAgentFromRequest(extra, args);
|
|
900
|
+
const doctorResult = await options.onDoctorRunner(doctorAgentId);
|
|
901
|
+
return {
|
|
902
|
+
content: [
|
|
903
|
+
{
|
|
904
|
+
type: "text",
|
|
905
|
+
text: JSON.stringify(doctorResult, null, 2),
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
};
|
|
909
|
+
}
|
|
573
910
|
default:
|
|
574
911
|
throw new Error(`Unknown tool: ${name}`);
|
|
575
912
|
}
|
|
576
913
|
}
|
|
577
914
|
catch (error) {
|
|
915
|
+
const payload = {
|
|
916
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
917
|
+
};
|
|
918
|
+
if (error instanceof DenialError) {
|
|
919
|
+
payload.denial = error.denial;
|
|
920
|
+
}
|
|
578
921
|
return {
|
|
579
|
-
content: [
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
isError: true
|
|
922
|
+
content: [
|
|
923
|
+
{
|
|
924
|
+
type: "text",
|
|
925
|
+
text: JSON.stringify(payload, null, 2),
|
|
926
|
+
},
|
|
927
|
+
],
|
|
928
|
+
isError: true,
|
|
586
929
|
};
|
|
587
930
|
}
|
|
588
931
|
});
|
|
@@ -600,31 +943,31 @@ function createMCPServer(options) {
|
|
|
600
943
|
*/
|
|
601
944
|
function makeAPIRequest(targetUrl, request) {
|
|
602
945
|
return new Promise((resolve, reject) => {
|
|
603
|
-
const client = targetUrl.protocol ===
|
|
946
|
+
const client = targetUrl.protocol === "https:" ? https_1.default : http_1.default;
|
|
604
947
|
const options = {
|
|
605
948
|
hostname: targetUrl.hostname,
|
|
606
949
|
port: targetUrl.port,
|
|
607
950
|
path: targetUrl.pathname + targetUrl.search,
|
|
608
951
|
method: request.method,
|
|
609
952
|
headers: {
|
|
610
|
-
|
|
611
|
-
...request.headers
|
|
612
|
-
}
|
|
953
|
+
"User-Agent": "janee/" + pkgVersion,
|
|
954
|
+
...request.headers,
|
|
955
|
+
},
|
|
613
956
|
};
|
|
614
957
|
const req = client.request(options, (res) => {
|
|
615
|
-
let body =
|
|
616
|
-
res.on(
|
|
958
|
+
let body = "";
|
|
959
|
+
res.on("data", (chunk) => {
|
|
617
960
|
body += chunk;
|
|
618
961
|
});
|
|
619
|
-
res.on(
|
|
962
|
+
res.on("end", () => {
|
|
620
963
|
resolve({
|
|
621
964
|
statusCode: res.statusCode || 500,
|
|
622
965
|
headers: res.headers,
|
|
623
|
-
body
|
|
966
|
+
body,
|
|
624
967
|
});
|
|
625
968
|
});
|
|
626
969
|
});
|
|
627
|
-
req.on(
|
|
970
|
+
req.on("error", (error) => {
|
|
628
971
|
reject(error);
|
|
629
972
|
});
|
|
630
973
|
if (request.body) {
|
|
@@ -641,8 +984,8 @@ function makeAPIRequest(targetUrl, request) {
|
|
|
641
984
|
function captureClientInfo(transport, clientSessions) {
|
|
642
985
|
const original = transport.onmessage;
|
|
643
986
|
transport.onmessage = (message, extra) => {
|
|
644
|
-
if (message?.method ===
|
|
645
|
-
const key = extra?.sessionId ||
|
|
987
|
+
if (message?.method === "initialize" && message?.params?.clientInfo?.name) {
|
|
988
|
+
const key = extra?.sessionId || "__default__";
|
|
646
989
|
clientSessions.set(key, message.params.clientInfo.name);
|
|
647
990
|
}
|
|
648
991
|
return original?.call(transport, message, extra);
|
|
@@ -657,7 +1000,7 @@ async function startMCPServer(serverOptions) {
|
|
|
657
1000
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
658
1001
|
await server.connect(transport);
|
|
659
1002
|
captureClientInfo(transport, clientSessions);
|
|
660
|
-
console.error(
|
|
1003
|
+
console.error("Janee MCP server started (stdio)");
|
|
661
1004
|
return mcpResult;
|
|
662
1005
|
}
|
|
663
1006
|
/**
|
|
@@ -679,83 +1022,103 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
679
1022
|
const sessions = new Map();
|
|
680
1023
|
// Authority REST endpoints -- active when runnerKey is provided
|
|
681
1024
|
if (httpOptions.runnerKey && httpOptions.authorityHooks) {
|
|
682
|
-
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require(
|
|
1025
|
+
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require("crypto")));
|
|
683
1026
|
const runnerKey = httpOptions.runnerKey;
|
|
684
1027
|
const hooks = httpOptions.authorityHooks;
|
|
685
1028
|
const authMiddleware = (req, res, next) => {
|
|
686
|
-
const provided = req.header(
|
|
687
|
-
if (!provided ||
|
|
1029
|
+
const provided = req.header("x-janee-runner-key");
|
|
1030
|
+
if (!provided ||
|
|
1031
|
+
provided.length !== runnerKey.length ||
|
|
688
1032
|
!timingSafeEqual(Buffer.from(provided), Buffer.from(runnerKey))) {
|
|
689
|
-
res.status(401).json({ error:
|
|
1033
|
+
res.status(401).json({ error: "Unauthorized runner request" });
|
|
690
1034
|
return;
|
|
691
1035
|
}
|
|
692
1036
|
next();
|
|
693
1037
|
};
|
|
694
|
-
app.get(
|
|
695
|
-
res.status(200).json({ ok: true, mode:
|
|
1038
|
+
app.get("/v1/health", (_req, res) => {
|
|
1039
|
+
res.status(200).json({ ok: true, mode: "authority" });
|
|
696
1040
|
});
|
|
697
|
-
app.post(
|
|
1041
|
+
app.post("/v1/exec/authorize", authMiddleware, async (req, res) => {
|
|
698
1042
|
try {
|
|
699
1043
|
const body = req.body;
|
|
700
|
-
if (!body?.runner?.runnerId ||
|
|
701
|
-
|
|
1044
|
+
if (!body?.runner?.runnerId ||
|
|
1045
|
+
!Array.isArray(body?.command) ||
|
|
1046
|
+
body.command.length === 0 ||
|
|
1047
|
+
!body.capabilityId) {
|
|
1048
|
+
res.status(400).json({ error: "Invalid authorize request" });
|
|
702
1049
|
return;
|
|
703
1050
|
}
|
|
704
1051
|
const response = await hooks.authorizeExec(body);
|
|
705
1052
|
res.status(200).json(response);
|
|
706
1053
|
}
|
|
707
1054
|
catch (error) {
|
|
708
|
-
res
|
|
1055
|
+
res
|
|
1056
|
+
.status(403)
|
|
1057
|
+
.json({
|
|
1058
|
+
error: error instanceof Error ? error.message : "Authorization failed",
|
|
1059
|
+
});
|
|
709
1060
|
}
|
|
710
1061
|
});
|
|
711
|
-
app.post(
|
|
1062
|
+
app.post("/v1/exec/complete", authMiddleware, async (req, res) => {
|
|
712
1063
|
try {
|
|
713
1064
|
if (!req.body?.grantId) {
|
|
714
|
-
res.status(400).json({ error:
|
|
1065
|
+
res.status(400).json({ error: "grantId is required" });
|
|
715
1066
|
return;
|
|
716
1067
|
}
|
|
717
1068
|
await hooks.completeExec(req.body);
|
|
718
1069
|
res.status(200).json({ ok: true });
|
|
719
1070
|
}
|
|
720
1071
|
catch (error) {
|
|
721
|
-
res
|
|
1072
|
+
res
|
|
1073
|
+
.status(500)
|
|
1074
|
+
.json({
|
|
1075
|
+
error: error instanceof Error ? error.message : "completion failed",
|
|
1076
|
+
});
|
|
722
1077
|
}
|
|
723
1078
|
});
|
|
724
1079
|
if (hooks.testService) {
|
|
725
|
-
app.post(
|
|
1080
|
+
app.post("/v1/test", authMiddleware, async (req, res) => {
|
|
726
1081
|
try {
|
|
727
|
-
const result = await hooks.testService(req.body?.service
|
|
1082
|
+
const result = await hooks.testService(req.body?.service, {
|
|
1083
|
+
timeout: req.body?.timeout,
|
|
1084
|
+
});
|
|
728
1085
|
res.status(200).json(result);
|
|
729
1086
|
}
|
|
730
1087
|
catch (error) {
|
|
731
|
-
res
|
|
1088
|
+
res
|
|
1089
|
+
.status(500)
|
|
1090
|
+
.json({
|
|
1091
|
+
error: error instanceof Error ? error.message : "Test failed",
|
|
1092
|
+
});
|
|
732
1093
|
}
|
|
733
1094
|
});
|
|
734
1095
|
}
|
|
735
1096
|
}
|
|
736
1097
|
// Sweep idle sessions every 60 seconds
|
|
737
|
-
const idleSweepInterval = idleTimeoutMs > 0
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1098
|
+
const idleSweepInterval = idleTimeoutMs > 0
|
|
1099
|
+
? setInterval(async () => {
|
|
1100
|
+
const now = Date.now();
|
|
1101
|
+
for (const [sid, session] of sessions.entries()) {
|
|
1102
|
+
if (now - session.lastActivityAt > idleTimeoutMs) {
|
|
1103
|
+
console.error(`Closing idle session ${sid} (inactive for ${Math.round((now - session.lastActivityAt) / 1000)}s)`);
|
|
1104
|
+
sessions.delete(sid);
|
|
1105
|
+
try {
|
|
1106
|
+
await session.transport.close?.();
|
|
1107
|
+
await session.server.close();
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
// best-effort cleanup
|
|
1111
|
+
}
|
|
749
1112
|
}
|
|
750
1113
|
}
|
|
751
|
-
}
|
|
752
|
-
|
|
1114
|
+
}, Math.min(idleTimeoutMs, 60_000))
|
|
1115
|
+
: undefined;
|
|
753
1116
|
if (idleSweepInterval) {
|
|
754
1117
|
idleSweepInterval.unref?.(); // Don't prevent Node from exiting
|
|
755
1118
|
}
|
|
756
|
-
app.all(
|
|
1119
|
+
app.all("/mcp", async (req, res) => {
|
|
757
1120
|
try {
|
|
758
|
-
const sessionId = req.headers[
|
|
1121
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
759
1122
|
if (sessionId && sessions.has(sessionId)) {
|
|
760
1123
|
const session = sessions.get(sessionId);
|
|
761
1124
|
session.lastActivityAt = Date.now();
|
|
@@ -768,7 +1131,11 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
768
1131
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
769
1132
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
770
1133
|
onsessioninitialized: (sid) => {
|
|
771
|
-
sessions.set(sid, {
|
|
1134
|
+
sessions.set(sid, {
|
|
1135
|
+
transport,
|
|
1136
|
+
server,
|
|
1137
|
+
lastActivityAt: Date.now(),
|
|
1138
|
+
});
|
|
772
1139
|
if (clientName)
|
|
773
1140
|
clientSessions.set(sid, clientName);
|
|
774
1141
|
},
|
|
@@ -788,25 +1155,28 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
788
1155
|
}
|
|
789
1156
|
else if (sessionId) {
|
|
790
1157
|
res.status(404).json({
|
|
791
|
-
jsonrpc:
|
|
792
|
-
error: { code: -32001, message:
|
|
1158
|
+
jsonrpc: "2.0",
|
|
1159
|
+
error: { code: -32001, message: "Session not found" },
|
|
793
1160
|
id: null,
|
|
794
1161
|
});
|
|
795
1162
|
}
|
|
796
1163
|
else {
|
|
797
1164
|
res.status(400).json({
|
|
798
|
-
jsonrpc:
|
|
799
|
-
error: {
|
|
1165
|
+
jsonrpc: "2.0",
|
|
1166
|
+
error: {
|
|
1167
|
+
code: -32600,
|
|
1168
|
+
message: "Bad Request: Missing session ID or not an initialize request",
|
|
1169
|
+
},
|
|
800
1170
|
id: null,
|
|
801
1171
|
});
|
|
802
1172
|
}
|
|
803
1173
|
}
|
|
804
1174
|
catch (error) {
|
|
805
|
-
console.error(
|
|
1175
|
+
console.error("Error handling MCP request:", error);
|
|
806
1176
|
if (!res.headersSent) {
|
|
807
1177
|
res.status(500).json({
|
|
808
|
-
jsonrpc:
|
|
809
|
-
error: { code: -32603, message:
|
|
1178
|
+
jsonrpc: "2.0",
|
|
1179
|
+
error: { code: -32603, message: "Internal server error" },
|
|
810
1180
|
id: null,
|
|
811
1181
|
});
|
|
812
1182
|
}
|