@true-and-useful/janee 0.14.0 → 0.16.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 +82 -0
- package/dist/cli/cli-utils.d.ts +7 -0
- package/dist/cli/cli-utils.d.ts.map +1 -0
- package/dist/cli/cli-utils.js +55 -0
- package/dist/cli/cli-utils.js.map +1 -0
- package/dist/cli/commands/add.d.ts +4 -0
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +65 -147
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/capability.d.ts +2 -3
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +30 -155
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/config.d.ts.map +1 -1
- package/dist/cli/commands/config.js +10 -34
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/diagnose.d.ts.map +1 -1
- package/dist/cli/commands/diagnose.js +9 -21
- package/dist/cli/commands/diagnose.js.map +1 -1
- package/dist/cli/commands/list.d.ts.map +1 -1
- package/dist/cli/commands/list.js +3 -26
- package/dist/cli/commands/list.js.map +1 -1
- package/dist/cli/commands/logs.d.ts.map +1 -1
- package/dist/cli/commands/logs.js +2 -17
- package/dist/cli/commands/logs.js.map +1 -1
- package/dist/cli/commands/overview.d.ts +4 -0
- package/dist/cli/commands/overview.d.ts.map +1 -0
- package/dist/cli/commands/overview.js +115 -0
- package/dist/cli/commands/overview.js.map +1 -0
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/remove.js +4 -35
- package/dist/cli/commands/remove.js.map +1 -1
- package/dist/cli/commands/revoke.d.ts.map +1 -1
- package/dist/cli/commands/revoke.js +3 -8
- package/dist/cli/commands/revoke.js.map +1 -1
- package/dist/cli/commands/serve-mcp.d.ts.map +1 -1
- package/dist/cli/commands/serve-mcp.js +24 -34
- package/dist/cli/commands/serve-mcp.js.map +1 -1
- package/dist/cli/commands/service-edit.d.ts +2 -0
- package/dist/cli/commands/service-edit.d.ts.map +1 -1
- package/dist/cli/commands/service-edit.js +36 -48
- package/dist/cli/commands/service-edit.js.map +1 -1
- package/dist/cli/commands/sessions.d.ts.map +1 -1
- package/dist/cli/commands/sessions.js +3 -18
- package/dist/cli/commands/sessions.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +3 -18
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/test.d.ts.map +1 -1
- package/dist/cli/commands/test.js +5 -41
- package/dist/cli/commands/test.js.map +1 -1
- package/dist/cli/commands/whoami.d.ts.map +1 -1
- package/dist/cli/commands/whoami.js +3 -17
- package/dist/cli/commands/whoami.js.map +1 -1
- package/dist/cli/config-yaml.d.ts +7 -1
- package/dist/cli/config-yaml.d.ts.map +1 -1
- package/dist/cli/config-yaml.js +19 -0
- package/dist/cli/config-yaml.js.map +1 -1
- package/dist/cli/index.js +16 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/audit.d.ts +2 -12
- package/dist/core/audit.d.ts.map +1 -1
- package/dist/core/audit.js +1 -1
- package/dist/core/audit.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +14 -0
- package/dist/core/auth.js.map +1 -1
- package/dist/core/authority.d.ts +6 -0
- package/dist/core/authority.d.ts.map +1 -1
- package/dist/core/authority.js +19 -13
- package/dist/core/authority.js.map +1 -1
- package/dist/core/directory.d.ts +1 -1
- package/dist/core/directory.d.ts.map +1 -1
- package/dist/core/directory.js +19 -0
- package/dist/core/directory.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +12 -11
- package/dist/core/exec.js.map +1 -1
- package/dist/core/mcp-server.d.ts +10 -25
- package/dist/core/mcp-server.d.ts.map +1 -1
- package/dist/core/mcp-server.js +47 -578
- package/dist/core/mcp-server.js.map +1 -1
- package/dist/core/runner-proxy.d.ts.map +1 -1
- package/dist/core/runner-proxy.js +2 -1
- package/dist/core/runner-proxy.js.map +1 -1
- package/dist/core/sessions.d.ts +10 -0
- package/dist/core/sessions.d.ts.map +1 -1
- package/dist/core/sessions.js.map +1 -1
- package/dist/core/signing.d.ts +24 -0
- package/dist/core/signing.d.ts.map +1 -1
- package/dist/core/signing.js +85 -0
- package/dist/core/signing.js.map +1 -1
- package/dist/core/tool-handlers.d.ts +39 -0
- package/dist/core/tool-handlers.d.ts.map +1 -0
- package/dist/core/tool-handlers.js +378 -0
- package/dist/core/tool-handlers.js.map +1 -0
- package/dist/core/types.d.ts +28 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +16 -0
- package/dist/core/types.js.map +1 -0
- package/package.json +1 -1
package/dist/core/mcp-server.js
CHANGED
|
@@ -57,21 +57,11 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
57
57
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
58
58
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
59
59
|
const agent_scope_js_1 = require("./agent-scope.js");
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const rules_js_1 = require("./rules.js");
|
|
60
|
+
const tool_handlers_js_1 = require("./tool-handlers.js");
|
|
61
|
+
const types_js_2 = require("./types.js");
|
|
63
62
|
// Read version from package.json
|
|
64
63
|
const packageJsonPath = (0, path_1.join)(__dirname, "../../package.json");
|
|
65
64
|
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;
|
|
75
65
|
/**
|
|
76
66
|
* Check whether an agent can access a capability.
|
|
77
67
|
* Checks capability-level allowedAgents first, then falls back to
|
|
@@ -85,7 +75,8 @@ function canAccessCapability(agentId, cap, service, defaultAccessPolicy) {
|
|
|
85
75
|
if (cap.allowedAgents && cap.allowedAgents.length > 0) {
|
|
86
76
|
return cap.allowedAgents.includes(agentId);
|
|
87
77
|
}
|
|
88
|
-
|
|
78
|
+
const effectiveAccess = cap.access ?? defaultAccessPolicy;
|
|
79
|
+
if (effectiveAccess === "restricted") {
|
|
89
80
|
return false;
|
|
90
81
|
}
|
|
91
82
|
return (0, agent_scope_js_1.canAgentAccess)(agentId, service?.ownership);
|
|
@@ -105,10 +96,13 @@ function explainAccessDenial(agentId, cap, service, defaultAccessPolicy) {
|
|
|
105
96
|
}
|
|
106
97
|
return null;
|
|
107
98
|
}
|
|
108
|
-
|
|
99
|
+
const effectiveAccess = cap.access ?? defaultAccessPolicy;
|
|
100
|
+
if (effectiveAccess === 'restricted') {
|
|
109
101
|
return {
|
|
110
102
|
reason: 'DEFAULT_ACCESS_RESTRICTED',
|
|
111
|
-
detail:
|
|
103
|
+
detail: cap.access
|
|
104
|
+
? `Capability access is "restricted" and has no allowedAgents list`
|
|
105
|
+
: `defaultAccess is "restricted" and capability has no allowedAgents list`
|
|
112
106
|
};
|
|
113
107
|
}
|
|
114
108
|
if (!(0, agent_scope_js_1.canAgentAccess)(agentId, service?.ownership)) {
|
|
@@ -119,23 +113,8 @@ function explainAccessDenial(agentId, cap, service, defaultAccessPolicy) {
|
|
|
119
113
|
}
|
|
120
114
|
return null;
|
|
121
115
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
*/
|
|
125
|
-
function parseTTL(ttl) {
|
|
126
|
-
const match = ttl.match(/^(\d+)([smhd])$/);
|
|
127
|
-
if (!match)
|
|
128
|
-
throw new Error(`Invalid TTL format: ${ttl}`);
|
|
129
|
-
const value = parseInt(match[1]);
|
|
130
|
-
const unit = match[2];
|
|
131
|
-
const multipliers = {
|
|
132
|
-
s: 1,
|
|
133
|
-
m: 60,
|
|
134
|
-
h: 3600,
|
|
135
|
-
d: 86400,
|
|
136
|
-
};
|
|
137
|
-
return value * multipliers[unit];
|
|
138
|
-
}
|
|
116
|
+
var types_js_3 = require("./types.js");
|
|
117
|
+
Object.defineProperty(exports, "DenialError", { enumerable: true, get: function () { return types_js_3.DenialError; } });
|
|
139
118
|
/**
|
|
140
119
|
* Create and start MCP server
|
|
141
120
|
*/
|
|
@@ -380,6 +359,21 @@ function createMCPServer(options) {
|
|
|
380
359
|
const result = await onForwardToolCall(name, (args || {}), forwardAgentId);
|
|
381
360
|
return result;
|
|
382
361
|
}
|
|
362
|
+
const ctx = {
|
|
363
|
+
getCapabilities: () => capabilities,
|
|
364
|
+
getServices: () => services,
|
|
365
|
+
defaultAccess,
|
|
366
|
+
sessionManager,
|
|
367
|
+
auditLogger,
|
|
368
|
+
onExecute,
|
|
369
|
+
onExecCommand,
|
|
370
|
+
onForwardToolCall,
|
|
371
|
+
onPersistOwnership,
|
|
372
|
+
resolveAgent: resolveAgentFromRequest,
|
|
373
|
+
clientSessions,
|
|
374
|
+
explainAccessDenial,
|
|
375
|
+
canAccessCapability,
|
|
376
|
+
};
|
|
383
377
|
switch (name) {
|
|
384
378
|
case "list_services": {
|
|
385
379
|
const listAgentId = resolveAgentFromRequest(extra, args);
|
|
@@ -416,482 +410,32 @@ function createMCPServer(options) {
|
|
|
416
410
|
capabilities = result.capabilities;
|
|
417
411
|
services = result.services;
|
|
418
412
|
return {
|
|
419
|
-
content: [
|
|
420
|
-
{
|
|
413
|
+
content: [{
|
|
421
414
|
type: "text",
|
|
422
415
|
text: JSON.stringify({
|
|
423
|
-
success: true,
|
|
424
|
-
|
|
425
|
-
services: services.size,
|
|
426
|
-
capabilities: capabilities.length,
|
|
427
|
-
changes: {
|
|
428
|
-
services: services.size - prevServiceCount,
|
|
429
|
-
capabilities: capabilities.length - prevCapCount,
|
|
430
|
-
},
|
|
416
|
+
success: true, message: "Configuration reloaded successfully",
|
|
417
|
+
services: services.size, capabilities: capabilities.length,
|
|
418
|
+
changes: { services: services.size - prevServiceCount, capabilities: capabilities.length - prevCapCount },
|
|
431
419
|
}, null, 2),
|
|
432
|
-
},
|
|
433
|
-
],
|
|
420
|
+
}],
|
|
434
421
|
};
|
|
435
422
|
}
|
|
436
423
|
catch (error) {
|
|
437
424
|
throw new Error(`Failed to reload config: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
438
425
|
}
|
|
439
426
|
}
|
|
440
|
-
case "execute":
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// Find capability
|
|
453
|
-
const cap = capabilities.find((c) => c.name === capability);
|
|
454
|
-
if (!cap) {
|
|
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
|
-
});
|
|
460
|
-
}
|
|
461
|
-
// Reject exec-mode capabilities — they should use janee_exec instead
|
|
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
|
-
});
|
|
468
|
-
}
|
|
469
|
-
// Check if reason required
|
|
470
|
-
if (cap.requiresReason && !reason) {
|
|
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
|
-
});
|
|
476
|
-
}
|
|
477
|
-
// Check rules (path-based policies)
|
|
478
|
-
const ruleCheck = (0, rules_js_1.checkRules)(cap.rules, method, path);
|
|
479
|
-
if (!ruleCheck.allowed) {
|
|
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
|
-
});
|
|
488
|
-
}
|
|
489
|
-
// Check agent-scoped access (capability-level allowedAgents, then service-level ownership)
|
|
490
|
-
const executeAgentId = resolveAgentFromRequest(extra, args);
|
|
491
|
-
const executeSvc = services.get(cap.service);
|
|
492
|
-
if (!canAccessCapability(executeAgentId, cap, executeSvc, defaultAccess)) {
|
|
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
|
-
});
|
|
506
|
-
}
|
|
507
|
-
// Get or create session
|
|
508
|
-
const ttlSeconds = parseTTL(cap.ttl);
|
|
509
|
-
const session = sessionManager.createSession(cap.name, cap.service, ttlSeconds, { agentId: executeAgentId, reason });
|
|
510
|
-
// Build API request
|
|
511
|
-
const apiReq = {
|
|
512
|
-
service: cap.service,
|
|
513
|
-
path,
|
|
514
|
-
method,
|
|
515
|
-
headers: headers || {},
|
|
516
|
-
body,
|
|
517
|
-
};
|
|
518
|
-
// Execute
|
|
519
|
-
const response = await onExecute(session, apiReq);
|
|
520
|
-
return {
|
|
521
|
-
content: [
|
|
522
|
-
{
|
|
523
|
-
type: "text",
|
|
524
|
-
text: JSON.stringify({
|
|
525
|
-
status: response.statusCode,
|
|
526
|
-
body: response.body,
|
|
527
|
-
}, null, 2),
|
|
528
|
-
},
|
|
529
|
-
],
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
case "janee_exec": {
|
|
533
|
-
if (!onExecCommand) {
|
|
534
|
-
throw new Error("CLI execution not supported in this configuration");
|
|
535
|
-
}
|
|
536
|
-
const { capability: execCapName, command: rawExecCommand, cwd: execCwd, stdin: execStdin, reason: execReason, } = args;
|
|
537
|
-
if (!execCapName) {
|
|
538
|
-
throw new Error("Missing required argument: capability");
|
|
539
|
-
}
|
|
540
|
-
if (!rawExecCommand ||
|
|
541
|
-
(Array.isArray(rawExecCommand) && rawExecCommand.length === 0) ||
|
|
542
|
-
(typeof rawExecCommand === "string" && rawExecCommand.trim() === "")) {
|
|
543
|
-
throw new Error("Missing required argument: command");
|
|
544
|
-
}
|
|
545
|
-
const execCommand = Array.isArray(rawExecCommand)
|
|
546
|
-
? rawExecCommand
|
|
547
|
-
: typeof rawExecCommand === "string"
|
|
548
|
-
? rawExecCommand.trim().split(/\s+/)
|
|
549
|
-
: [];
|
|
550
|
-
let execCap;
|
|
551
|
-
let execSession;
|
|
552
|
-
if (onForwardToolCall) {
|
|
553
|
-
// Runner mode: Authority handles validation and credential injection.
|
|
554
|
-
// Build a minimal capability stub so onExecCommand has the name.
|
|
555
|
-
execCap = {
|
|
556
|
-
name: execCapName,
|
|
557
|
-
service: "",
|
|
558
|
-
ttl: "1h",
|
|
559
|
-
mode: "exec",
|
|
560
|
-
workDir: execCwd,
|
|
561
|
-
};
|
|
562
|
-
execSession = { agentId: resolveAgentFromRequest(extra, args) };
|
|
563
|
-
}
|
|
564
|
-
else {
|
|
565
|
-
// Standalone mode: validate locally
|
|
566
|
-
const foundCap = capabilities.find((c) => c.name === execCapName);
|
|
567
|
-
if (!foundCap) {
|
|
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
|
-
});
|
|
573
|
-
}
|
|
574
|
-
execCap = execCwd ? { ...foundCap, workDir: execCwd } : foundCap;
|
|
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
|
-
});
|
|
581
|
-
}
|
|
582
|
-
const execAgentId = resolveAgentFromRequest(extra, args);
|
|
583
|
-
const execSvc = services.get(execCap.service);
|
|
584
|
-
if (!canAccessCapability(execAgentId, execCap, execSvc, defaultAccess)) {
|
|
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
|
-
});
|
|
598
|
-
}
|
|
599
|
-
if (execCap.requiresReason && !execReason) {
|
|
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
|
-
});
|
|
605
|
-
}
|
|
606
|
-
const cmdValidation = (0, exec_js_1.validateCommand)(execCommand, execCap.allowCommands || []);
|
|
607
|
-
if (!cmdValidation.allowed) {
|
|
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
|
-
});
|
|
616
|
-
}
|
|
617
|
-
const execTtlSeconds = parseTTL(execCap.ttl);
|
|
618
|
-
execSession = sessionManager.createSession(execCap.name, execCap.service, execTtlSeconds, { reason: execReason });
|
|
619
|
-
}
|
|
620
|
-
const execResult = await onExecCommand(execSession, execCap, execCommand, execStdin);
|
|
621
|
-
// Log to audit
|
|
622
|
-
auditLogger.log({
|
|
623
|
-
service: execCap.service,
|
|
624
|
-
path: execCommand.join(" "),
|
|
625
|
-
method: "EXEC",
|
|
626
|
-
headers: { "x-janee-reason": execReason || "" },
|
|
627
|
-
}, {
|
|
628
|
-
statusCode: execResult.exitCode === 0 ? 200 : 500,
|
|
629
|
-
headers: {},
|
|
630
|
-
body: execResult.stdout,
|
|
631
|
-
}, execResult.executionTimeMs);
|
|
632
|
-
return {
|
|
633
|
-
content: [
|
|
634
|
-
{
|
|
635
|
-
type: "text",
|
|
636
|
-
text: JSON.stringify({
|
|
637
|
-
exitCode: execResult.exitCode,
|
|
638
|
-
stdout: execResult.stdout,
|
|
639
|
-
stderr: execResult.stderr,
|
|
640
|
-
executionTimeMs: execResult.executionTimeMs,
|
|
641
|
-
executionTarget: "runner",
|
|
642
|
-
}, null, 2),
|
|
643
|
-
},
|
|
644
|
-
],
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
case "manage_credential": {
|
|
648
|
-
const { action: credAction, service: credService, targetAgentId: credTarget, } = args;
|
|
649
|
-
const credAgentId = resolveAgentFromRequest(extra, args);
|
|
650
|
-
if (!credService) {
|
|
651
|
-
throw new Error("Missing required argument: service");
|
|
652
|
-
}
|
|
653
|
-
const svc = services.get(credService);
|
|
654
|
-
if (!svc) {
|
|
655
|
-
throw new Error(`Unknown service: ${credService}`);
|
|
656
|
-
}
|
|
657
|
-
if (credAction === "view") {
|
|
658
|
-
return {
|
|
659
|
-
content: [
|
|
660
|
-
{
|
|
661
|
-
type: "text",
|
|
662
|
-
text: JSON.stringify({
|
|
663
|
-
service: credService,
|
|
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
|
-
],
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
// Grant/revoke require ownership verification
|
|
675
|
-
if (!credAgentId) {
|
|
676
|
-
throw new Error("agentId is required for grant/revoke actions");
|
|
677
|
-
}
|
|
678
|
-
if (!svc.ownership) {
|
|
679
|
-
throw new Error("Cannot manage access for legacy credentials without ownership metadata. Re-add the service to enable scoping.");
|
|
680
|
-
}
|
|
681
|
-
if (svc.ownership.createdBy !== credAgentId) {
|
|
682
|
-
throw new Error("Only the credential owner can grant or revoke access");
|
|
683
|
-
}
|
|
684
|
-
if (credAction === "grant") {
|
|
685
|
-
if (!credTarget) {
|
|
686
|
-
throw new Error("targetAgentId is required for grant action");
|
|
687
|
-
}
|
|
688
|
-
const { grantAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
689
|
-
svc.ownership = grantAccess(svc.ownership, credTarget);
|
|
690
|
-
// Persist ownership change to config storage
|
|
691
|
-
if (onPersistOwnership) {
|
|
692
|
-
onPersistOwnership(credService, svc.ownership);
|
|
693
|
-
}
|
|
694
|
-
return {
|
|
695
|
-
content: [
|
|
696
|
-
{
|
|
697
|
-
type: "text",
|
|
698
|
-
text: JSON.stringify({
|
|
699
|
-
success: true,
|
|
700
|
-
message: `Granted access to ${credTarget}`,
|
|
701
|
-
ownership: svc.ownership,
|
|
702
|
-
persisted: !!onPersistOwnership,
|
|
703
|
-
}, null, 2),
|
|
704
|
-
},
|
|
705
|
-
],
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
if (credAction === "revoke") {
|
|
709
|
-
if (!credTarget) {
|
|
710
|
-
throw new Error("targetAgentId is required for revoke action");
|
|
711
|
-
}
|
|
712
|
-
const { revokeAccess } = await Promise.resolve().then(() => __importStar(require("./agent-scope.js")));
|
|
713
|
-
svc.ownership = revokeAccess(svc.ownership, credTarget);
|
|
714
|
-
// Persist ownership change to config storage
|
|
715
|
-
if (onPersistOwnership) {
|
|
716
|
-
onPersistOwnership(credService, svc.ownership);
|
|
717
|
-
}
|
|
718
|
-
return {
|
|
719
|
-
content: [
|
|
720
|
-
{
|
|
721
|
-
type: "text",
|
|
722
|
-
text: JSON.stringify({
|
|
723
|
-
success: true,
|
|
724
|
-
message: `Revoked access from ${credTarget}`,
|
|
725
|
-
ownership: svc.ownership,
|
|
726
|
-
persisted: !!onPersistOwnership,
|
|
727
|
-
}, null, 2),
|
|
728
|
-
},
|
|
729
|
-
],
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
throw new Error(`Unknown action: ${credAction}. Use 'view', 'grant', or 'revoke'.`);
|
|
733
|
-
}
|
|
734
|
-
case "test_service": {
|
|
735
|
-
const { service: testSvcName, timeout: testTimeout } = (args ||
|
|
736
|
-
{});
|
|
737
|
-
const testOpts = testTimeout ? { timeout: testTimeout } : {};
|
|
738
|
-
let targets;
|
|
739
|
-
if (testSvcName) {
|
|
740
|
-
const svc = services.get(testSvcName);
|
|
741
|
-
if (!svc) {
|
|
742
|
-
throw new Error(`Unknown service: ${testSvcName}. Use list_services to see available services.`);
|
|
743
|
-
}
|
|
744
|
-
targets = [[testSvcName, svc]];
|
|
745
|
-
}
|
|
746
|
-
else {
|
|
747
|
-
targets = Array.from(services.entries());
|
|
748
|
-
}
|
|
749
|
-
if (targets.length === 0) {
|
|
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` });
|
|
845
|
-
}
|
|
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');
|
|
855
|
-
return {
|
|
856
|
-
content: [{
|
|
857
|
-
type: 'text',
|
|
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)
|
|
865
|
-
}]
|
|
866
|
-
};
|
|
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
|
-
}
|
|
427
|
+
case "execute":
|
|
428
|
+
return await (0, tool_handlers_js_1.handleExecute)(ctx, args, extra);
|
|
429
|
+
case "janee_exec":
|
|
430
|
+
return await (0, tool_handlers_js_1.handleExec)(ctx, args, extra);
|
|
431
|
+
case "manage_credential":
|
|
432
|
+
return await (0, tool_handlers_js_1.handleManageCredential)(ctx, args, extra);
|
|
433
|
+
case "test_service":
|
|
434
|
+
return await (0, tool_handlers_js_1.handleTestService)(ctx, args);
|
|
435
|
+
case "explain_access":
|
|
436
|
+
return (0, tool_handlers_js_1.handleExplainAccess)(ctx, args, extra);
|
|
437
|
+
case "whoami":
|
|
438
|
+
return (0, tool_handlers_js_1.handleWhoami)(ctx, args, extra);
|
|
895
439
|
case "doctor": {
|
|
896
440
|
if (!options.onDoctorRunner) {
|
|
897
441
|
throw new Error("Doctor diagnostics only available in runner mode.");
|
|
@@ -899,12 +443,7 @@ function createMCPServer(options) {
|
|
|
899
443
|
const doctorAgentId = resolveAgentFromRequest(extra, args);
|
|
900
444
|
const doctorResult = await options.onDoctorRunner(doctorAgentId);
|
|
901
445
|
return {
|
|
902
|
-
content: [
|
|
903
|
-
{
|
|
904
|
-
type: "text",
|
|
905
|
-
text: JSON.stringify(doctorResult, null, 2),
|
|
906
|
-
},
|
|
907
|
-
],
|
|
446
|
+
content: [{ type: "text", text: JSON.stringify(doctorResult, null, 2) }],
|
|
908
447
|
};
|
|
909
448
|
}
|
|
910
449
|
default:
|
|
@@ -915,7 +454,7 @@ function createMCPServer(options) {
|
|
|
915
454
|
const payload = {
|
|
916
455
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
917
456
|
};
|
|
918
|
-
if (error instanceof DenialError) {
|
|
457
|
+
if (error instanceof types_js_2.DenialError) {
|
|
919
458
|
payload.denial = error.denial;
|
|
920
459
|
}
|
|
921
460
|
return {
|
|
@@ -1020,79 +559,9 @@ async function startMCPServerHTTP(serverOptions, httpOptions) {
|
|
|
1020
559
|
app.use(express_1.default.json());
|
|
1021
560
|
const idleTimeoutMs = httpOptions.idleTimeoutMs ?? 30 * 60 * 1000; // default 30 min
|
|
1022
561
|
const sessions = new Map();
|
|
1023
|
-
// Authority REST endpoints -- active when runnerKey is provided
|
|
1024
562
|
if (httpOptions.runnerKey && httpOptions.authorityHooks) {
|
|
1025
|
-
const {
|
|
1026
|
-
|
|
1027
|
-
const hooks = httpOptions.authorityHooks;
|
|
1028
|
-
const authMiddleware = (req, res, next) => {
|
|
1029
|
-
const provided = req.header("x-janee-runner-key");
|
|
1030
|
-
if (!provided ||
|
|
1031
|
-
provided.length !== runnerKey.length ||
|
|
1032
|
-
!timingSafeEqual(Buffer.from(provided), Buffer.from(runnerKey))) {
|
|
1033
|
-
res.status(401).json({ error: "Unauthorized runner request" });
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
next();
|
|
1037
|
-
};
|
|
1038
|
-
app.get("/v1/health", (_req, res) => {
|
|
1039
|
-
res.status(200).json({ ok: true, mode: "authority" });
|
|
1040
|
-
});
|
|
1041
|
-
app.post("/v1/exec/authorize", authMiddleware, async (req, res) => {
|
|
1042
|
-
try {
|
|
1043
|
-
const body = req.body;
|
|
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" });
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1051
|
-
const response = await hooks.authorizeExec(body);
|
|
1052
|
-
res.status(200).json(response);
|
|
1053
|
-
}
|
|
1054
|
-
catch (error) {
|
|
1055
|
-
res
|
|
1056
|
-
.status(403)
|
|
1057
|
-
.json({
|
|
1058
|
-
error: error instanceof Error ? error.message : "Authorization failed",
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
});
|
|
1062
|
-
app.post("/v1/exec/complete", authMiddleware, async (req, res) => {
|
|
1063
|
-
try {
|
|
1064
|
-
if (!req.body?.grantId) {
|
|
1065
|
-
res.status(400).json({ error: "grantId is required" });
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
await hooks.completeExec(req.body);
|
|
1069
|
-
res.status(200).json({ ok: true });
|
|
1070
|
-
}
|
|
1071
|
-
catch (error) {
|
|
1072
|
-
res
|
|
1073
|
-
.status(500)
|
|
1074
|
-
.json({
|
|
1075
|
-
error: error instanceof Error ? error.message : "completion failed",
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
if (hooks.testService) {
|
|
1080
|
-
app.post("/v1/test", authMiddleware, async (req, res) => {
|
|
1081
|
-
try {
|
|
1082
|
-
const result = await hooks.testService(req.body?.service, {
|
|
1083
|
-
timeout: req.body?.timeout,
|
|
1084
|
-
});
|
|
1085
|
-
res.status(200).json(result);
|
|
1086
|
-
}
|
|
1087
|
-
catch (error) {
|
|
1088
|
-
res
|
|
1089
|
-
.status(500)
|
|
1090
|
-
.json({
|
|
1091
|
-
error: error instanceof Error ? error.message : "Test failed",
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
563
|
+
const { mountAuthorityRoutes } = await Promise.resolve().then(() => __importStar(require("./authority.js")));
|
|
564
|
+
mountAuthorityRoutes(app, httpOptions.runnerKey, httpOptions.authorityHooks);
|
|
1096
565
|
}
|
|
1097
566
|
// Sweep idle sessions every 60 seconds
|
|
1098
567
|
const idleSweepInterval = idleTimeoutMs > 0
|