@musashishao/folderforge 1.2.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 +181 -0
- package/dist/adapters/child-mcp/client.js +114 -0
- package/dist/adapters/child-mcp/registry.js +66 -0
- package/dist/audit/audit-log.js +45 -0
- package/dist/audit/event-types.js +1 -0
- package/dist/core/config.js +211 -0
- package/dist/core/container.js +51 -0
- package/dist/core/errors.js +37 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/types.js +4 -0
- package/dist/dashboard/server.js +191 -0
- package/dist/lsp/protocol.js +116 -0
- package/dist/main.js +190 -0
- package/dist/managers/db-manager.js +161 -0
- package/dist/managers/lsp-manager.js +269 -0
- package/dist/managers/process-manager.js +140 -0
- package/dist/policy/approvals.js +143 -0
- package/dist/policy/command-policy.js +99 -0
- package/dist/policy/glob-match.js +61 -0
- package/dist/policy/path-policy.js +73 -0
- package/dist/policy/policy-engine.js +156 -0
- package/dist/policy/rate-limiter.js +96 -0
- package/dist/policy/risk.js +112 -0
- package/dist/policy/secret-policy.js +132 -0
- package/dist/server/mcp-server.js +144 -0
- package/dist/server/transports/http.js +133 -0
- package/dist/server/transports/stdio.js +14 -0
- package/dist/tools/adapter-tools.js +62 -0
- package/dist/tools/browser-tools.js +76 -0
- package/dist/tools/build-tools.js +78 -0
- package/dist/tools/code-tools.js +250 -0
- package/dist/tools/coverage-tools.js +135 -0
- package/dist/tools/db-tools.js +130 -0
- package/dist/tools/diff-util.js +45 -0
- package/dist/tools/error-parser.js +57 -0
- package/dist/tools/file-tools.js +319 -0
- package/dist/tools/format-tools.js +118 -0
- package/dist/tools/git-tools.js +371 -0
- package/dist/tools/index.js +63 -0
- package/dist/tools/memory-tools.js +54 -0
- package/dist/tools/output-schemas.js +100 -0
- package/dist/tools/pagination.js +92 -0
- package/dist/tools/pkg-tools.js +260 -0
- package/dist/tools/process-tools.js +128 -0
- package/dist/tools/registry.js +194 -0
- package/dist/tools/schema-lock.js +152 -0
- package/dist/tools/search-tools.js +176 -0
- package/dist/tools/security-tools.js +147 -0
- package/dist/tools/terminal-tools.js +57 -0
- package/dist/tools/workspace-tools.js +186 -0
- package/dist/workspace/memory-store.js +67 -0
- package/dist/workspace/onboarding.js +46 -0
- package/dist/workspace/project-detector.js +95 -0
- package/dist/workspace/workspace-manager.js +106 -0
- package/docs/adapters.md +76 -0
- package/docs/architecture.md +66 -0
- package/docs/roadmap.md +172 -0
- package/docs/security.md +94 -0
- package/docs/tools.md +129 -0
- package/examples/claude-desktop.json +18 -0
- package/examples/codex.toml +18 -0
- package/examples/config.basic.yaml +37 -0
- package/examples/config.full.yaml +120 -0
- package/package.json +74 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FolderForge tool-schema lock (1.0 freeze).
|
|
3
|
+
*
|
|
4
|
+
* This file is the SOURCE OF TRUTH for the public native tool surface at 1.0. Tool
|
|
5
|
+
* names and their `mutates` / `risk` contract are frozen here. Any change that
|
|
6
|
+
* renames a tool, removes one, or alters its mutation/risk classification is a
|
|
7
|
+
* BREAKING change: it must be a major-version bump and an intentional edit to
|
|
8
|
+
* this file.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: some tools (e.g. `parse_errors`) are internal helpers but still live in
|
|
11
|
+
* the native registry. They are frozen here because the schema-lock guard is
|
|
12
|
+
* intentionally strict: CI should catch any accidental changes to the live tool
|
|
13
|
+
* catalog.
|
|
14
|
+
*
|
|
15
|
+
* Adding a brand-new tool is backwards-compatible: register it, then add an
|
|
16
|
+
* entry here. The guard test (`tests/unit/schema-lock.test.ts`) fails if the
|
|
17
|
+
* live registry and this lock ever diverge, so accidental renames/removals are
|
|
18
|
+
* caught in CI.
|
|
19
|
+
*
|
|
20
|
+
* DO NOT edit casually. Treat edits to this file as API changes.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The frozen 1.0 native tool catalog. Child-MCP adapter tools (namespaced,
|
|
24
|
+
* e.g. `serena__find_symbol`) are intentionally NOT part of the frozen surface
|
|
25
|
+
* because they are discovered dynamically from external servers.
|
|
26
|
+
*/
|
|
27
|
+
export const FROZEN_TOOLS = [
|
|
28
|
+
// --- workspace ---
|
|
29
|
+
{ name: 'workspace_status', mutates: false, risk: 'LOW' },
|
|
30
|
+
{ name: 'workspace_list', mutates: false, risk: 'LOW' },
|
|
31
|
+
{ name: 'workspace_health', mutates: false, risk: 'LOW' },
|
|
32
|
+
{ name: 'workspace_route', mutates: false, risk: 'LOW' },
|
|
33
|
+
{ name: 'workspace_activate', mutates: true, risk: 'MEDIUM' },
|
|
34
|
+
{ name: 'workspace_switch', mutates: true, risk: 'MEDIUM' },
|
|
35
|
+
{ name: 'workspace_deactivate', mutates: true, risk: 'MEDIUM' },
|
|
36
|
+
{ name: 'workspace_onboard', mutates: true, risk: 'MEDIUM' },
|
|
37
|
+
{ name: 'project_detect_commands', mutates: false, risk: 'LOW' },
|
|
38
|
+
// --- files ---
|
|
39
|
+
{ name: 'file_read', mutates: false, risk: 'LOW' },
|
|
40
|
+
{ name: 'file_read_many', mutates: false, risk: 'LOW' },
|
|
41
|
+
{ name: 'file_write', mutates: true, risk: 'MEDIUM' },
|
|
42
|
+
{ name: 'file_patch', mutates: true, risk: 'MEDIUM' },
|
|
43
|
+
{ name: 'file_edit_block', mutates: true, risk: 'MEDIUM' },
|
|
44
|
+
{ name: 'file_move', mutates: true, risk: 'MEDIUM' },
|
|
45
|
+
{ name: 'file_copy', mutates: true, risk: 'MEDIUM' },
|
|
46
|
+
{ name: 'list_directory', mutates: false, risk: 'LOW' },
|
|
47
|
+
{ name: 'file_delete', mutates: true, risk: 'HIGH' },
|
|
48
|
+
// --- search ---
|
|
49
|
+
{ name: 'search_files', mutates: false, risk: 'LOW' },
|
|
50
|
+
{ name: 'search_text', mutates: false, risk: 'LOW' },
|
|
51
|
+
{ name: 'search_ast', mutates: false, risk: 'LOW' },
|
|
52
|
+
// --- terminal ---
|
|
53
|
+
{ name: 'shell_exec', mutates: true, risk: 'MEDIUM' },
|
|
54
|
+
// --- processes ---
|
|
55
|
+
{ name: 'process_start', mutates: true, risk: 'MEDIUM' },
|
|
56
|
+
{ name: 'process_read', mutates: false, risk: 'LOW' },
|
|
57
|
+
{ name: 'process_tail', mutates: false, risk: 'LOW' },
|
|
58
|
+
{ name: 'process_write', mutates: true, risk: 'MEDIUM' },
|
|
59
|
+
{ name: 'process_stop', mutates: true, risk: 'MEDIUM' },
|
|
60
|
+
{ name: 'process_list', mutates: false, risk: 'LOW' },
|
|
61
|
+
{ name: 'process_kill', mutates: true, risk: 'HIGH' },
|
|
62
|
+
// --- git ---
|
|
63
|
+
{ name: 'git_status', mutates: false, risk: 'LOW' },
|
|
64
|
+
{ name: 'git_diff', mutates: false, risk: 'LOW' },
|
|
65
|
+
{ name: 'git_log', mutates: false, risk: 'LOW' },
|
|
66
|
+
{ name: 'git_show', mutates: false, risk: 'LOW' },
|
|
67
|
+
{ name: 'git_blame', mutates: false, risk: 'LOW' },
|
|
68
|
+
{ name: 'git_branch', mutates: false, risk: 'LOW' },
|
|
69
|
+
{ name: 'git_fetch', mutates: true, risk: 'MEDIUM' },
|
|
70
|
+
{ name: 'git_pull', mutates: true, risk: 'HIGH' },
|
|
71
|
+
{ name: 'git_stash', mutates: true, risk: 'MEDIUM' },
|
|
72
|
+
{ name: 'git_add', mutates: true, risk: 'MEDIUM' },
|
|
73
|
+
{ name: 'git_checkout', mutates: true, risk: 'MEDIUM' },
|
|
74
|
+
{ name: 'git_commit', mutates: true, risk: 'HIGH' },
|
|
75
|
+
{ name: 'git_push', mutates: true, risk: 'CRITICAL' },
|
|
76
|
+
{ name: 'git_reset', mutates: true, risk: 'CRITICAL' },
|
|
77
|
+
// --- build / quality ---
|
|
78
|
+
{ name: 'run_test', mutates: false, risk: 'LOW' },
|
|
79
|
+
{ name: 'run_lint', mutates: false, risk: 'LOW' },
|
|
80
|
+
{ name: 'run_typecheck', mutates: false, risk: 'LOW' },
|
|
81
|
+
{ name: 'run_build', mutates: true, risk: 'MEDIUM' },
|
|
82
|
+
{ name: 'run_coverage', mutates: false, risk: 'LOW' },
|
|
83
|
+
// NOTE: parse_errors is an internal helper tool used by build/quality tooling.
|
|
84
|
+
// It is part of the native registry and therefore frozen for 1.0.
|
|
85
|
+
{ name: 'parse_errors', mutates: false, risk: 'MEDIUM' },
|
|
86
|
+
// --- packages (Gap 2) ---
|
|
87
|
+
{ name: 'pkg_list', mutates: false, risk: 'LOW' },
|
|
88
|
+
{ name: 'pkg_outdated', mutates: false, risk: 'LOW' },
|
|
89
|
+
{ name: 'pkg_audit', mutates: false, risk: 'LOW' },
|
|
90
|
+
{ name: 'pkg_run', mutates: true, risk: 'MEDIUM' },
|
|
91
|
+
{ name: 'pkg_add', mutates: true, risk: 'HIGH' },
|
|
92
|
+
{ name: 'pkg_remove', mutates: true, risk: 'HIGH' },
|
|
93
|
+
// --- formatting (Gap 3) ---
|
|
94
|
+
{ name: 'format_check', mutates: false, risk: 'LOW' },
|
|
95
|
+
{ name: 'format_apply', mutates: true, risk: 'MEDIUM' },
|
|
96
|
+
// --- memory ---
|
|
97
|
+
{ name: 'memory_list', mutates: false, risk: 'LOW' },
|
|
98
|
+
{ name: 'memory_read', mutates: false, risk: 'LOW' },
|
|
99
|
+
{ name: 'memory_write', mutates: true, risk: 'MEDIUM' },
|
|
100
|
+
{ name: 'memory_update', mutates: true, risk: 'MEDIUM' },
|
|
101
|
+
// --- code intelligence ---
|
|
102
|
+
{ name: 'code_symbols_overview', mutates: false, risk: 'LOW' },
|
|
103
|
+
{ name: 'code_find_symbol', mutates: false, risk: 'LOW' },
|
|
104
|
+
{ name: 'code_find_references', mutates: false, risk: 'LOW' },
|
|
105
|
+
{ name: 'code_find_definition', mutates: false, risk: 'LOW' },
|
|
106
|
+
{ name: 'code_find_implementations', mutates: false, risk: 'LOW' },
|
|
107
|
+
{ name: 'code_diagnostics', mutates: false, risk: 'LOW' },
|
|
108
|
+
{ name: 'code_replace_symbol_body', mutates: true, risk: 'MEDIUM' },
|
|
109
|
+
{ name: 'code_insert_before_symbol', mutates: true, risk: 'MEDIUM' },
|
|
110
|
+
{ name: 'code_insert_after_symbol', mutates: true, risk: 'MEDIUM' },
|
|
111
|
+
{ name: 'code_rename_symbol', mutates: true, risk: 'MEDIUM' },
|
|
112
|
+
// --- browser ---
|
|
113
|
+
{ name: 'browser_snapshot', mutates: false, risk: 'LOW' },
|
|
114
|
+
{ name: 'browser_console', mutates: false, risk: 'LOW' },
|
|
115
|
+
{ name: 'browser_network', mutates: false, risk: 'LOW' },
|
|
116
|
+
{ name: 'browser_open', mutates: true, risk: 'MEDIUM' },
|
|
117
|
+
{ name: 'browser_click', mutates: true, risk: 'MEDIUM' },
|
|
118
|
+
{ name: 'browser_type', mutates: true, risk: 'MEDIUM' },
|
|
119
|
+
{ name: 'browser_screenshot', mutates: true, risk: 'MEDIUM' },
|
|
120
|
+
{ name: 'browser_close', mutates: true, risk: 'MEDIUM' },
|
|
121
|
+
{ name: 'browser_eval', mutates: true, risk: 'HIGH' },
|
|
122
|
+
// --- database ---
|
|
123
|
+
{ name: 'db_list_connections', mutates: false, risk: 'LOW' },
|
|
124
|
+
{ name: 'db_list_tables', mutates: false, risk: 'LOW' },
|
|
125
|
+
{ name: 'db_describe_table', mutates: false, risk: 'LOW' },
|
|
126
|
+
{ name: 'db_query_readonly', mutates: false, risk: 'LOW' },
|
|
127
|
+
{ name: 'db_explain', mutates: false, risk: 'LOW' },
|
|
128
|
+
{ name: 'db_connect', mutates: true, risk: 'MEDIUM' },
|
|
129
|
+
{ name: 'db_run_migration', mutates: true, risk: 'HIGH' },
|
|
130
|
+
{ name: 'db_write', mutates: true, risk: 'HIGH' },
|
|
131
|
+
// --- security ---
|
|
132
|
+
{ name: 'secret_scan', mutates: false, risk: 'LOW' },
|
|
133
|
+
// --- policy / audit / approvals ---
|
|
134
|
+
{ name: 'policy_get', mutates: false, risk: 'LOW' },
|
|
135
|
+
{ name: 'policy_explain', mutates: false, risk: 'LOW' },
|
|
136
|
+
{ name: 'policy_ratelimits', mutates: false, risk: 'LOW' },
|
|
137
|
+
{ name: 'policy_set_mode', mutates: true, risk: 'MEDIUM' },
|
|
138
|
+
{ name: 'audit_recent', mutates: false, risk: 'LOW' },
|
|
139
|
+
// NOTE: audit_export and approval_request are not in TOOL_RISK, so they fall
|
|
140
|
+
// back to the defineTool default of MEDIUM. They are frozen here at their
|
|
141
|
+
// ACTUAL runtime risk to keep the lock truthful. Reclassifying them to LOW is
|
|
142
|
+
// a deliberate, separate change (see docs/roadmap.md post-1.0 notes).
|
|
143
|
+
{ name: 'audit_export', mutates: false, risk: 'MEDIUM' },
|
|
144
|
+
{ name: 'approval_status', mutates: false, risk: 'LOW' },
|
|
145
|
+
{ name: 'approval_request', mutates: false, risk: 'MEDIUM' },
|
|
146
|
+
];
|
|
147
|
+
/** Set of frozen tool names for O(1) membership checks. */
|
|
148
|
+
export const FROZEN_TOOL_NAMES = new Set(FROZEN_TOOLS.map((t) => t.name));
|
|
149
|
+
/** Lookup the frozen contract for a tool name. */
|
|
150
|
+
export function frozenTool(name) {
|
|
151
|
+
return FROZEN_TOOLS.find((t) => t.name === name);
|
|
152
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
import fg from 'fast-glob';
|
|
4
|
+
import { defineTool } from './registry.js';
|
|
5
|
+
export function searchTools() {
|
|
6
|
+
return [
|
|
7
|
+
defineTool({
|
|
8
|
+
name: 'search_files',
|
|
9
|
+
description: 'Find files by name or glob pattern inside the workspace.',
|
|
10
|
+
group: 'search',
|
|
11
|
+
mutates: false,
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
glob: { type: 'string', description: 'Glob, e.g. src/**/*.ts' },
|
|
16
|
+
limit: { type: 'number' },
|
|
17
|
+
},
|
|
18
|
+
required: ['glob'],
|
|
19
|
+
},
|
|
20
|
+
handler: async (args, ctx) => {
|
|
21
|
+
const pattern = String(args.glob);
|
|
22
|
+
const matches = await fg(pattern, {
|
|
23
|
+
cwd: ctx.projectRoot,
|
|
24
|
+
dot: false,
|
|
25
|
+
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
26
|
+
onlyFiles: true,
|
|
27
|
+
});
|
|
28
|
+
const limit = Number(args.limit ?? 200);
|
|
29
|
+
return { ok: true, data: { matches: matches.slice(0, limit), total: matches.length } };
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
defineTool({
|
|
33
|
+
name: 'search_text',
|
|
34
|
+
description: 'Search text/regex across files (ripgrep-style, native).',
|
|
35
|
+
group: 'search',
|
|
36
|
+
mutates: false,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
query: { type: 'string' },
|
|
41
|
+
glob: { type: 'string' },
|
|
42
|
+
caseSensitive: { type: 'boolean' },
|
|
43
|
+
limit: { type: 'number' },
|
|
44
|
+
},
|
|
45
|
+
required: ['query'],
|
|
46
|
+
},
|
|
47
|
+
handler: async (args, ctx) => {
|
|
48
|
+
const glob = String(args.glob ?? '**/*');
|
|
49
|
+
const limit = Number(args.limit ?? 200);
|
|
50
|
+
const flags = args.caseSensitive ? 'g' : 'gi';
|
|
51
|
+
let re;
|
|
52
|
+
try {
|
|
53
|
+
re = new RegExp(String(args.query), flags);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Treat as literal if invalid regex.
|
|
57
|
+
re = new RegExp(escapeRegExp(String(args.query)), flags);
|
|
58
|
+
}
|
|
59
|
+
const files = await fg(glob, {
|
|
60
|
+
cwd: ctx.projectRoot,
|
|
61
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
62
|
+
onlyFiles: true,
|
|
63
|
+
absolute: true,
|
|
64
|
+
});
|
|
65
|
+
const results = [];
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
if (results.length >= limit)
|
|
68
|
+
break;
|
|
69
|
+
try {
|
|
70
|
+
if (statSync(file).size > 2_000_000)
|
|
71
|
+
continue;
|
|
72
|
+
const content = readFileSync(file, 'utf8');
|
|
73
|
+
const lines = content.split('\n');
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
re.lastIndex = 0;
|
|
76
|
+
if (re.test(lines[i])) {
|
|
77
|
+
results.push({
|
|
78
|
+
file: relative(ctx.projectRoot, file),
|
|
79
|
+
line: i + 1,
|
|
80
|
+
text: ctx.container.policy.secret.redact(lines[i].slice(0, 300)),
|
|
81
|
+
});
|
|
82
|
+
if (results.length >= limit)
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// skip unreadable/binary
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, data: { matches: results, count: results.length } };
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
defineTool({
|
|
95
|
+
name: 'search_ast',
|
|
96
|
+
description: 'Structural search for code declarations (functions, classes, methods, ' +
|
|
97
|
+
'interfaces, types, consts) by name across the workspace. Lightweight, ' +
|
|
98
|
+
'regex-backed structural matching - no language server required.',
|
|
99
|
+
group: 'search',
|
|
100
|
+
mutates: false,
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
name: { type: 'string', description: 'Symbol name or partial name to find.' },
|
|
105
|
+
kind: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Restrict to one kind: function | class | method | interface | type | const',
|
|
108
|
+
enum: ['function', 'class', 'method', 'interface', 'type', 'const'],
|
|
109
|
+
},
|
|
110
|
+
glob: { type: 'string', description: 'File glob to limit the search (default common source files).' },
|
|
111
|
+
limit: { type: 'number' },
|
|
112
|
+
},
|
|
113
|
+
required: ['name'],
|
|
114
|
+
},
|
|
115
|
+
handler: async (args, ctx) => {
|
|
116
|
+
const name = String(args.name);
|
|
117
|
+
const kindFilter = args.kind === undefined ? null : String(args.kind);
|
|
118
|
+
const glob = String(args.glob ?? '**/*.{ts,tsx,js,jsx,mjs,cjs,py,go,rs,java,rb,c,h,cpp,cs}');
|
|
119
|
+
const limit = Number(args.limit ?? 200);
|
|
120
|
+
const escaped = escapeRegExp(name);
|
|
121
|
+
// Structural declaration patterns. Each entry maps a kind to a regex whose
|
|
122
|
+
// match indicates a declaration of `name`.
|
|
123
|
+
const patterns = [
|
|
124
|
+
{ kind: 'function', re: new RegExp(`\\b(?:async\\s+)?function\\s+(${escaped})\\b`) },
|
|
125
|
+
{ kind: 'function', re: new RegExp(`\\b(?:export\\s+)?(?:const|let|var)\\s+(${escaped})\\s*=\\s*(?:async\\s*)?\\(`) },
|
|
126
|
+
{ kind: 'function', re: new RegExp(`\\bdef\\s+(${escaped})\\s*\\(`) }, // python
|
|
127
|
+
{ kind: 'function', re: new RegExp(`\\bfunc\\s+(${escaped})\\s*\\(`) }, // go
|
|
128
|
+
{ kind: 'class', re: new RegExp(`\\bclass\\s+(${escaped})\\b`) },
|
|
129
|
+
{ kind: 'interface', re: new RegExp(`\\binterface\\s+(${escaped})\\b`) },
|
|
130
|
+
{ kind: 'type', re: new RegExp(`\\btype\\s+(${escaped})\\b`) },
|
|
131
|
+
{ kind: 'const', re: new RegExp(`\\b(?:export\\s+)?(?:const|let|var)\\s+(${escaped})\\b`) },
|
|
132
|
+
{ kind: 'method', re: new RegExp(`^\\s*(?:public|private|protected|static|async|\\s)*\\b(${escaped})\\s*\\(`) },
|
|
133
|
+
].filter((p) => !kindFilter || p.kind === kindFilter);
|
|
134
|
+
const files = await fg(glob, {
|
|
135
|
+
cwd: ctx.projectRoot,
|
|
136
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
|
137
|
+
onlyFiles: true,
|
|
138
|
+
absolute: true,
|
|
139
|
+
});
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
if (results.length >= limit)
|
|
143
|
+
break;
|
|
144
|
+
try {
|
|
145
|
+
if (statSync(file).size > 2_000_000)
|
|
146
|
+
continue;
|
|
147
|
+
const lines = readFileSync(file, 'utf8').split('\n');
|
|
148
|
+
for (let i = 0; i < lines.length; i++) {
|
|
149
|
+
const line = lines[i];
|
|
150
|
+
for (const { kind, re } of patterns) {
|
|
151
|
+
if (re.test(line)) {
|
|
152
|
+
results.push({
|
|
153
|
+
file: relative(ctx.projectRoot, file),
|
|
154
|
+
line: i + 1,
|
|
155
|
+
kind,
|
|
156
|
+
text: line.trim().slice(0, 300),
|
|
157
|
+
});
|
|
158
|
+
break; // one hit per line is enough
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (results.length >= limit)
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// skip unreadable/binary
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, data: { matches: results, count: results.length } };
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
function escapeRegExp(s) {
|
|
175
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
176
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { defineTool } from './registry.js';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
export function securityTools() {
|
|
4
|
+
return [
|
|
5
|
+
defineTool({
|
|
6
|
+
name: 'policy_get',
|
|
7
|
+
description: 'Return the current policy: mode, approval rules, blocked commands, allowed dirs.',
|
|
8
|
+
group: 'security',
|
|
9
|
+
mutates: false,
|
|
10
|
+
inputSchema: { type: 'object', properties: {} },
|
|
11
|
+
handler: async (_a, ctx) => ({ ok: true, data: ctx.container.policy.describe() }),
|
|
12
|
+
}),
|
|
13
|
+
defineTool({
|
|
14
|
+
name: 'policy_explain',
|
|
15
|
+
description: 'Dry-run a tool call and explain the policy decision (allow | deny | approval) ' +
|
|
16
|
+
'and why, without executing the tool or creating an approval request.',
|
|
17
|
+
group: 'security',
|
|
18
|
+
mutates: false,
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
tool: { type: 'string', description: 'Name of the tool to evaluate.' },
|
|
23
|
+
args: { type: 'object', description: 'Args the call would use (used for risk classification).' },
|
|
24
|
+
},
|
|
25
|
+
required: ['tool'],
|
|
26
|
+
},
|
|
27
|
+
handler: async (args, ctx) => {
|
|
28
|
+
const toolName = String(args.tool);
|
|
29
|
+
const callArgs = args.args ?? {};
|
|
30
|
+
const registry = ctx.container.registry;
|
|
31
|
+
const def = registry?.get(toolName);
|
|
32
|
+
if (!def) {
|
|
33
|
+
return { ok: false, error: `Unknown tool: ${toolName}` };
|
|
34
|
+
}
|
|
35
|
+
// Mirror the per-call risk reclassification done in ToolRegistry.call:
|
|
36
|
+
// shell_exec risk depends on the actual command.
|
|
37
|
+
let risk = def.risk;
|
|
38
|
+
if (toolName === 'shell_exec' && typeof callArgs.command === 'string') {
|
|
39
|
+
risk = ctx.container.policy.command.classify(callArgs.command).risk;
|
|
40
|
+
}
|
|
41
|
+
const explanation = ctx.container.policy.explain(toolName, risk, def.mutates, callArgs);
|
|
42
|
+
return { ok: true, data: { tool: toolName, ...explanation } };
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
defineTool({
|
|
46
|
+
name: 'policy_set_mode',
|
|
47
|
+
description: 'Set the policy mode: readonly | safe | dev | danger.',
|
|
48
|
+
group: 'security',
|
|
49
|
+
mutates: true,
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: { mode: { type: 'string', enum: ['readonly', 'safe', 'dev', 'danger'] } },
|
|
53
|
+
required: ['mode'],
|
|
54
|
+
},
|
|
55
|
+
handler: async (args, ctx) => {
|
|
56
|
+
const mode = String(args.mode);
|
|
57
|
+
if (!['readonly', 'safe', 'dev', 'danger'].includes(mode)) {
|
|
58
|
+
return { ok: false, error: `Invalid mode: ${mode}` };
|
|
59
|
+
}
|
|
60
|
+
ctx.container.policy.setMode(mode);
|
|
61
|
+
return { ok: true, data: { mode } };
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
defineTool({
|
|
65
|
+
name: 'audit_recent',
|
|
66
|
+
description: 'Return recent audit events.',
|
|
67
|
+
group: 'security',
|
|
68
|
+
mutates: false,
|
|
69
|
+
inputSchema: { type: 'object', properties: { limit: { type: 'number' } } },
|
|
70
|
+
handler: async (args, ctx) => ({ ok: true, data: { events: ctx.container.audit.recent(Number(args.limit ?? 50)) } }),
|
|
71
|
+
}),
|
|
72
|
+
defineTool({
|
|
73
|
+
name: 'audit_export',
|
|
74
|
+
description: 'Return the path and contents of the JSONL audit log.',
|
|
75
|
+
group: 'security',
|
|
76
|
+
mutates: false,
|
|
77
|
+
inputSchema: { type: 'object', properties: {} },
|
|
78
|
+
handler: async (_a, ctx) => ({
|
|
79
|
+
ok: true,
|
|
80
|
+
data: { path: ctx.container.audit.exportPath(), jsonl: ctx.container.audit.exportRaw() },
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
83
|
+
defineTool({
|
|
84
|
+
name: 'approval_status',
|
|
85
|
+
description: 'Check the status of an approval request, or list pending ones.',
|
|
86
|
+
group: 'security',
|
|
87
|
+
mutates: false,
|
|
88
|
+
inputSchema: { type: 'object', properties: { id: { type: 'string' } } },
|
|
89
|
+
handler: async (args, ctx) => {
|
|
90
|
+
if (args.id) {
|
|
91
|
+
const req = ctx.container.policy.approvals.get(String(args.id));
|
|
92
|
+
return req ? { ok: true, data: req } : { ok: false, error: 'Approval not found.' };
|
|
93
|
+
}
|
|
94
|
+
return { ok: true, data: { pending: ctx.container.policy.approvals.pending() } };
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
defineTool({
|
|
98
|
+
name: 'approval_request',
|
|
99
|
+
description: 'Create a manual approval request for a tool action.',
|
|
100
|
+
group: 'security',
|
|
101
|
+
mutates: false,
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: { tool: { type: 'string' }, reason: { type: 'string' } },
|
|
105
|
+
required: ['tool'],
|
|
106
|
+
},
|
|
107
|
+
handler: async (args, ctx) => {
|
|
108
|
+
const req = ctx.container.policy.approvals.create(String(args.tool), {}, 'HIGH', String(args.reason ?? 'manual'));
|
|
109
|
+
return { ok: true, data: req };
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
defineTool({
|
|
113
|
+
name: 'policy_ratelimits',
|
|
114
|
+
description: 'Show per-tool rate-limit usage: configured window/quota and how many ' +
|
|
115
|
+
'calls have been used in the current window and rolling 24h.',
|
|
116
|
+
group: 'security',
|
|
117
|
+
mutates: false,
|
|
118
|
+
inputSchema: { type: 'object', properties: {} },
|
|
119
|
+
handler: async (_a, ctx) => ({
|
|
120
|
+
ok: true,
|
|
121
|
+
data: {
|
|
122
|
+
enabled: ctx.config.rateLimit.enabled,
|
|
123
|
+
default: ctx.config.rateLimit.default,
|
|
124
|
+
usage: ctx.container.rateLimiter.snapshot(),
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
}),
|
|
128
|
+
defineTool({
|
|
129
|
+
name: 'secret_scan',
|
|
130
|
+
description: 'Scan a file or text for potential secrets.',
|
|
131
|
+
group: 'security',
|
|
132
|
+
mutates: false,
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: { path: { type: 'string' }, text: { type: 'string' } },
|
|
136
|
+
},
|
|
137
|
+
handler: async (args, ctx) => {
|
|
138
|
+
let text = String(args.text ?? '');
|
|
139
|
+
if (args.path) {
|
|
140
|
+
const abs = ctx.container.policy.path.resolveSafe(String(args.path), ctx.projectRoot);
|
|
141
|
+
text = readFileSync(abs, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, data: { findings: ctx.container.policy.secret.scan(text) } };
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { defineTool } from './registry.js';
|
|
3
|
+
export function terminalTools() {
|
|
4
|
+
return [
|
|
5
|
+
defineTool({
|
|
6
|
+
name: 'shell_exec',
|
|
7
|
+
description: 'Run a single shell command in the workspace with timeout, blocklist, and output limits.',
|
|
8
|
+
group: 'terminal',
|
|
9
|
+
mutates: true,
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
command: { type: 'string' },
|
|
14
|
+
cwd: { type: 'string' },
|
|
15
|
+
timeoutMs: { type: 'number' },
|
|
16
|
+
},
|
|
17
|
+
required: ['command'],
|
|
18
|
+
},
|
|
19
|
+
handler: async (args, ctx) => {
|
|
20
|
+
const command = String(args.command);
|
|
21
|
+
const cls = ctx.container.policy.command.classify(command);
|
|
22
|
+
if (cls.risk === 'CRITICAL' && ctx.container.policy.getMode() !== 'danger') {
|
|
23
|
+
return { ok: false, error: `Blocked destructive command: ${cls.blockedReason ?? command}` };
|
|
24
|
+
}
|
|
25
|
+
const cwd = args.cwd
|
|
26
|
+
? ctx.container.policy.path.resolveSafe(String(args.cwd), ctx.projectRoot)
|
|
27
|
+
: ctx.projectRoot;
|
|
28
|
+
const timeout = Number(args.timeoutMs ?? ctx.config.terminal.defaultTimeoutMs);
|
|
29
|
+
const maxBytes = ctx.config.terminal.maxOutputBytes;
|
|
30
|
+
const started = Date.now();
|
|
31
|
+
try {
|
|
32
|
+
const sub = await execa(ctx.config.terminal.shell, ['-lc', command], {
|
|
33
|
+
cwd,
|
|
34
|
+
timeout,
|
|
35
|
+
reject: false,
|
|
36
|
+
all: false,
|
|
37
|
+
maxBuffer: maxBytes * 4,
|
|
38
|
+
});
|
|
39
|
+
const redact = (s) => ctx.container.policy.secret.redact((s ?? '').slice(0, maxBytes));
|
|
40
|
+
return {
|
|
41
|
+
ok: sub.exitCode === 0,
|
|
42
|
+
data: {
|
|
43
|
+
exitCode: sub.exitCode,
|
|
44
|
+
stdout: redact(sub.stdout),
|
|
45
|
+
stderr: redact(sub.stderr),
|
|
46
|
+
durationMs: Date.now() - started,
|
|
47
|
+
risk: cls.risk,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return { ok: false, error: `Execution failed: ${String(err)}` };
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
];
|
|
57
|
+
}
|