@opencoven/coven-code 0.0.1 → 0.0.2
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 -1
- package/docs/CLI.md +65 -1
- package/docs/DEMO.md +450 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/README.md +1 -0
- package/package.json +7 -6
- package/src/agent/{local.mjs → fixture.mjs} +1 -1
- package/src/cli/execute.mjs +6 -4
- package/src/cli/interactive-core.mjs +5 -279
- package/src/cli/interactive-io.mjs +101 -0
- package/src/cli/interactive-slash.mjs +184 -0
- package/src/cli/repl.mjs +1 -2
- package/src/cli/tui-actions.mjs +72 -0
- package/src/cli/tui-blessed.mjs +198 -0
- package/src/cli/tui-keys.mjs +80 -0
- package/src/cli/tui-lane.mjs +73 -0
- package/src/cli/tui-render.mjs +169 -0
- package/src/cli/tui-submit.mjs +82 -0
- package/src/cli/tui.mjs +30 -613
- package/src/commands/permissions-eval.mjs +122 -0
- package/src/commands/permissions-rules.mjs +53 -0
- package/src/commands/permissions-text.mjs +112 -0
- package/src/commands/permissions.mjs +15 -281
- package/src/commands/usage.mjs +1 -1
- package/src/constants.mjs +7 -1
- package/src/mcp/local.mjs +55 -0
- package/src/mcp/parsers.mjs +46 -0
- package/src/mcp/probe.mjs +12 -351
- package/src/mcp/remote-oauth.mjs +55 -0
- package/src/mcp/remote-session.mjs +54 -0
- package/src/mcp/remote-sse.mjs +82 -0
- package/src/mcp/remote.mjs +74 -0
- package/src/plugins/api.mjs +187 -0
- package/src/plugins/configuration.mjs +124 -0
- package/src/plugins/discover.mjs +8 -804
- package/src/plugins/helpers.mjs +187 -0
- package/src/plugins/subsystems.mjs +198 -0
- package/src/plugins/validators.mjs +142 -0
- package/src/sdk-execute.mjs +82 -0
- package/src/sdk-settings.mjs +88 -0
- package/src/sdk.mjs +13 -164
- package/src/tools/builtin/oracle.mjs +2 -2
- package/src/tools/builtin/runtime-content.mjs +31 -0
- package/src/tools/builtin/runtime-decisions.mjs +115 -0
- package/src/tools/builtin/runtime.mjs +18 -148
- package/src/tools/builtin/task.mjs +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
3
|
+
import { globMatch } from '../util/glob.mjs';
|
|
4
|
+
import {
|
|
5
|
+
COMMAND_ALLOWLIST_SETTING,
|
|
6
|
+
DANGEROUSLY_ALLOW_ALL_SETTING,
|
|
7
|
+
GUARDED_FILES_ALLOWLIST_SETTING,
|
|
8
|
+
PERMISSIONS_SETTING,
|
|
9
|
+
builtinPermissionRules,
|
|
10
|
+
commandAllowlistPermissionRules,
|
|
11
|
+
isUndefinedMatchValue,
|
|
12
|
+
loadUserPermissionRules,
|
|
13
|
+
} from './permissions-rules.mjs';
|
|
14
|
+
|
|
15
|
+
export function evaluatePermission(tool, toolArgs, parsed = {}, options = {}) {
|
|
16
|
+
const ruleGroups = [
|
|
17
|
+
{ source: 'user', rules: loadUserPermissionRules(parsed) },
|
|
18
|
+
{ source: 'command-allowlist', rules: commandAllowlistPermissionRules(parsed) },
|
|
19
|
+
{
|
|
20
|
+
source: 'built-in',
|
|
21
|
+
rules: builtinPermissionRules(),
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
for (const group of ruleGroups) {
|
|
26
|
+
for (const [index, rule] of group.rules.entries()) {
|
|
27
|
+
if (!globMatch(rule.tool, tool)) continue;
|
|
28
|
+
if (!matchesContext(rule.context, options.context ?? 'thread')) continue;
|
|
29
|
+
if (!matchesArguments(rule.matches, toolArgs)) continue;
|
|
30
|
+
return {
|
|
31
|
+
action: rule.action,
|
|
32
|
+
to: rule.to,
|
|
33
|
+
message: rule.message,
|
|
34
|
+
matchedRule: index,
|
|
35
|
+
source: group.source,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { action: 'reject', source: 'default' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function resolvePermissionDecision(tool, toolArgs, parsed = {}, options = {}) {
|
|
44
|
+
const settings = readEffectiveSettings(parsed);
|
|
45
|
+
if (isDangerouslyAllowAll(parsed, settings)) return { action: 'allow', source: 'dangerously-allow-all' };
|
|
46
|
+
if (!legacyPermissionsConfigured(settings)) return { action: 'allow', source: 'default-no-approval' };
|
|
47
|
+
const decision = evaluatePermission(tool, toolArgs, parsed, options);
|
|
48
|
+
if (decision.action !== 'delegate') return decision;
|
|
49
|
+
if (!decision.to) {
|
|
50
|
+
return { ...decision, action: 'reject', message: 'Delegate permission rule is missing a target program' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = spawnSync(decision.to, {
|
|
54
|
+
input: `${JSON.stringify(toolArgs)}\n`,
|
|
55
|
+
env: {
|
|
56
|
+
...process.env,
|
|
57
|
+
AGENT: 'coven-code',
|
|
58
|
+
AGENT_TOOL_NAME: tool,
|
|
59
|
+
COVEN_CODE_THREAD_ID: options.threadId ?? '',
|
|
60
|
+
AGENT_THREAD_ID: options.threadId ?? '',
|
|
61
|
+
},
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
shell: false,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (result.status === 0) return { ...decision, action: 'allow', delegatedAction: 'allow' };
|
|
67
|
+
if (result.status === 1) return { ...decision, action: 'ask', delegatedAction: 'ask' };
|
|
68
|
+
return {
|
|
69
|
+
...decision,
|
|
70
|
+
action: 'reject',
|
|
71
|
+
delegatedAction: 'reject',
|
|
72
|
+
message: result.stderr.trim() || result.error?.message || `Permission delegate ${decision.to} rejected the tool call`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isDangerouslyAllowAll(parsed = {}, settings = readEffectiveSettings(parsed)) {
|
|
77
|
+
if (parsed.dangerouslyAllowAll) return true;
|
|
78
|
+
return settings[DANGEROUSLY_ALLOW_ALL_SETTING] === true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function legacyPermissionsConfigured(settings = {}) {
|
|
82
|
+
return Object.hasOwn(settings, PERMISSIONS_SETTING)
|
|
83
|
+
|| Object.hasOwn(settings, GUARDED_FILES_ALLOWLIST_SETTING)
|
|
84
|
+
|| Object.hasOwn(settings, COMMAND_ALLOWLIST_SETTING)
|
|
85
|
+
|| settings[DANGEROUSLY_ALLOW_ALL_SETTING] === false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function matchesContext(ruleContext, actualContext) {
|
|
89
|
+
return !ruleContext || ruleContext === actualContext;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function matchesArguments(matches, toolArgs) {
|
|
93
|
+
if (!matches || Object.keys(matches).length === 0) return true;
|
|
94
|
+
for (const [key, pattern] of Object.entries(matches)) {
|
|
95
|
+
const actual = valueAtPath(toolArgs, key);
|
|
96
|
+
if (!matchesPattern(pattern, actual)) return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function matchesPattern(pattern, actual) {
|
|
102
|
+
if (isUndefinedMatchValue(pattern)) return actual === undefined;
|
|
103
|
+
if (Array.isArray(pattern)) return pattern.some((entry) => matchesPattern(entry, actual));
|
|
104
|
+
if (pattern && typeof pattern === 'object') {
|
|
105
|
+
if (!actual || typeof actual !== 'object') return false;
|
|
106
|
+
return Object.entries(pattern).every(([key, value]) => matchesPattern(value, valueAtPath(actual, key)));
|
|
107
|
+
}
|
|
108
|
+
if (typeof pattern === 'string') {
|
|
109
|
+
if (typeof actual !== 'string') return false;
|
|
110
|
+
if (isRegexPattern(pattern)) return new RegExp(pattern.slice(1, -1)).test(actual);
|
|
111
|
+
return globMatch(pattern, actual);
|
|
112
|
+
}
|
|
113
|
+
return Object.is(pattern, actual);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isRegexPattern(value) {
|
|
117
|
+
return value.length >= 2 && value.startsWith('/') && value.endsWith('/');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function valueAtPath(value, key) {
|
|
121
|
+
return key.split('.').reduce((current, part) => current?.[part], value);
|
|
122
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BUILTIN_PERMISSIONS } from '../constants.mjs';
|
|
2
|
+
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
3
|
+
|
|
4
|
+
export const PERMISSIONS_SETTING = 'covenCode.permissions';
|
|
5
|
+
export const COMMAND_ALLOWLIST_SETTING = 'covenCode.commands.allowlist';
|
|
6
|
+
export const DANGEROUSLY_ALLOW_ALL_SETTING = 'covenCode.dangerouslyAllowAll';
|
|
7
|
+
export const GUARDED_FILES_ALLOWLIST_SETTING = 'covenCode.guardedFiles.allowlist';
|
|
8
|
+
|
|
9
|
+
export const UNDEFINED_MATCH_VALUE = Object.freeze({ __covenCodeLiteral: 'undefined' });
|
|
10
|
+
|
|
11
|
+
export function isUndefinedMatchValue(value) {
|
|
12
|
+
return Boolean(
|
|
13
|
+
value
|
|
14
|
+
&& typeof value === 'object'
|
|
15
|
+
&& !Array.isArray(value)
|
|
16
|
+
&& value.__covenCodeLiteral === 'undefined'
|
|
17
|
+
&& Object.keys(value).length === 1,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadUserPermissionRules(parsed = {}) {
|
|
22
|
+
const settings = readEffectiveSettings(parsed);
|
|
23
|
+
return Array.isArray(settings[PERMISSIONS_SETTING]) ? settings[PERMISSIONS_SETTING] : [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function builtinPermissionRules() {
|
|
27
|
+
return BUILTIN_PERMISSIONS.map(([action, builtinTool, cmd]) => ({
|
|
28
|
+
action,
|
|
29
|
+
tool: builtinTool,
|
|
30
|
+
matches: builtinTool === 'Bash' ? { cmd: `${cmd}*` } : undefined,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function commandAllowlistPermissionRules(parsed = {}) {
|
|
35
|
+
const allowlist = readEffectiveSettings(parsed)[COMMAND_ALLOWLIST_SETTING];
|
|
36
|
+
if (!Array.isArray(allowlist)) return [];
|
|
37
|
+
return allowlist
|
|
38
|
+
.map((command) => String(command).trim())
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((command) => ({
|
|
41
|
+
action: 'allow',
|
|
42
|
+
tool: 'Bash',
|
|
43
|
+
matches: { cmd: commandAllowlistPattern(command) },
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function commandAllowlistPattern(command) {
|
|
48
|
+
return `/^${escapeRegex(command)}(?:\\s|$).*/`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function escapeRegex(value) {
|
|
52
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
53
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { UsageError } from '../cli/parse.mjs';
|
|
2
|
+
import { shellQuote, splitShellWords } from '../util/shell.mjs';
|
|
3
|
+
import { UNDEFINED_MATCH_VALUE, isUndefinedMatchValue } from './permissions-rules.mjs';
|
|
4
|
+
|
|
5
|
+
export function formatPermissionRule(rule) {
|
|
6
|
+
const parts = [rule.action];
|
|
7
|
+
for (const key of ['context', 'to', 'message']) {
|
|
8
|
+
if (rule[key] !== undefined) parts.push(`--${key}`, formatPermissionToken(rule[key]));
|
|
9
|
+
}
|
|
10
|
+
parts.push(formatPermissionToken(rule.tool));
|
|
11
|
+
for (const [key, value] of flattenMatches(rule.matches)) {
|
|
12
|
+
parts.push(`--${key}`, formatPermissionToken(value));
|
|
13
|
+
}
|
|
14
|
+
return parts.join(' ');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function flattenMatches(matches, prefix = '') {
|
|
18
|
+
if (!matches) return [];
|
|
19
|
+
const entries = [];
|
|
20
|
+
for (const [key, value] of Object.entries(matches)) {
|
|
21
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const entry of value) entries.push(...flattenMatchValue(fullKey, entry));
|
|
24
|
+
} else {
|
|
25
|
+
entries.push(...flattenMatchValue(fullKey, value));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function flattenMatchValue(key, value) {
|
|
32
|
+
if (isUndefinedMatchValue(value)) return [[key, value]];
|
|
33
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return flattenMatches(value, key);
|
|
34
|
+
return [[key, value]];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatPermissionToken(value) {
|
|
38
|
+
if (isUndefinedMatchValue(value)) return 'undefined';
|
|
39
|
+
if (value === null) return 'null';
|
|
40
|
+
if (value === undefined) return 'undefined';
|
|
41
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
42
|
+
const text = String(value);
|
|
43
|
+
return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : shellQuote(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parsePermissionText(input) {
|
|
47
|
+
return input
|
|
48
|
+
.split(/\r?\n/)
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
51
|
+
.map((line) => parsePermissionRule(splitShellWords(line)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parsePermissionRule(tokens) {
|
|
55
|
+
const [action] = tokens;
|
|
56
|
+
const { flags: actionArgs, index: toolIndex } = parseActionArgs(tokens, 1);
|
|
57
|
+
const tool = tokens[toolIndex];
|
|
58
|
+
const rest = tokens.slice(toolIndex + 1);
|
|
59
|
+
if (!action || !tool) throw new UsageError('permission rules require: <action> <tool>');
|
|
60
|
+
const rule = { action, tool, ...actionArgs };
|
|
61
|
+
const matches = parseFlagMatches(rest, { undefinedPattern: true });
|
|
62
|
+
if (Object.keys(matches).length > 0) rule.matches = matches;
|
|
63
|
+
if (matches.to) {
|
|
64
|
+
rule.to = matches.to;
|
|
65
|
+
delete rule.matches.to;
|
|
66
|
+
if (Object.keys(rule.matches).length === 0) delete rule.matches;
|
|
67
|
+
}
|
|
68
|
+
return rule;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseActionArgs(tokens, startIndex) {
|
|
72
|
+
const flags = {};
|
|
73
|
+
let index = startIndex;
|
|
74
|
+
while (tokens[index]?.startsWith('--')) {
|
|
75
|
+
const key = tokens[index].slice(2);
|
|
76
|
+
flags[key] = parsePermissionValue(tokens[index + 1] ?? '');
|
|
77
|
+
index += 2;
|
|
78
|
+
}
|
|
79
|
+
return { flags, index };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseFlagMatches(tokens, options = {}) {
|
|
83
|
+
const matches = {};
|
|
84
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
85
|
+
const token = tokens[index];
|
|
86
|
+
if (!token.startsWith('--')) continue;
|
|
87
|
+
const key = token.slice(2);
|
|
88
|
+
const value = parsePermissionValue(tokens[index + 1] ?? '', options);
|
|
89
|
+
index += 1;
|
|
90
|
+
setMatchValue(matches, key, value);
|
|
91
|
+
}
|
|
92
|
+
return matches;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parsePermissionValue(value, options = {}) {
|
|
96
|
+
if (value === 'undefined') return options.undefinedPattern ? { ...UNDEFINED_MATCH_VALUE } : undefined;
|
|
97
|
+
if (/^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(value)) return JSON.parse(value);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setMatchValue(target, key, value) {
|
|
102
|
+
const parts = key.split('.').filter(Boolean);
|
|
103
|
+
let current = target;
|
|
104
|
+
for (const part of parts.slice(0, -1)) {
|
|
105
|
+
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) current[part] = {};
|
|
106
|
+
current = current[part];
|
|
107
|
+
}
|
|
108
|
+
const finalKey = parts.at(-1) ?? key;
|
|
109
|
+
if (!Object.hasOwn(current, finalKey)) current[finalKey] = value;
|
|
110
|
+
else if (Array.isArray(current[finalKey])) current[finalKey].push(value);
|
|
111
|
+
else current[finalKey] = [current[finalKey], value];
|
|
112
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { BUILTIN_PERMISSIONS } from '../constants.mjs';
|
|
3
|
-
import { readSettings, readEffectiveSettings, writeSettings } from '../settings/load.mjs';
|
|
4
|
-
import { globMatch } from '../util/glob.mjs';
|
|
5
|
-
import { shellQuote, splitShellWords } from '../util/shell.mjs';
|
|
6
1
|
import { UsageError } from '../cli/parse.mjs';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
import { readSettings, writeSettings } from '../settings/load.mjs';
|
|
3
|
+
import { evaluatePermission, resolvePermissionDecision } from './permissions-eval.mjs';
|
|
4
|
+
import {
|
|
5
|
+
PERMISSIONS_SETTING,
|
|
6
|
+
builtinPermissionRules,
|
|
7
|
+
loadUserPermissionRules,
|
|
8
|
+
} from './permissions-rules.mjs';
|
|
9
|
+
import {
|
|
10
|
+
formatPermissionRule,
|
|
11
|
+
parseFlagMatches,
|
|
12
|
+
parsePermissionRule,
|
|
13
|
+
parsePermissionText,
|
|
14
|
+
} from './permissions-text.mjs';
|
|
15
|
+
|
|
16
|
+
export { resolvePermissionDecision };
|
|
13
17
|
|
|
14
18
|
export async function runPermissions(args, stdin = '', parsed = {}) {
|
|
15
19
|
const subcommand = args[0] ?? 'list';
|
|
@@ -56,273 +60,3 @@ export async function runPermissions(args, stdin = '', parsed = {}) {
|
|
|
56
60
|
|
|
57
61
|
throw new UsageError(`Unknown permissions command: ${subcommand}`);
|
|
58
62
|
}
|
|
59
|
-
|
|
60
|
-
export function loadUserPermissionRules(parsed = {}) {
|
|
61
|
-
const settings = readEffectiveSettings(parsed);
|
|
62
|
-
return Array.isArray(settings[PERMISSIONS_SETTING]) ? settings[PERMISSIONS_SETTING] : [];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function formatPermissionRule(rule) {
|
|
66
|
-
const parts = [rule.action];
|
|
67
|
-
for (const key of ['context', 'to', 'message']) {
|
|
68
|
-
if (rule[key] !== undefined) parts.push(`--${key}`, formatPermissionToken(rule[key]));
|
|
69
|
-
}
|
|
70
|
-
parts.push(formatPermissionToken(rule.tool));
|
|
71
|
-
for (const [key, value] of flattenMatches(rule.matches)) {
|
|
72
|
-
parts.push(`--${key}`, formatPermissionToken(value));
|
|
73
|
-
}
|
|
74
|
-
return parts.join(' ');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function flattenMatches(matches, prefix = '') {
|
|
78
|
-
if (!matches) return [];
|
|
79
|
-
const entries = [];
|
|
80
|
-
for (const [key, value] of Object.entries(matches)) {
|
|
81
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
82
|
-
if (Array.isArray(value)) {
|
|
83
|
-
for (const entry of value) entries.push(...flattenMatchValue(fullKey, entry));
|
|
84
|
-
} else {
|
|
85
|
-
entries.push(...flattenMatchValue(fullKey, value));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return entries;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function flattenMatchValue(key, value) {
|
|
92
|
-
if (isUndefinedMatchValue(value)) return [[key, value]];
|
|
93
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) return flattenMatches(value, key);
|
|
94
|
-
return [[key, value]];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function formatPermissionToken(value) {
|
|
98
|
-
if (isUndefinedMatchValue(value)) return 'undefined';
|
|
99
|
-
if (value === null) return 'null';
|
|
100
|
-
if (value === undefined) return 'undefined';
|
|
101
|
-
if (typeof value === 'boolean' || typeof value === 'number') return String(value);
|
|
102
|
-
const text = String(value);
|
|
103
|
-
return /^[A-Za-z0-9_./:@=-]+$/.test(text) ? text : shellQuote(text);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function parsePermissionText(input) {
|
|
107
|
-
return input
|
|
108
|
-
.split(/\r?\n/)
|
|
109
|
-
.map((line) => line.trim())
|
|
110
|
-
.filter((line) => line && !line.startsWith('#'))
|
|
111
|
-
.map((line) => parsePermissionRule(splitShellWords(line)));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function parsePermissionRule(tokens) {
|
|
115
|
-
const [action] = tokens;
|
|
116
|
-
const { flags: actionArgs, index: toolIndex } = parseActionArgs(tokens, 1);
|
|
117
|
-
const tool = tokens[toolIndex];
|
|
118
|
-
const rest = tokens.slice(toolIndex + 1);
|
|
119
|
-
if (!action || !tool) throw new UsageError('permission rules require: <action> <tool>');
|
|
120
|
-
const rule = { action, tool, ...actionArgs };
|
|
121
|
-
const matches = parseFlagMatches(rest, { undefinedPattern: true });
|
|
122
|
-
if (Object.keys(matches).length > 0) rule.matches = matches;
|
|
123
|
-
if (matches.to) {
|
|
124
|
-
rule.to = matches.to;
|
|
125
|
-
delete rule.matches.to;
|
|
126
|
-
if (Object.keys(rule.matches).length === 0) delete rule.matches;
|
|
127
|
-
}
|
|
128
|
-
return rule;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function parseActionArgs(tokens, startIndex) {
|
|
132
|
-
const flags = {};
|
|
133
|
-
let index = startIndex;
|
|
134
|
-
while (tokens[index]?.startsWith('--')) {
|
|
135
|
-
const key = tokens[index].slice(2);
|
|
136
|
-
flags[key] = parsePermissionValue(tokens[index + 1] ?? '');
|
|
137
|
-
index += 2;
|
|
138
|
-
}
|
|
139
|
-
return { flags, index };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function parseFlagMatches(tokens, options = {}) {
|
|
143
|
-
return parseFlagMatchesWithOptions(tokens, options);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function parseFlagMatchesWithOptions(tokens, options = {}) {
|
|
147
|
-
const matches = {};
|
|
148
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
149
|
-
const token = tokens[index];
|
|
150
|
-
if (!token.startsWith('--')) continue;
|
|
151
|
-
const key = token.slice(2);
|
|
152
|
-
const value = parsePermissionValue(tokens[index + 1] ?? '', options);
|
|
153
|
-
index += 1;
|
|
154
|
-
setMatchValue(matches, key, value);
|
|
155
|
-
}
|
|
156
|
-
return matches;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function parsePermissionValue(value, options = {}) {
|
|
160
|
-
if (value === 'undefined') return options.undefinedPattern ? { ...UNDEFINED_MATCH_VALUE } : undefined;
|
|
161
|
-
if (/^(?:true|false|null|-?\d+(?:\.\d+)?)$/.test(value)) return JSON.parse(value);
|
|
162
|
-
return value;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function setMatchValue(target, key, value) {
|
|
166
|
-
const parts = key.split('.').filter(Boolean);
|
|
167
|
-
let current = target;
|
|
168
|
-
for (const part of parts.slice(0, -1)) {
|
|
169
|
-
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) current[part] = {};
|
|
170
|
-
current = current[part];
|
|
171
|
-
}
|
|
172
|
-
const finalKey = parts.at(-1) ?? key;
|
|
173
|
-
if (!hasOwn(current, finalKey)) current[finalKey] = value;
|
|
174
|
-
else if (Array.isArray(current[finalKey])) current[finalKey].push(value);
|
|
175
|
-
else current[finalKey] = [current[finalKey], value];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function evaluatePermission(tool, toolArgs, parsed = {}, options = {}) {
|
|
179
|
-
const ruleGroups = [
|
|
180
|
-
{ source: 'user', rules: loadUserPermissionRules(parsed) },
|
|
181
|
-
{ source: 'command-allowlist', rules: commandAllowlistPermissionRules(parsed) },
|
|
182
|
-
{
|
|
183
|
-
source: 'built-in',
|
|
184
|
-
rules: builtinPermissionRules(),
|
|
185
|
-
},
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
for (const group of ruleGroups) {
|
|
189
|
-
for (const [index, rule] of group.rules.entries()) {
|
|
190
|
-
if (!globMatch(rule.tool, tool)) continue;
|
|
191
|
-
if (!matchesContext(rule.context, options.context ?? 'thread')) continue;
|
|
192
|
-
if (!matchesArguments(rule.matches, toolArgs)) continue;
|
|
193
|
-
return {
|
|
194
|
-
action: rule.action,
|
|
195
|
-
to: rule.to,
|
|
196
|
-
message: rule.message,
|
|
197
|
-
matchedRule: index,
|
|
198
|
-
source: group.source,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return { action: 'reject', source: 'default' };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function builtinPermissionRules() {
|
|
207
|
-
return BUILTIN_PERMISSIONS.map(([action, builtinTool, cmd]) => ({
|
|
208
|
-
action,
|
|
209
|
-
tool: builtinTool,
|
|
210
|
-
matches: builtinTool === 'Bash' ? { cmd: `${cmd}*` } : undefined,
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function commandAllowlistPermissionRules(parsed = {}) {
|
|
215
|
-
const allowlist = readEffectiveSettings(parsed)[COMMAND_ALLOWLIST_SETTING];
|
|
216
|
-
if (!Array.isArray(allowlist)) return [];
|
|
217
|
-
return allowlist
|
|
218
|
-
.map((command) => String(command).trim())
|
|
219
|
-
.filter(Boolean)
|
|
220
|
-
.map((command) => ({
|
|
221
|
-
action: 'allow',
|
|
222
|
-
tool: 'Bash',
|
|
223
|
-
matches: { cmd: commandAllowlistPattern(command) },
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function commandAllowlistPattern(command) {
|
|
228
|
-
return `/^${escapeRegex(command)}(?:\\s|$).*/`;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function escapeRegex(value) {
|
|
232
|
-
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export function resolvePermissionDecision(tool, toolArgs, parsed = {}, options = {}) {
|
|
236
|
-
const settings = readEffectiveSettings(parsed);
|
|
237
|
-
if (isDangerouslyAllowAll(parsed, settings)) return { action: 'allow', source: 'dangerously-allow-all' };
|
|
238
|
-
if (!legacyPermissionsConfigured(settings)) return { action: 'allow', source: 'default-no-approval' };
|
|
239
|
-
const decision = evaluatePermission(tool, toolArgs, parsed, options);
|
|
240
|
-
if (decision.action !== 'delegate') return decision;
|
|
241
|
-
if (!decision.to) {
|
|
242
|
-
return { ...decision, action: 'reject', message: 'Delegate permission rule is missing a target program' };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const result = spawnSync(decision.to, {
|
|
246
|
-
input: `${JSON.stringify(toolArgs)}\n`,
|
|
247
|
-
env: {
|
|
248
|
-
...process.env,
|
|
249
|
-
AGENT: 'coven-code',
|
|
250
|
-
AGENT_TOOL_NAME: tool,
|
|
251
|
-
COVEN_CODE_THREAD_ID: options.threadId ?? '',
|
|
252
|
-
AGENT_THREAD_ID: options.threadId ?? '',
|
|
253
|
-
},
|
|
254
|
-
encoding: 'utf8',
|
|
255
|
-
shell: false,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
if (result.status === 0) return { ...decision, action: 'allow', delegatedAction: 'allow' };
|
|
259
|
-
if (result.status === 1) return { ...decision, action: 'ask', delegatedAction: 'ask' };
|
|
260
|
-
return {
|
|
261
|
-
...decision,
|
|
262
|
-
action: 'reject',
|
|
263
|
-
delegatedAction: 'reject',
|
|
264
|
-
message: result.stderr.trim() || result.error?.message || `Permission delegate ${decision.to} rejected the tool call`,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function isDangerouslyAllowAll(parsed = {}, settings = readEffectiveSettings(parsed)) {
|
|
269
|
-
if (parsed.dangerouslyAllowAll) return true;
|
|
270
|
-
return settings[DANGEROUSLY_ALLOW_ALL_SETTING] === true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function legacyPermissionsConfigured(settings = {}) {
|
|
274
|
-
return hasOwn(settings, PERMISSIONS_SETTING)
|
|
275
|
-
|| hasOwn(settings, GUARDED_FILES_ALLOWLIST_SETTING)
|
|
276
|
-
|| hasOwn(settings, COMMAND_ALLOWLIST_SETTING)
|
|
277
|
-
|| settings[DANGEROUSLY_ALLOW_ALL_SETTING] === false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function hasOwn(value, key) {
|
|
281
|
-
return Object.prototype.hasOwnProperty.call(value, key);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function matchesContext(ruleContext, actualContext) {
|
|
285
|
-
return !ruleContext || ruleContext === actualContext;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function matchesArguments(matches, toolArgs) {
|
|
289
|
-
if (!matches || Object.keys(matches).length === 0) return true;
|
|
290
|
-
for (const [key, pattern] of Object.entries(matches)) {
|
|
291
|
-
const actual = valueAtPath(toolArgs, key);
|
|
292
|
-
if (!matchesPattern(pattern, actual)) return false;
|
|
293
|
-
}
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function matchesPattern(pattern, actual) {
|
|
298
|
-
if (isUndefinedMatchValue(pattern)) return actual === undefined;
|
|
299
|
-
if (Array.isArray(pattern)) return pattern.some((entry) => matchesPattern(entry, actual));
|
|
300
|
-
if (pattern && typeof pattern === 'object') {
|
|
301
|
-
if (!actual || typeof actual !== 'object') return false;
|
|
302
|
-
return Object.entries(pattern).every(([key, value]) => matchesPattern(value, valueAtPath(actual, key)));
|
|
303
|
-
}
|
|
304
|
-
if (typeof pattern === 'string') {
|
|
305
|
-
if (typeof actual !== 'string') return false;
|
|
306
|
-
if (isRegexPattern(pattern)) return new RegExp(pattern.slice(1, -1)).test(actual);
|
|
307
|
-
return globMatch(pattern, actual);
|
|
308
|
-
}
|
|
309
|
-
return Object.is(pattern, actual);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function isRegexPattern(value) {
|
|
313
|
-
return value.length >= 2 && value.startsWith('/') && value.endsWith('/');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function valueAtPath(value, key) {
|
|
317
|
-
return key.split('.').reduce((current, part) => current?.[part], value);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function isUndefinedMatchValue(value) {
|
|
321
|
-
return Boolean(
|
|
322
|
-
value
|
|
323
|
-
&& typeof value === 'object'
|
|
324
|
-
&& !Array.isArray(value)
|
|
325
|
-
&& value.__covenCodeLiteral === 'undefined'
|
|
326
|
-
&& Object.keys(value).length === 1,
|
|
327
|
-
);
|
|
328
|
-
}
|
package/src/commands/usage.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listThreads } from '../threads/store.mjs';
|
|
2
|
-
import { estimateTokenCount } from '../agent/
|
|
2
|
+
import { estimateTokenCount } from '../agent/fixture.mjs';
|
|
3
3
|
import { readEffectiveSettings } from '../settings/load.mjs';
|
|
4
4
|
|
|
5
5
|
export function runUsage(parsed = {}) {
|
package/src/constants.mjs
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const packageJson = JSON.parse(
|
|
4
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
export const VERSION = packageJson.version;
|
|
2
8
|
export const PRODUCT_NAME = 'Coven Code';
|
|
3
9
|
export const CLI_NAME = 'coven-code';
|
|
4
10
|
export const PACKAGE_NAME = '@opencoven/coven-code';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { parseMcpCallOutput, parseMcpResourceOutput, parseMcpToolsOutput } from './parsers.mjs';
|
|
3
|
+
|
|
4
|
+
export function queryLocalMcpTools(config = {}) {
|
|
5
|
+
return parseMcpToolsOutput(queryLocalMcpToolsResult(config).stdout ?? '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function queryLocalMcpToolsResult(config = {}) {
|
|
9
|
+
if (!config.command) return [];
|
|
10
|
+
return spawnSync(config.command, config.args ?? [], {
|
|
11
|
+
input: localMcpRequestInput('tools/list', {}),
|
|
12
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
timeout: 1500,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function callLocalMcpTool(config = {}, name, args = {}) {
|
|
19
|
+
if (!config.command) return '';
|
|
20
|
+
const result = spawnSync(config.command, config.args ?? [], {
|
|
21
|
+
input: localMcpRequestInput('tools/call', { name, arguments: args }),
|
|
22
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
timeout: 1500,
|
|
25
|
+
});
|
|
26
|
+
return parseMcpCallOutput(result.stdout);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readLocalMcpResource(config = {}, uri) {
|
|
30
|
+
if (!config.command) return '';
|
|
31
|
+
const result = spawnSync(config.command, config.args ?? [], {
|
|
32
|
+
input: localMcpRequestInput('resources/read', { uri }),
|
|
33
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
timeout: 1500,
|
|
36
|
+
});
|
|
37
|
+
return parseMcpResourceOutput(result.stdout);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function localMcpRequestInput(method, params = {}) {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
jsonrpc: '2.0',
|
|
44
|
+
id: 1,
|
|
45
|
+
method: 'initialize',
|
|
46
|
+
params: {
|
|
47
|
+
protocolVersion: '2025-06-18',
|
|
48
|
+
capabilities: {},
|
|
49
|
+
clientInfo: { name: 'coven-code', version: '0.0.0' },
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{ jsonrpc: '2.0', method: 'notifications/initialized' },
|
|
53
|
+
{ jsonrpc: '2.0', id: 2, method, params },
|
|
54
|
+
].map((message) => JSON.stringify(message)).join('\n') + '\n';
|
|
55
|
+
}
|