@true-and-useful/janee 0.11.3 → 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/add.d.ts +1 -0
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +23 -5
- package/dist/cli/commands/add.js.map +1 -1
- 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/serve-mcp.d.ts.map +1 -1
- package/dist/cli/commands/serve-mcp.js +46 -97
- 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/test.d.ts +6 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +101 -0
- package/dist/cli/commands/test.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 +15 -15
- package/dist/cli/config-yaml.d.ts.map +1 -1
- package/dist/cli/config-yaml.js +213 -90
- package/dist/cli/config-yaml.js.map +1 -1
- package/dist/cli/index.js +97 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/core/auth.d.ts +25 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +136 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/authority.d.ts +5 -1
- package/dist/core/authority.d.ts.map +1 -1
- package/dist/core/authority.js +93 -38
- package/dist/core/authority.js.map +1 -1
- package/dist/core/directory.d.ts +2 -0
- package/dist/core/directory.d.ts.map +1 -1
- package/dist/core/directory.js +23 -0
- package/dist/core/directory.js.map +1 -1
- package/dist/core/health.d.ts +62 -1
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +242 -8
- package/dist/core/health.js.map +1 -1
- package/dist/core/mcp-server.d.ts +36 -12
- package/dist/core/mcp-server.d.ts.map +1 -1
- package/dist/core/mcp-server.js +635 -235
- package/dist/core/mcp-server.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/package.json +8 -1
package/dist/core/mcp-server.js
CHANGED
|
@@ -40,26 +40,38 @@ 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;
|
|
46
48
|
exports.startMCPServer = startMCPServer;
|
|
47
49
|
exports.startMCPServerHTTP = startMCPServerHTTP;
|
|
50
|
+
const express_1 = __importDefault(require("express"));
|
|
51
|
+
const fs_1 = require("fs");
|
|
52
|
+
const http_1 = __importDefault(require("http"));
|
|
53
|
+
const https_1 = __importDefault(require("https"));
|
|
54
|
+
const path_1 = require("path");
|
|
48
55
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
49
56
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
50
57
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
51
58
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
52
|
-
const rules_js_1 = require("./rules.js");
|
|
53
|
-
const http_1 = __importDefault(require("http"));
|
|
54
|
-
const https_1 = __importDefault(require("https"));
|
|
55
|
-
const express_1 = __importDefault(require("express"));
|
|
56
|
-
const exec_js_1 = require("./exec.js");
|
|
57
|
-
const fs_1 = require("fs");
|
|
58
59
|
const agent_scope_js_1 = require("./agent-scope.js");
|
|
59
|
-
const
|
|
60
|
+
const exec_js_1 = require("./exec.js");
|
|
61
|
+
const health_js_1 = require("./health.js");
|
|
62
|
+
const rules_js_1 = require("./rules.js");
|
|
60
63
|
// Read version from package.json
|
|
61
|
-
const packageJsonPath = (0, path_1.join)(__dirname,
|
|
62
|
-
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;
|
|
63
75
|
/**
|
|
64
76
|
* Check whether an agent can access a capability.
|
|
65
77
|
* Checks capability-level allowedAgents first, then falls back to
|
|
@@ -73,11 +85,40 @@ function canAccessCapability(agentId, cap, service, defaultAccessPolicy) {
|
|
|
73
85
|
if (cap.allowedAgents && cap.allowedAgents.length > 0) {
|
|
74
86
|
return cap.allowedAgents.includes(agentId);
|
|
75
87
|
}
|
|
76
|
-
if (defaultAccessPolicy ===
|
|
88
|
+
if (defaultAccessPolicy === "restricted") {
|
|
77
89
|
return false;
|
|
78
90
|
}
|
|
79
91
|
return (0, agent_scope_js_1.canAgentAccess)(agentId, service?.ownership);
|
|
80
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
|
+
}
|
|
81
122
|
/**
|
|
82
123
|
* Parse TTL string to seconds
|
|
83
124
|
*/
|
|
@@ -91,7 +132,7 @@ function parseTTL(ttl) {
|
|
|
91
132
|
s: 1,
|
|
92
133
|
m: 60,
|
|
93
134
|
h: 3600,
|
|
94
|
-
d: 86400
|
|
135
|
+
d: 86400,
|
|
95
136
|
};
|
|
96
137
|
return value * multipliers[unit];
|
|
97
138
|
}
|
|
@@ -99,7 +140,7 @@ function parseTTL(ttl) {
|
|
|
99
140
|
* Create and start MCP server
|
|
100
141
|
*/
|
|
101
142
|
function createMCPServer(options) {
|
|
102
|
-
const { sessionManager, auditLogger, defaultAccess, onExecute, onExecCommand, onReloadConfig, onPersistOwnership, onForwardToolCall } = options;
|
|
143
|
+
const { sessionManager, auditLogger, defaultAccess, onExecute, onExecCommand, onReloadConfig, onPersistOwnership, onForwardToolCall, } = options;
|
|
103
144
|
// Store as mutable to support hot-reloading
|
|
104
145
|
let capabilities = options.capabilities;
|
|
105
146
|
let services = options.services;
|
|
@@ -107,12 +148,12 @@ function createMCPServer(options) {
|
|
|
107
148
|
// Populated by captureClientInfo() after connecting a transport.
|
|
108
149
|
const clientSessions = new Map();
|
|
109
150
|
const server = new index_js_1.Server({
|
|
110
|
-
name:
|
|
111
|
-
version: pkgVersion
|
|
151
|
+
name: "janee",
|
|
152
|
+
version: pkgVersion,
|
|
112
153
|
}, {
|
|
113
154
|
capabilities: {
|
|
114
|
-
tools: {}
|
|
115
|
-
}
|
|
155
|
+
tools: {},
|
|
156
|
+
},
|
|
116
157
|
});
|
|
117
158
|
/**
|
|
118
159
|
* Resolve agent identity from the MCP session.
|
|
@@ -122,126 +163,192 @@ function createMCPServer(options) {
|
|
|
122
163
|
* Falls back to args.agentId for legacy scenarios.
|
|
123
164
|
*/
|
|
124
165
|
function resolveAgentFromRequest(extra, args) {
|
|
125
|
-
const sessionKey = extra?.sessionId ||
|
|
126
|
-
const clientName = clientSessions.get(sessionKey) || clientSessions.get(
|
|
127
|
-
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);
|
|
128
172
|
}
|
|
129
173
|
// Tool: list_services
|
|
130
174
|
const listServicesTool = {
|
|
131
|
-
name:
|
|
132
|
-
description:
|
|
175
|
+
name: "list_services",
|
|
176
|
+
description: "List available API capabilities managed by Janee",
|
|
133
177
|
inputSchema: {
|
|
134
|
-
type:
|
|
178
|
+
type: "object",
|
|
135
179
|
properties: {},
|
|
136
|
-
required: []
|
|
137
|
-
}
|
|
180
|
+
required: [],
|
|
181
|
+
},
|
|
138
182
|
};
|
|
139
183
|
// Tool: execute
|
|
140
184
|
const executeTool = {
|
|
141
|
-
name:
|
|
142
|
-
description:
|
|
185
|
+
name: "execute",
|
|
186
|
+
description: "Execute an API request through Janee proxy",
|
|
143
187
|
inputSchema: {
|
|
144
|
-
type:
|
|
188
|
+
type: "object",
|
|
145
189
|
properties: {
|
|
146
190
|
capability: {
|
|
147
|
-
type:
|
|
148
|
-
description:
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Capability name to use (from list_services)",
|
|
149
193
|
},
|
|
150
194
|
method: {
|
|
151
|
-
type:
|
|
152
|
-
enum: [
|
|
153
|
-
description:
|
|
195
|
+
type: "string",
|
|
196
|
+
enum: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
197
|
+
description: "HTTP method",
|
|
154
198
|
},
|
|
155
199
|
path: {
|
|
156
|
-
type:
|
|
157
|
-
description:
|
|
200
|
+
type: "string",
|
|
201
|
+
description: "API path (e.g., /v1/customers)",
|
|
158
202
|
},
|
|
159
203
|
body: {
|
|
160
|
-
type:
|
|
161
|
-
description:
|
|
204
|
+
type: "string",
|
|
205
|
+
description: "Request body (JSON string, optional)",
|
|
162
206
|
},
|
|
163
207
|
headers: {
|
|
164
|
-
type:
|
|
165
|
-
description:
|
|
166
|
-
additionalProperties: { type:
|
|
208
|
+
type: "object",
|
|
209
|
+
description: "Additional headers (optional)",
|
|
210
|
+
additionalProperties: { type: "string" },
|
|
167
211
|
},
|
|
168
212
|
reason: {
|
|
169
|
-
type:
|
|
170
|
-
description:
|
|
171
|
-
}
|
|
213
|
+
type: "string",
|
|
214
|
+
description: "Reason for this request (required for some capabilities)",
|
|
215
|
+
},
|
|
172
216
|
},
|
|
173
|
-
required: [
|
|
174
|
-
}
|
|
217
|
+
required: ["capability", "method", "path"],
|
|
218
|
+
},
|
|
175
219
|
};
|
|
176
220
|
// Tool: reload_config
|
|
177
221
|
const reloadConfigTool = {
|
|
178
|
-
name:
|
|
179
|
-
description:
|
|
222
|
+
name: "reload_config",
|
|
223
|
+
description: "Reload Janee configuration from disk without restarting the server. Use after adding new services or capabilities.",
|
|
180
224
|
inputSchema: {
|
|
181
|
-
type:
|
|
225
|
+
type: "object",
|
|
182
226
|
properties: {},
|
|
183
|
-
required: []
|
|
184
|
-
}
|
|
227
|
+
required: [],
|
|
228
|
+
},
|
|
185
229
|
};
|
|
186
230
|
// Tool: janee_exec (RFC 0001 - Secure CLI Execution)
|
|
187
231
|
const execTool = {
|
|
188
|
-
name:
|
|
189
|
-
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.",
|
|
190
234
|
inputSchema: {
|
|
191
|
-
type:
|
|
235
|
+
type: "object",
|
|
192
236
|
properties: {
|
|
193
237
|
capability: {
|
|
194
|
-
type:
|
|
195
|
-
description:
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "Capability name (must be exec mode, from list_services)",
|
|
196
240
|
},
|
|
197
241
|
command: {
|
|
198
|
-
type:
|
|
199
|
-
items: { type:
|
|
200
|
-
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"]',
|
|
201
245
|
},
|
|
202
246
|
cwd: {
|
|
203
|
-
type:
|
|
204
|
-
description:
|
|
247
|
+
type: "string",
|
|
248
|
+
description: "Working directory for the command (defaults to process cwd)",
|
|
205
249
|
},
|
|
206
250
|
stdin: {
|
|
207
|
-
type:
|
|
208
|
-
description:
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Optional stdin input to pipe to the command",
|
|
209
253
|
},
|
|
210
254
|
reason: {
|
|
211
|
-
type:
|
|
212
|
-
description:
|
|
213
|
-
}
|
|
255
|
+
type: "string",
|
|
256
|
+
description: "Reason for this execution (required for some capabilities)",
|
|
257
|
+
},
|
|
214
258
|
},
|
|
215
|
-
required: [
|
|
216
|
-
}
|
|
259
|
+
required: ["capability", "command"],
|
|
260
|
+
},
|
|
217
261
|
};
|
|
218
262
|
// Tool: manage_credential (agent-scoped credential access control)
|
|
219
263
|
const manageCredentialTool = {
|
|
220
|
-
name:
|
|
221
|
-
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.",
|
|
222
266
|
inputSchema: {
|
|
223
|
-
type:
|
|
267
|
+
type: "object",
|
|
224
268
|
properties: {
|
|
225
269
|
action: {
|
|
226
|
-
type:
|
|
227
|
-
enum: [
|
|
228
|
-
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",
|
|
229
273
|
},
|
|
230
274
|
service: {
|
|
231
|
-
type:
|
|
232
|
-
description:
|
|
275
|
+
type: "string",
|
|
276
|
+
description: "Service name to manage",
|
|
233
277
|
},
|
|
234
278
|
targetAgentId: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "Agent ID to grant/revoke access for (required for grant/revoke actions)",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
required: ["action", "service"],
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
// Tool: test_service
|
|
287
|
+
const testServiceTool = {
|
|
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.",
|
|
290
|
+
inputSchema: {
|
|
291
|
+
type: "object",
|
|
292
|
+
properties: {
|
|
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: {
|
|
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: {
|
|
235
335
|
type: 'string',
|
|
236
|
-
description: '
|
|
336
|
+
description: 'Request path for rules evaluation (optional, only applies to proxy capabilities).'
|
|
237
337
|
}
|
|
238
338
|
},
|
|
239
|
-
required: ['
|
|
339
|
+
required: ['capability']
|
|
240
340
|
}
|
|
241
341
|
};
|
|
242
342
|
// Register tools
|
|
243
343
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
244
|
-
const tools = [
|
|
344
|
+
const tools = [
|
|
345
|
+
listServicesTool,
|
|
346
|
+
executeTool,
|
|
347
|
+
manageCredentialTool,
|
|
348
|
+
testServiceTool,
|
|
349
|
+
whoamiTool,
|
|
350
|
+
explainAccessTool,
|
|
351
|
+
];
|
|
245
352
|
if (onExecCommand && !options.hideExecTool) {
|
|
246
353
|
tools.push(execTool);
|
|
247
354
|
}
|
|
@@ -254,42 +361,40 @@ function createMCPServer(options) {
|
|
|
254
361
|
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
|
|
255
362
|
const { name, arguments: args } = request.params;
|
|
256
363
|
try {
|
|
257
|
-
// Runner proxy: forward non-exec tools to the Authority
|
|
258
|
-
if (onForwardToolCall && name !==
|
|
364
|
+
// Runner proxy: forward all non-exec tools to the Authority
|
|
365
|
+
if (onForwardToolCall && name !== "janee_exec") {
|
|
259
366
|
const forwardAgentId = resolveAgentFromRequest(extra, args);
|
|
260
367
|
const result = await onForwardToolCall(name, (args || {}), forwardAgentId);
|
|
261
368
|
return result;
|
|
262
369
|
}
|
|
263
370
|
switch (name) {
|
|
264
|
-
case
|
|
371
|
+
case "list_services": {
|
|
265
372
|
const listAgentId = resolveAgentFromRequest(extra, args);
|
|
266
373
|
return {
|
|
267
|
-
content: [
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return canAccessCapability(listAgentId, cap, svc, defaultAccess);
|
|
273
|
-
})
|
|
274
|
-
.map(cap => ({
|
|
374
|
+
content: [
|
|
375
|
+
{
|
|
376
|
+
type: "text",
|
|
377
|
+
text: JSON.stringify(capabilities.map((cap) => ({
|
|
378
|
+
accessible: canAccessCapability(listAgentId, cap, services.get(cap.service), defaultAccess),
|
|
275
379
|
name: cap.name,
|
|
276
380
|
service: cap.service,
|
|
277
|
-
mode: cap.mode ||
|
|
381
|
+
mode: cap.mode || "proxy",
|
|
278
382
|
ttl: cap.ttl,
|
|
279
383
|
autoApprove: cap.autoApprove,
|
|
280
384
|
requiresReason: cap.requiresReason,
|
|
281
385
|
rules: cap.rules,
|
|
282
|
-
...(cap.mode ===
|
|
386
|
+
...(cap.mode === "exec" && {
|
|
283
387
|
allowCommands: cap.allowCommands,
|
|
284
388
|
env: cap.env ? Object.keys(cap.env) : undefined,
|
|
285
|
-
})
|
|
286
|
-
})), null, 2)
|
|
287
|
-
}
|
|
389
|
+
}),
|
|
390
|
+
})), null, 2),
|
|
391
|
+
},
|
|
392
|
+
],
|
|
288
393
|
};
|
|
289
394
|
}
|
|
290
|
-
case
|
|
395
|
+
case "reload_config": {
|
|
291
396
|
if (!onReloadConfig) {
|
|
292
|
-
throw new Error(
|
|
397
|
+
throw new Error("Config reload not supported");
|
|
293
398
|
}
|
|
294
399
|
try {
|
|
295
400
|
const result = onReloadConfig();
|
|
@@ -298,63 +403,93 @@ function createMCPServer(options) {
|
|
|
298
403
|
capabilities = result.capabilities;
|
|
299
404
|
services = result.services;
|
|
300
405
|
return {
|
|
301
|
-
content: [
|
|
302
|
-
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
303
409
|
text: JSON.stringify({
|
|
304
410
|
success: true,
|
|
305
|
-
message:
|
|
411
|
+
message: "Configuration reloaded successfully",
|
|
306
412
|
services: services.size,
|
|
307
413
|
capabilities: capabilities.length,
|
|
308
414
|
changes: {
|
|
309
415
|
services: services.size - prevServiceCount,
|
|
310
|
-
capabilities: capabilities.length - prevCapCount
|
|
311
|
-
}
|
|
312
|
-
}, null, 2)
|
|
313
|
-
}
|
|
416
|
+
capabilities: capabilities.length - prevCapCount,
|
|
417
|
+
},
|
|
418
|
+
}, null, 2),
|
|
419
|
+
},
|
|
420
|
+
],
|
|
314
421
|
};
|
|
315
422
|
}
|
|
316
423
|
catch (error) {
|
|
317
|
-
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"}`);
|
|
318
425
|
}
|
|
319
426
|
}
|
|
320
|
-
case
|
|
427
|
+
case "execute": {
|
|
321
428
|
const { capability, method, path, body, headers, reason } = args;
|
|
322
429
|
// Validate required arguments
|
|
323
430
|
if (!capability) {
|
|
324
|
-
throw new Error(
|
|
431
|
+
throw new Error("Missing required argument: capability");
|
|
325
432
|
}
|
|
326
433
|
if (!method) {
|
|
327
|
-
throw new Error(
|
|
434
|
+
throw new Error("Missing required argument: method (GET, POST, PUT, DELETE, etc.)");
|
|
328
435
|
}
|
|
329
436
|
if (!path) {
|
|
330
|
-
throw new Error(
|
|
437
|
+
throw new Error("Missing required argument: path");
|
|
331
438
|
}
|
|
332
439
|
// Find capability
|
|
333
|
-
const cap = capabilities.find(c => c.name === capability);
|
|
440
|
+
const cap = capabilities.find((c) => c.name === capability);
|
|
334
441
|
if (!cap) {
|
|
335
|
-
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
|
+
});
|
|
336
447
|
}
|
|
337
448
|
// Reject exec-mode capabilities — they should use janee_exec instead
|
|
338
|
-
if (cap.mode ===
|
|
339
|
-
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
|
+
});
|
|
340
455
|
}
|
|
341
456
|
// Check if reason required
|
|
342
457
|
if (cap.requiresReason && !reason) {
|
|
343
|
-
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
|
+
});
|
|
344
463
|
}
|
|
345
464
|
// Check rules (path-based policies)
|
|
346
465
|
const ruleCheck = (0, rules_js_1.checkRules)(cap.rules, method, path);
|
|
347
466
|
if (!ruleCheck.allowed) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
+
});
|
|
351
475
|
}
|
|
352
476
|
// Check agent-scoped access (capability-level allowedAgents, then service-level ownership)
|
|
353
477
|
const executeAgentId = resolveAgentFromRequest(extra, args);
|
|
354
478
|
const executeSvc = services.get(cap.service);
|
|
355
479
|
if (!canAccessCapability(executeAgentId, cap, executeSvc, defaultAccess)) {
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
});
|
|
358
493
|
}
|
|
359
494
|
// Get or create session
|
|
360
495
|
const ttlSeconds = parseTTL(cap.ttl);
|
|
@@ -365,34 +500,38 @@ function createMCPServer(options) {
|
|
|
365
500
|
path,
|
|
366
501
|
method,
|
|
367
502
|
headers: headers || {},
|
|
368
|
-
body
|
|
503
|
+
body,
|
|
369
504
|
};
|
|
370
505
|
// Execute
|
|
371
506
|
const response = await onExecute(session, apiReq);
|
|
372
507
|
return {
|
|
373
|
-
content: [
|
|
374
|
-
|
|
508
|
+
content: [
|
|
509
|
+
{
|
|
510
|
+
type: "text",
|
|
375
511
|
text: JSON.stringify({
|
|
376
512
|
status: response.statusCode,
|
|
377
|
-
body: response.body
|
|
378
|
-
}, null, 2)
|
|
379
|
-
}
|
|
513
|
+
body: response.body,
|
|
514
|
+
}, null, 2),
|
|
515
|
+
},
|
|
516
|
+
],
|
|
380
517
|
};
|
|
381
518
|
}
|
|
382
|
-
case
|
|
519
|
+
case "janee_exec": {
|
|
383
520
|
if (!onExecCommand) {
|
|
384
|
-
throw new Error(
|
|
521
|
+
throw new Error("CLI execution not supported in this configuration");
|
|
385
522
|
}
|
|
386
|
-
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;
|
|
387
524
|
if (!execCapName) {
|
|
388
|
-
throw new Error(
|
|
525
|
+
throw new Error("Missing required argument: capability");
|
|
389
526
|
}
|
|
390
|
-
if (!rawExecCommand ||
|
|
391
|
-
|
|
527
|
+
if (!rawExecCommand ||
|
|
528
|
+
(Array.isArray(rawExecCommand) && rawExecCommand.length === 0) ||
|
|
529
|
+
(typeof rawExecCommand === "string" && rawExecCommand.trim() === "")) {
|
|
530
|
+
throw new Error("Missing required argument: command");
|
|
392
531
|
}
|
|
393
532
|
const execCommand = Array.isArray(rawExecCommand)
|
|
394
533
|
? rawExecCommand
|
|
395
|
-
: typeof rawExecCommand ===
|
|
534
|
+
: typeof rawExecCommand === "string"
|
|
396
535
|
? rawExecCommand.trim().split(/\s+/)
|
|
397
536
|
: [];
|
|
398
537
|
let execCap;
|
|
@@ -400,32 +539,67 @@ function createMCPServer(options) {
|
|
|
400
539
|
if (onForwardToolCall) {
|
|
401
540
|
// Runner mode: Authority handles validation and credential injection.
|
|
402
541
|
// Build a minimal capability stub so onExecCommand has the name.
|
|
403
|
-
execCap = {
|
|
542
|
+
execCap = {
|
|
543
|
+
name: execCapName,
|
|
544
|
+
service: "",
|
|
545
|
+
ttl: "1h",
|
|
546
|
+
mode: "exec",
|
|
547
|
+
workDir: execCwd,
|
|
548
|
+
};
|
|
404
549
|
execSession = { agentId: resolveAgentFromRequest(extra, args) };
|
|
405
550
|
}
|
|
406
551
|
else {
|
|
407
552
|
// Standalone mode: validate locally
|
|
408
|
-
const foundCap = capabilities.find(c => c.name === execCapName);
|
|
553
|
+
const foundCap = capabilities.find((c) => c.name === execCapName);
|
|
409
554
|
if (!foundCap) {
|
|
410
|
-
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
|
+
});
|
|
411
560
|
}
|
|
412
561
|
execCap = execCwd ? { ...foundCap, workDir: execCwd } : foundCap;
|
|
413
|
-
if (execCap.mode !==
|
|
414
|
-
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
|
+
});
|
|
415
568
|
}
|
|
416
569
|
const execAgentId = resolveAgentFromRequest(extra, args);
|
|
417
570
|
const execSvc = services.get(execCap.service);
|
|
418
571
|
if (!canAccessCapability(execAgentId, execCap, execSvc, defaultAccess)) {
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
});
|
|
421
585
|
}
|
|
422
586
|
if (execCap.requiresReason && !execReason) {
|
|
423
|
-
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
|
+
});
|
|
424
592
|
}
|
|
425
593
|
const cmdValidation = (0, exec_js_1.validateCommand)(execCommand, execCap.allowCommands || []);
|
|
426
594
|
if (!cmdValidation.allowed) {
|
|
427
|
-
auditLogger.logDenied(execCap.service,
|
|
428
|
-
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
|
+
});
|
|
429
603
|
}
|
|
430
604
|
const execTtlSeconds = parseTTL(execCap.ttl);
|
|
431
605
|
execSession = sessionManager.createSession(execCap.name, execCap.service, execTtlSeconds, { reason: execReason });
|
|
@@ -434,153 +608,338 @@ function createMCPServer(options) {
|
|
|
434
608
|
// Log to audit
|
|
435
609
|
auditLogger.log({
|
|
436
610
|
service: execCap.service,
|
|
437
|
-
path: execCommand.join(
|
|
438
|
-
method:
|
|
439
|
-
headers: {
|
|
611
|
+
path: execCommand.join(" "),
|
|
612
|
+
method: "EXEC",
|
|
613
|
+
headers: { "x-janee-reason": execReason || "" },
|
|
440
614
|
}, {
|
|
441
615
|
statusCode: execResult.exitCode === 0 ? 200 : 500,
|
|
442
616
|
headers: {},
|
|
443
617
|
body: execResult.stdout,
|
|
444
618
|
}, execResult.executionTimeMs);
|
|
445
619
|
return {
|
|
446
|
-
content: [
|
|
447
|
-
|
|
620
|
+
content: [
|
|
621
|
+
{
|
|
622
|
+
type: "text",
|
|
448
623
|
text: JSON.stringify({
|
|
449
624
|
exitCode: execResult.exitCode,
|
|
450
625
|
stdout: execResult.stdout,
|
|
451
626
|
stderr: execResult.stderr,
|
|
452
627
|
executionTimeMs: execResult.executionTimeMs,
|
|
453
|
-
executionTarget:
|
|
454
|
-
}, null, 2)
|
|
455
|
-
}
|
|
628
|
+
executionTarget: "runner",
|
|
629
|
+
}, null, 2),
|
|
630
|
+
},
|
|
631
|
+
],
|
|
456
632
|
};
|
|
457
633
|
}
|
|
458
|
-
case
|
|
459
|
-
const { action: credAction, service: credService, targetAgentId: credTarget } = args;
|
|
634
|
+
case "manage_credential": {
|
|
635
|
+
const { action: credAction, service: credService, targetAgentId: credTarget, } = args;
|
|
460
636
|
const credAgentId = resolveAgentFromRequest(extra, args);
|
|
461
637
|
if (!credService) {
|
|
462
|
-
throw new Error(
|
|
638
|
+
throw new Error("Missing required argument: service");
|
|
463
639
|
}
|
|
464
640
|
const svc = services.get(credService);
|
|
465
641
|
if (!svc) {
|
|
466
642
|
throw new Error(`Unknown service: ${credService}`);
|
|
467
643
|
}
|
|
468
|
-
if (credAction ===
|
|
644
|
+
if (credAction === "view") {
|
|
469
645
|
return {
|
|
470
|
-
content: [
|
|
471
|
-
|
|
646
|
+
content: [
|
|
647
|
+
{
|
|
648
|
+
type: "text",
|
|
472
649
|
text: JSON.stringify({
|
|
473
650
|
service: credService,
|
|
474
|
-
ownership: svc.ownership || {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
],
|
|
478
659
|
};
|
|
479
660
|
}
|
|
480
661
|
// Grant/revoke require ownership verification
|
|
481
662
|
if (!credAgentId) {
|
|
482
|
-
throw new Error(
|
|
663
|
+
throw new Error("agentId is required for grant/revoke actions");
|
|
483
664
|
}
|
|
484
665
|
if (!svc.ownership) {
|
|
485
|
-
throw new Error(
|
|
666
|
+
throw new Error("Cannot manage access for legacy credentials without ownership metadata. Re-add the service to enable scoping.");
|
|
486
667
|
}
|
|
487
668
|
if (svc.ownership.createdBy !== credAgentId) {
|
|
488
|
-
throw new Error(
|
|
669
|
+
throw new Error("Only the credential owner can grant or revoke access");
|
|
489
670
|
}
|
|
490
|
-
if (credAction ===
|
|
671
|
+
if (credAction === "grant") {
|
|
491
672
|
if (!credTarget) {
|
|
492
|
-
throw new Error(
|
|
673
|
+
throw new Error("targetAgentId is required for grant action");
|
|
493
674
|
}
|
|
494
|
-
const { grantAccess } = await Promise.resolve().then(() => __importStar(require(
|
|
675
|
+
const { grantAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
495
676
|
svc.ownership = grantAccess(svc.ownership, credTarget);
|
|
496
677
|
// Persist ownership change to config storage
|
|
497
678
|
if (onPersistOwnership) {
|
|
498
679
|
onPersistOwnership(credService, svc.ownership);
|
|
499
680
|
}
|
|
500
681
|
return {
|
|
501
|
-
content: [
|
|
502
|
-
|
|
682
|
+
content: [
|
|
683
|
+
{
|
|
684
|
+
type: "text",
|
|
503
685
|
text: JSON.stringify({
|
|
504
686
|
success: true,
|
|
505
687
|
message: `Granted access to ${credTarget}`,
|
|
506
688
|
ownership: svc.ownership,
|
|
507
|
-
persisted: !!onPersistOwnership
|
|
508
|
-
}, null, 2)
|
|
509
|
-
}
|
|
689
|
+
persisted: !!onPersistOwnership,
|
|
690
|
+
}, null, 2),
|
|
691
|
+
},
|
|
692
|
+
],
|
|
510
693
|
};
|
|
511
694
|
}
|
|
512
|
-
if (credAction ===
|
|
695
|
+
if (credAction === "revoke") {
|
|
513
696
|
if (!credTarget) {
|
|
514
|
-
throw new Error(
|
|
697
|
+
throw new Error("targetAgentId is required for revoke action");
|
|
515
698
|
}
|
|
516
|
-
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require(
|
|
699
|
+
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
517
700
|
svc.ownership = revokeAccess(svc.ownership, credTarget);
|
|
518
701
|
// Persist ownership change to config storage
|
|
519
702
|
if (onPersistOwnership) {
|
|
520
703
|
onPersistOwnership(credService, svc.ownership);
|
|
521
704
|
}
|
|
522
705
|
return {
|
|
523
|
-
content: [
|
|
524
|
-
|
|
706
|
+
content: [
|
|
707
|
+
{
|
|
708
|
+
type: "text",
|
|
525
709
|
text: JSON.stringify({
|
|
526
710
|
success: true,
|
|
527
711
|
message: `Revoked access from ${credTarget}`,
|
|
528
712
|
ownership: svc.ownership,
|
|
529
|
-
persisted: !!onPersistOwnership
|
|
713
|
+
persisted: !!onPersistOwnership,
|
|
714
|
+
}, null, 2),
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
throw new Error(`Unknown action: ${credAction}. Use 'view', 'grant', or 'revoke'.`);
|
|
720
|
+
}
|
|
721
|
+
case "test_service": {
|
|
722
|
+
const { service: testSvcName, timeout: testTimeout } = (args ||
|
|
723
|
+
{});
|
|
724
|
+
const testOpts = testTimeout ? { timeout: testTimeout } : {};
|
|
725
|
+
let targets;
|
|
726
|
+
if (testSvcName) {
|
|
727
|
+
const svc = services.get(testSvcName);
|
|
728
|
+
if (!svc) {
|
|
729
|
+
throw new Error(`Unknown service: ${testSvcName}. Use list_services to see available services.`);
|
|
730
|
+
}
|
|
731
|
+
targets = [[testSvcName, svc]];
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
targets = Array.from(services.entries());
|
|
735
|
+
}
|
|
736
|
+
if (targets.length === 0) {
|
|
737
|
+
throw new Error("No services configured");
|
|
738
|
+
}
|
|
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.`
|
|
530
765
|
}, null, 2)
|
|
531
766
|
}]
|
|
532
767
|
};
|
|
533
768
|
}
|
|
534
|
-
|
|
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);
|
|
863
|
+
return {
|
|
864
|
+
content: [{
|
|
865
|
+
type: 'text',
|
|
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)
|
|
879
|
+
}]
|
|
880
|
+
};
|
|
535
881
|
}
|
|
536
882
|
default:
|
|
537
883
|
throw new Error(`Unknown tool: ${name}`);
|
|
538
884
|
}
|
|
539
885
|
}
|
|
540
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
|
+
}
|
|
541
893
|
return {
|
|
542
|
-
content: [
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
isError: true
|
|
894
|
+
content: [
|
|
895
|
+
{
|
|
896
|
+
type: "text",
|
|
897
|
+
text: JSON.stringify(payload, null, 2),
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
isError: true,
|
|
549
901
|
};
|
|
550
902
|
}
|
|
551
903
|
});
|
|
552
|
-
return {
|
|
904
|
+
return {
|
|
905
|
+
server,
|
|
906
|
+
clientSessions,
|
|
907
|
+
reloadConfig: (result) => {
|
|
908
|
+
capabilities = result.capabilities;
|
|
909
|
+
services = result.services;
|
|
910
|
+
},
|
|
911
|
+
};
|
|
553
912
|
}
|
|
554
913
|
/**
|
|
555
914
|
* Make HTTP/HTTPS request to real API
|
|
556
915
|
*/
|
|
557
916
|
function makeAPIRequest(targetUrl, request) {
|
|
558
917
|
return new Promise((resolve, reject) => {
|
|
559
|
-
const client = targetUrl.protocol ===
|
|
918
|
+
const client = targetUrl.protocol === "https:" ? https_1.default : http_1.default;
|
|
560
919
|
const options = {
|
|
561
920
|
hostname: targetUrl.hostname,
|
|
562
921
|
port: targetUrl.port,
|
|
563
922
|
path: targetUrl.pathname + targetUrl.search,
|
|
564
923
|
method: request.method,
|
|
565
924
|
headers: {
|
|
566
|
-
|
|
567
|
-
...request.headers
|
|
568
|
-
}
|
|
925
|
+
"User-Agent": "janee/" + pkgVersion,
|
|
926
|
+
...request.headers,
|
|
927
|
+
},
|
|
569
928
|
};
|
|
570
929
|
const req = client.request(options, (res) => {
|
|
571
|
-
let body =
|
|
572
|
-
res.on(
|
|
930
|
+
let body = "";
|
|
931
|
+
res.on("data", (chunk) => {
|
|
573
932
|
body += chunk;
|
|
574
933
|
});
|
|
575
|
-
res.on(
|
|
934
|
+
res.on("end", () => {
|
|
576
935
|
resolve({
|
|
577
936
|
statusCode: res.statusCode || 500,
|
|
578
937
|
headers: res.headers,
|
|
579
|
-
body
|
|
938
|
+
body,
|
|
580
939
|
});
|
|
581
940
|
});
|
|
582
941
|
});
|
|
583
|
-
req.on(
|
|
942
|
+
req.on("error", (error) => {
|
|
584
943
|
reject(error);
|
|
585
944
|
});
|
|
586
945
|
if (request.body) {
|
|
@@ -597,8 +956,8 @@ function makeAPIRequest(targetUrl, request) {
|
|
|
597
956
|
function captureClientInfo(transport, clientSessions) {
|
|
598
957
|
const original = transport.onmessage;
|
|
599
958
|
transport.onmessage = (message, extra) => {
|
|
600
|
-
if (message?.method ===
|
|
601
|
-
const key = extra?.sessionId ||
|
|
959
|
+
if (message?.method === "initialize" && message?.params?.clientInfo?.name) {
|
|
960
|
+
const key = extra?.sessionId || "__default__";
|
|
602
961
|
clientSessions.set(key, message.params.clientInfo.name);
|
|
603
962
|
}
|
|
604
963
|
return original?.call(transport, message, extra);
|
|
@@ -608,11 +967,13 @@ function captureClientInfo(transport, clientSessions) {
|
|
|
608
967
|
* Start MCP server with stdio transport (single session).
|
|
609
968
|
*/
|
|
610
969
|
async function startMCPServer(serverOptions) {
|
|
611
|
-
const
|
|
970
|
+
const mcpResult = createMCPServer(serverOptions);
|
|
971
|
+
const { server, clientSessions } = mcpResult;
|
|
612
972
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
613
973
|
await server.connect(transport);
|
|
614
974
|
captureClientInfo(transport, clientSessions);
|
|
615
|
-
console.error(
|
|
975
|
+
console.error("Janee MCP server started (stdio)");
|
|
976
|
+
return mcpResult;
|
|
616
977
|
}
|
|
617
978
|
/**
|
|
618
979
|
* Start MCP server with StreamableHTTP transport over HTTP.
|
|
@@ -633,72 +994,103 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
633
994
|
const sessions = new Map();
|
|
634
995
|
// Authority REST endpoints -- active when runnerKey is provided
|
|
635
996
|
if (httpOptions.runnerKey && httpOptions.authorityHooks) {
|
|
636
|
-
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require(
|
|
997
|
+
const { timingSafeEqual } = await Promise.resolve().then(() => __importStar(require("crypto")));
|
|
637
998
|
const runnerKey = httpOptions.runnerKey;
|
|
638
999
|
const hooks = httpOptions.authorityHooks;
|
|
639
1000
|
const authMiddleware = (req, res, next) => {
|
|
640
|
-
const provided = req.header(
|
|
641
|
-
if (!provided ||
|
|
1001
|
+
const provided = req.header("x-janee-runner-key");
|
|
1002
|
+
if (!provided ||
|
|
1003
|
+
provided.length !== runnerKey.length ||
|
|
642
1004
|
!timingSafeEqual(Buffer.from(provided), Buffer.from(runnerKey))) {
|
|
643
|
-
res.status(401).json({ error:
|
|
1005
|
+
res.status(401).json({ error: "Unauthorized runner request" });
|
|
644
1006
|
return;
|
|
645
1007
|
}
|
|
646
1008
|
next();
|
|
647
1009
|
};
|
|
648
|
-
app.get(
|
|
649
|
-
res.status(200).json({ ok: true, mode:
|
|
1010
|
+
app.get("/v1/health", (_req, res) => {
|
|
1011
|
+
res.status(200).json({ ok: true, mode: "authority" });
|
|
650
1012
|
});
|
|
651
|
-
app.post(
|
|
1013
|
+
app.post("/v1/exec/authorize", authMiddleware, async (req, res) => {
|
|
652
1014
|
try {
|
|
653
1015
|
const body = req.body;
|
|
654
|
-
if (!body?.runner?.runnerId ||
|
|
655
|
-
|
|
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" });
|
|
656
1021
|
return;
|
|
657
1022
|
}
|
|
658
1023
|
const response = await hooks.authorizeExec(body);
|
|
659
1024
|
res.status(200).json(response);
|
|
660
1025
|
}
|
|
661
1026
|
catch (error) {
|
|
662
|
-
res
|
|
1027
|
+
res
|
|
1028
|
+
.status(403)
|
|
1029
|
+
.json({
|
|
1030
|
+
error: error instanceof Error ? error.message : "Authorization failed",
|
|
1031
|
+
});
|
|
663
1032
|
}
|
|
664
1033
|
});
|
|
665
|
-
app.post(
|
|
1034
|
+
app.post("/v1/exec/complete", authMiddleware, async (req, res) => {
|
|
666
1035
|
try {
|
|
667
1036
|
if (!req.body?.grantId) {
|
|
668
|
-
res.status(400).json({ error:
|
|
1037
|
+
res.status(400).json({ error: "grantId is required" });
|
|
669
1038
|
return;
|
|
670
1039
|
}
|
|
671
1040
|
await hooks.completeExec(req.body);
|
|
672
1041
|
res.status(200).json({ ok: true });
|
|
673
1042
|
}
|
|
674
1043
|
catch (error) {
|
|
675
|
-
res
|
|
1044
|
+
res
|
|
1045
|
+
.status(500)
|
|
1046
|
+
.json({
|
|
1047
|
+
error: error instanceof Error ? error.message : "completion failed",
|
|
1048
|
+
});
|
|
676
1049
|
}
|
|
677
1050
|
});
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
const idleSweepInterval = idleTimeoutMs > 0 ? setInterval(async () => {
|
|
681
|
-
const now = Date.now();
|
|
682
|
-
for (const [sid, session] of sessions.entries()) {
|
|
683
|
-
if (now - session.lastActivityAt > idleTimeoutMs) {
|
|
684
|
-
console.error(`Closing idle session ${sid} (inactive for ${Math.round((now - session.lastActivityAt) / 1000)}s)`);
|
|
685
|
-
sessions.delete(sid);
|
|
1051
|
+
if (hooks.testService) {
|
|
1052
|
+
app.post("/v1/test", authMiddleware, async (req, res) => {
|
|
686
1053
|
try {
|
|
687
|
-
await
|
|
688
|
-
|
|
1054
|
+
const result = await hooks.testService(req.body?.service, {
|
|
1055
|
+
timeout: req.body?.timeout,
|
|
1056
|
+
});
|
|
1057
|
+
res.status(200).json(result);
|
|
689
1058
|
}
|
|
690
|
-
catch {
|
|
691
|
-
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
res
|
|
1061
|
+
.status(500)
|
|
1062
|
+
.json({
|
|
1063
|
+
error: error instanceof Error ? error.message : "Test failed",
|
|
1064
|
+
});
|
|
692
1065
|
}
|
|
693
|
-
}
|
|
1066
|
+
});
|
|
694
1067
|
}
|
|
695
|
-
}
|
|
1068
|
+
}
|
|
1069
|
+
// Sweep idle sessions every 60 seconds
|
|
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
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}, Math.min(idleTimeoutMs, 60_000))
|
|
1087
|
+
: undefined;
|
|
696
1088
|
if (idleSweepInterval) {
|
|
697
1089
|
idleSweepInterval.unref?.(); // Don't prevent Node from exiting
|
|
698
1090
|
}
|
|
699
|
-
app.all(
|
|
1091
|
+
app.all("/mcp", async (req, res) => {
|
|
700
1092
|
try {
|
|
701
|
-
const sessionId = req.headers[
|
|
1093
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
702
1094
|
if (sessionId && sessions.has(sessionId)) {
|
|
703
1095
|
const session = sessions.get(sessionId);
|
|
704
1096
|
session.lastActivityAt = Date.now();
|
|
@@ -706,11 +1098,16 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
706
1098
|
}
|
|
707
1099
|
else if (!sessionId && (0, types_js_1.isInitializeRequest)(req.body)) {
|
|
708
1100
|
const clientName = req.body?.params?.clientInfo?.name;
|
|
709
|
-
const
|
|
1101
|
+
const mcpResult = createMCPServer(serverOptions);
|
|
1102
|
+
const { server, clientSessions } = mcpResult;
|
|
710
1103
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
711
1104
|
sessionIdGenerator: () => crypto.randomUUID(),
|
|
712
1105
|
onsessioninitialized: (sid) => {
|
|
713
|
-
sessions.set(sid, {
|
|
1106
|
+
sessions.set(sid, {
|
|
1107
|
+
transport,
|
|
1108
|
+
server,
|
|
1109
|
+
lastActivityAt: Date.now(),
|
|
1110
|
+
});
|
|
714
1111
|
if (clientName)
|
|
715
1112
|
clientSessions.set(sid, clientName);
|
|
716
1113
|
},
|
|
@@ -730,25 +1127,28 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
730
1127
|
}
|
|
731
1128
|
else if (sessionId) {
|
|
732
1129
|
res.status(404).json({
|
|
733
|
-
jsonrpc:
|
|
734
|
-
error: { code: -32001, message:
|
|
1130
|
+
jsonrpc: "2.0",
|
|
1131
|
+
error: { code: -32001, message: "Session not found" },
|
|
735
1132
|
id: null,
|
|
736
1133
|
});
|
|
737
1134
|
}
|
|
738
1135
|
else {
|
|
739
1136
|
res.status(400).json({
|
|
740
|
-
jsonrpc:
|
|
741
|
-
error: {
|
|
1137
|
+
jsonrpc: "2.0",
|
|
1138
|
+
error: {
|
|
1139
|
+
code: -32600,
|
|
1140
|
+
message: "Bad Request: Missing session ID or not an initialize request",
|
|
1141
|
+
},
|
|
742
1142
|
id: null,
|
|
743
1143
|
});
|
|
744
1144
|
}
|
|
745
1145
|
}
|
|
746
1146
|
catch (error) {
|
|
747
|
-
console.error(
|
|
1147
|
+
console.error("Error handling MCP request:", error);
|
|
748
1148
|
if (!res.headersSent) {
|
|
749
1149
|
res.status(500).json({
|
|
750
|
-
jsonrpc:
|
|
751
|
-
error: { code: -32603, message:
|
|
1150
|
+
jsonrpc: "2.0",
|
|
1151
|
+
error: { code: -32603, message: "Internal server error" },
|
|
752
1152
|
id: null,
|
|
753
1153
|
});
|
|
754
1154
|
}
|