Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ All notable changes to Janee will be documented in this file.
### Added

- **Per-capability `access` override** — Capabilities can now set `access: open` or `access: restricted` to override the global `defaultAccess` policy. Useful for mixed environments where some capabilities (e.g. SerpAPI) should be open to all agents while others (e.g. Stripe) are locked to specific agents. Configurable via `janee cap add --access open` / `janee cap edit --access restricted` / `janee cap edit --clear-access`. Surfaced in `explain_access` traces and `cap list` output.
- **`janee overview` command** — One-screen summary of services, capabilities, and per-agent access. Shows which agents can reach which capabilities, highlights unreachable capabilities (restricted with no `allowedAgents`), and reports the global default access policy. Supports `--json`.
- **`janee add` always creates a default capability** — Running `janee add resend --key xxx` now creates both the service and a ready-to-use capability in one step. Previously, non-interactive usage with partial prompting (e.g. prompted for test path) would ask whether to create a capability; now it always does. Customize afterward with `janee cap edit`.

### Fixed

- **`diagnose access` now respects per-capability `access` override** — The CLI diagnose command was not checking `cap.access` when evaluating the default access policy step, causing incorrect trace output for capabilities with per-capability overrides.

### Changed

Expand Down
133 changes: 31 additions & 102 deletions src/cli/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,104 +664,13 @@ export async function addCommand(

saveYAMLConfig(config);

if (options.json) {
// JSON output for non-interactive/json mode
const result: any = {
ok: true,
service: serviceName,
message: `Added service "${serviceName}"`
};

// If readline was never opened, we're fully non-interactive.
// Auto-create a capability with sensible defaults.
if (!prompted && !config.capabilities[serviceName]) {
const capConfig: CapabilityConfig = {
service: serviceName,
ttl: '1h',
autoApprove: true,
};
if (options.exec) {
capConfig.mode = 'exec';
if (options.allowCommands) capConfig.allowCommands = options.allowCommands;
if (options.envMap) capConfig.env = parseEnvMap(options.envMap);
if (options.workDir) capConfig.workDir = options.workDir;
if (options.timeout) capConfig.timeout = parseInt(options.timeout, 10);
}
config.capabilities[serviceName] = capConfig;
saveYAMLConfig(config);
result.capability = serviceName;
result.capabilityMessage = options.exec
? `Added exec-mode capability "${serviceName}" (1h TTL, auto-approve, commands: ${(options.allowCommands || []).join(', ')})`
: `Added capability "${serviceName}" (1h TTL, auto-approve)`;
}

console.log(JSON.stringify(result));
return;
}

console.log(`✅ Added service "${serviceName}"`);
console.log();

// If readline was never opened, we're fully non-interactive.
// Auto-create a capability with sensible defaults instead of prompting.
if (!prompted) {
if (!config.capabilities[serviceName]) {
const capConfig: CapabilityConfig = {
service: serviceName,
ttl: '1h',
autoApprove: true,
};
if (options.exec) {
capConfig.mode = 'exec';
if (options.allowCommands) capConfig.allowCommands = options.allowCommands;
if (options.envMap) capConfig.env = parseEnvMap(options.envMap);
if (options.workDir) capConfig.workDir = options.workDir;
if (options.timeout) capConfig.timeout = parseInt(options.timeout, 10);
}
config.capabilities[serviceName] = capConfig;
saveYAMLConfig(config);
if (options.exec) {
console.log(`✅ Added exec-mode capability "${serviceName}" (1h TTL, auto-approve)`);
console.log(` Allowed commands: ${(options.allowCommands || []).join(', ')}`);
} else {
console.log(`✅ Added capability "${serviceName}" (1h TTL, auto-approve)`);
}
console.log();
}
console.log("Done! Run 'janee serve' to start.");
return;
}

// Ask about capability
const createCapAnswer = await getRL().question('Create a capability for this service? (Y/n): ');
const createCap = !createCapAnswer || createCapAnswer.toLowerCase() === 'y' || createCapAnswer.toLowerCase() === 'yes';

if (createCap) {
const capNameDefault = serviceName;
const capNameInput = await getRL().question(`Capability name (default: ${capNameDefault}): `);
const capName = capNameInput.trim() || capNameDefault;

// Check if capability already exists
if (config.capabilities[capName]) {
console.error(`❌ Capability "${capName}" already exists`);
process.exit(1);
}

const ttlInput = await getRL().question('TTL (e.g., 1h, 30m): ');
const ttl = ttlInput.trim() || '1h';

const autoApproveInput = await getRL().question('Auto-approve? (Y/n): ');
const autoApprove = !autoApproveInput || autoApproveInput.toLowerCase() === 'y' || autoApproveInput.toLowerCase() === 'yes';

const requiresReasonInput = await getRL().question('Requires reason? (y/N): ');
const requiresReason = requiresReasonInput.toLowerCase() === 'y' || requiresReasonInput.toLowerCase() === 'yes';

// Add capability
// Auto-create a default capability unless one already exists
const capCreated = !config.capabilities[serviceName];
if (capCreated) {
const capConfig: CapabilityConfig = {
service: serviceName,
ttl,
autoApprove,
requiresReason
ttl: '1h',
autoApprove: true,
};
if (options.exec) {
capConfig.mode = 'exec';
Expand All @@ -770,18 +679,38 @@ export async function addCommand(
if (options.workDir) capConfig.workDir = options.workDir;
if (options.timeout) capConfig.timeout = parseInt(options.timeout, 10);
}
config.capabilities[capName] = capConfig;

config.capabilities[serviceName] = capConfig;
saveYAMLConfig(config);

console.log(`✅ Added capability "${capName}"`);
console.log();
}

closeRL();
if (options.json) {
const result: any = { ok: true, service: serviceName, message: `Added service "${serviceName}"` };
if (capCreated) {
result.capability = serviceName;
result.message += ` with capability "${serviceName}"`;
}
console.log(JSON.stringify(result));
closeRL();
return;
}

console.log(`✅ Added service "${serviceName}"`);
if (capCreated) {
if (options.exec) {
console.log(`✅ Added exec-mode capability "${serviceName}" (1h TTL, auto-approve)`);
console.log(` Allowed commands: ${(options.allowCommands || []).join(', ')}`);
} else {
console.log(`✅ Added capability "${serviceName}" (1h TTL, auto-approve)`);
}
console.log(` Customize with: janee cap edit ${serviceName}`);
} else {
console.log(` Existing capability "${serviceName}" unchanged`);
}
console.log();
console.log("Done! Run 'janee serve' to start.");

closeRL();

} catch (error) {
handleCommandError(error, options.json);
}
Expand Down
7 changes: 6 additions & 1 deletion src/cli/commands/capability.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { cliError, handleCommandError, parseEnvMap, requireConfig } from '../cli-utils';
import {
cliError,
handleCommandError,
parseEnvMap,
requireConfig,
} from '../cli-utils';
import {
CapabilityConfig,
loadYAMLConfig,
Expand Down
10 changes: 6 additions & 4 deletions src/cli/commands/diagnose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ export async function diagnoseAccessCommand(
trace.push({ check: 'allowed_agents', result: 'skip', detail: `No allowedAgents restriction on this capability` });
}

// defaultAccess
// defaultAccess (with per-capability override)
if (agentId && (!cap.allowedAgents || cap.allowedAgents.length === 0)) {
if (defaultAccess === 'restricted') {
trace.push({ check: 'default_access', result: 'fail', detail: `defaultAccess is "restricted" and no allowedAgents list — agent blocked` });
const effectiveAccess = cap.access ?? defaultAccess;
const source = cap.access ? 'capability access' : 'global defaultAccess';
if (effectiveAccess === 'restricted') {
trace.push({ check: 'default_access', result: 'fail', detail: `${source} is "restricted" and no allowedAgents list — agent blocked` });
} else {
trace.push({ check: 'default_access', result: 'pass', detail: `defaultAccess is "${defaultAccess ?? 'open'}" — agent allowed` });
trace.push({ check: 'default_access', result: 'pass', detail: `${source} is "${effectiveAccess ?? 'open'}" — agent allowed` });
}
} else {
trace.push({ check: 'default_access', result: 'skip', detail: agentId ? `allowedAgents list takes precedence` : `No agent ID (admin/CLI)` });
Expand Down
Loading