@iinm/plain-agent 1.10.20 → 1.10.22
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 +20 -40
- package/config/config.predefined.json +9 -0
- package/package.json +1 -1
- package/src/config.d.ts +3 -0
- package/src/config.mjs +9 -0
- package/src/main.mjs +6 -4
- package/src/prompt.mjs +1 -17
- package/src/tool.d.ts +2 -0
- package/src/toolInputValidator.mjs +71 -23
- package/src/toolUseApprover.mjs +4 -1
- package/src/tools/tmuxCommand.mjs +11 -1
- package/src/utils/evalJSONConfig.mjs +17 -0
package/README.md
CHANGED
|
@@ -345,14 +345,13 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
345
345
|
└── agents/ # Project-specific agent roles
|
|
346
346
|
```
|
|
347
347
|
|
|
348
|
-
### Example
|
|
349
|
-
|
|
350
348
|
<details>
|
|
351
349
|
<summary><b>YOLO mode example (requires a sandbox for safety)</b></summary>
|
|
352
350
|
|
|
353
351
|
```js
|
|
354
352
|
{
|
|
355
353
|
"autoApproval": {
|
|
354
|
+
// Deny all actions except explicitly allowed
|
|
356
355
|
"defaultAction": "deny",
|
|
357
356
|
"maxApprovals": 100,
|
|
358
357
|
"patterns": [
|
|
@@ -361,7 +360,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
361
360
|
"action": "allow"
|
|
362
361
|
},
|
|
363
362
|
{
|
|
364
|
-
"toolName":
|
|
363
|
+
"toolName": "exec_command",
|
|
365
364
|
"action": "allow"
|
|
366
365
|
},
|
|
367
366
|
{
|
|
@@ -378,17 +377,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
378
377
|
"sandbox": {
|
|
379
378
|
"command": "plain-sandbox",
|
|
380
379
|
"args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
|
|
381
|
-
"separator": "--"
|
|
382
|
-
"rules": [
|
|
383
|
-
{
|
|
384
|
-
"pattern": {
|
|
385
|
-
"command": "npm",
|
|
386
|
-
"args": ["ci"]
|
|
387
|
-
},
|
|
388
|
-
"mode": "sandbox",
|
|
389
|
-
"additionalArgs": ["--allow-net", "0.0.0.0/0"]
|
|
390
|
-
}
|
|
391
|
-
]
|
|
380
|
+
"separator": "--"
|
|
392
381
|
}
|
|
393
382
|
}
|
|
394
383
|
```
|
|
@@ -401,17 +390,16 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
401
390
|
{
|
|
402
391
|
"autoApproval": {
|
|
403
392
|
// Absolute paths outside the working directory that are allowed. Relative paths are ignored.
|
|
404
|
-
"allowedPaths": ["/
|
|
393
|
+
"allowedPaths": ["/path/to/other/git-repo"],
|
|
394
|
+
// Allow access to git-unmanaged files (default: false).
|
|
395
|
+
// ⚠️ Changes to git-unmanaged files are hard to detect (e.g., node_modules). Sandbox is recommended.
|
|
396
|
+
"allowGitUnmanagedFiles": false,
|
|
397
|
+
// Default action when no patterns match. Can be "ask" (prompt user) or "deny" (block action).
|
|
405
398
|
"defaultAction": "ask",
|
|
406
399
|
// Maximum number of automatic approvals.
|
|
407
400
|
"maxApprovals": 50,
|
|
408
401
|
// Patterns are evaluated in order. First match wins.
|
|
409
402
|
"patterns": [
|
|
410
|
-
{
|
|
411
|
-
"toolName": { "$regex": "^(write_file|patch_file)$" },
|
|
412
|
-
"input": { "filePath": { "$regex": "^\\.plain-agent/memory/.+\\.md$" } },
|
|
413
|
-
"action": "allow"
|
|
414
|
-
},
|
|
415
403
|
{
|
|
416
404
|
"toolName": { "$regex": "^(write_file|patch_file)$" },
|
|
417
405
|
"input": { "filePath": { "$regex": "^src/" } },
|
|
@@ -438,8 +426,17 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
438
426
|
]
|
|
439
427
|
},
|
|
440
428
|
|
|
429
|
+
"tools": {
|
|
430
|
+
// Enable web tools. See Quick Start section.
|
|
431
|
+
"webSearch": {},
|
|
432
|
+
"webFetch": {},
|
|
433
|
+
// Enable the tmux tool
|
|
434
|
+
"tmux": { "enabled": true }
|
|
435
|
+
},
|
|
436
|
+
|
|
441
437
|
// Sandbox environment for the exec_command and tmux_command tools
|
|
442
438
|
"sandbox": {
|
|
439
|
+
// Commands are wrapped and executed with this command
|
|
443
440
|
"command": "plain-sandbox",
|
|
444
441
|
"args": ["--allow-write", "--skip-build", "--keep-alive", "30"],
|
|
445
442
|
// separator is inserted between sandbox flags and the user command to prevent bypasses
|
|
@@ -457,16 +454,15 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
457
454
|
{
|
|
458
455
|
"pattern": {
|
|
459
456
|
"command": "npm",
|
|
460
|
-
"args": ["install"]
|
|
457
|
+
"args": [{ "$regex": "^(install|ci)$" }]
|
|
461
458
|
},
|
|
462
459
|
"mode": "sandbox",
|
|
463
|
-
// Allow access to registry.npmjs.org
|
|
464
460
|
"additionalArgs": ["--allow-net", "registry.npmjs.org"]
|
|
465
461
|
}
|
|
466
462
|
]
|
|
467
463
|
},
|
|
468
464
|
|
|
469
|
-
//
|
|
465
|
+
// MCP servers
|
|
470
466
|
"mcpServers": {
|
|
471
467
|
"chrome_devtools": {
|
|
472
468
|
"command": "npx",
|
|
@@ -484,27 +480,11 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
484
480
|
// Enable only specific tools. If not specified, all tools are enabled.
|
|
485
481
|
"enabledTools": ["notion-search", "notion-fetch"]
|
|
486
482
|
}
|
|
487
|
-
},
|
|
488
|
-
"aws_knowledge": {
|
|
489
|
-
"command": "npx",
|
|
490
|
-
"args": ["-y", "mcp-remote", "https://knowledge-mcp.global.api.aws"]
|
|
491
|
-
},
|
|
492
|
-
// ⚠️ Add this to config.local.json to avoid committing secrets to Git
|
|
493
|
-
"google_developer-knowledge": {
|
|
494
|
-
"command": "npx",
|
|
495
|
-
"args": ["-y", "mcp-remote", "https://developerknowledge.googleapis.com/mcp", "--header", "X-Goog-Api-Key:<GOOGLE_API_KEY>"]
|
|
496
483
|
}
|
|
497
484
|
},
|
|
498
485
|
|
|
499
486
|
// Override the default notification command
|
|
500
487
|
"notifyCmd": { "command": "plain-notify-desktop", "args": [] }
|
|
501
|
-
|
|
502
|
-
// Voice input. See "Voice Input" below.
|
|
503
|
-
// ⚠️ Add this to config.local.json to avoid committing secrets to Git
|
|
504
|
-
"voiceInput": {
|
|
505
|
-
"provider": "openai",
|
|
506
|
-
"apiKey": "<OPENAI_API_KEY>"
|
|
507
|
-
}
|
|
508
488
|
}
|
|
509
489
|
```
|
|
510
490
|
</details>
|
|
@@ -517,7 +497,7 @@ The agent can use the following tools:
|
|
|
517
497
|
- **write_file**: Write a file.
|
|
518
498
|
- **patch_file**: Patch a file.
|
|
519
499
|
- **exec_command**: Run a command without shell interpretation.
|
|
520
|
-
- **tmux_command**: Run a tmux command.
|
|
500
|
+
- **tmux_command**: Run a tmux command. It is disabled by default.
|
|
521
501
|
- **web_search**: Search the web with one or more keyword sets and answer a question based on the combined results (requires Google API key, Vertex AI configuration, or the `command` provider with a local search command).
|
|
522
502
|
- **web_fetch**: Fetch the contents of a single URL and answer a question based on it (requires Google API key, Vertex AI configuration, or the `command` provider with a local fetch command such as `w3m`, `curl`, or `lynx`).
|
|
523
503
|
- **switch_to_subagent**: Switch to a subagent role within the same conversation, focusing on the specified goal.
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
"action": "deny",
|
|
10
10
|
"reason": "Use rg or fd instead"
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"toolName": "exec_command",
|
|
14
|
+
"input": {
|
|
15
|
+
"command": "bash",
|
|
16
|
+
"args": ["-c", { "$not": { "$regex": "[|><&;$`]" } }]
|
|
17
|
+
},
|
|
18
|
+
"action": "deny",
|
|
19
|
+
"reason": "Use bash -c only when shell features (|, >, <, &, ;, $, `) are required"
|
|
20
|
+
},
|
|
12
21
|
{
|
|
13
22
|
"toolName": "exec_command",
|
|
14
23
|
"input": {
|
package/package.json
CHANGED
package/src/config.d.ts
CHANGED
|
@@ -76,11 +76,14 @@ export type AppConfig = {
|
|
|
76
76
|
defaultAction?: "deny" | "ask";
|
|
77
77
|
/** Additional absolute paths to allow for auto-approval (outside working directory) */
|
|
78
78
|
allowedPaths?: string[];
|
|
79
|
+
/** Allow access to git-unmanaged files (default: false) */
|
|
80
|
+
allowGitUnmanagedFiles?: boolean;
|
|
79
81
|
};
|
|
80
82
|
sandbox?: ExecCommandSanboxConfig;
|
|
81
83
|
tools?: {
|
|
82
84
|
webSearch?: WebSearchToolConfig;
|
|
83
85
|
webFetch?: WebFetchToolConfig;
|
|
86
|
+
tmux?: { enabled: boolean };
|
|
84
87
|
};
|
|
85
88
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
86
89
|
notifyCmd?: { command: string; args?: string[] };
|
package/src/config.mjs
CHANGED
|
@@ -77,6 +77,9 @@ export async function loadAppConfig(options = {}) {
|
|
|
77
77
|
...(config.autoApproval?.allowedPaths ?? []),
|
|
78
78
|
...(merged.autoApproval?.allowedPaths ?? []),
|
|
79
79
|
],
|
|
80
|
+
allowGitUnmanagedFiles:
|
|
81
|
+
config.autoApproval?.allowGitUnmanagedFiles ??
|
|
82
|
+
merged.autoApproval?.allowGitUnmanagedFiles,
|
|
80
83
|
},
|
|
81
84
|
sandbox: config.sandbox ?? merged.sandbox,
|
|
82
85
|
tools: {
|
|
@@ -92,6 +95,12 @@ export async function loadAppConfig(options = {}) {
|
|
|
92
95
|
...config.tools.webFetch,
|
|
93
96
|
}
|
|
94
97
|
: merged.tools?.webFetch,
|
|
98
|
+
tmux: config.tools?.tmux
|
|
99
|
+
? {
|
|
100
|
+
...(merged.tools?.tmux ?? {}),
|
|
101
|
+
...config.tools.tmux,
|
|
102
|
+
}
|
|
103
|
+
: merged.tools?.tmux,
|
|
95
104
|
},
|
|
96
105
|
mcpServers: {
|
|
97
106
|
...(merged.mcpServers ?? {}),
|
package/src/main.mjs
CHANGED
|
@@ -139,8 +139,6 @@ export async function main(argv = process.argv) {
|
|
|
139
139
|
? new Date(resumedState.startTime)
|
|
140
140
|
: new Date();
|
|
141
141
|
const sessionId = resumedState ? resumedState.sessionId : generateSessionId();
|
|
142
|
-
const tmuxSessionId = `agent-${sessionId}`;
|
|
143
|
-
|
|
144
142
|
const isBatchMode = cliArgs.subcommand.type === "batch";
|
|
145
143
|
/** @type {string[]} */
|
|
146
144
|
const configFiles =
|
|
@@ -294,7 +292,6 @@ export async function main(argv = process.argv) {
|
|
|
294
292
|
workingDir: process.cwd(),
|
|
295
293
|
today: new Date().toISOString().split("T")[0],
|
|
296
294
|
sessionId,
|
|
297
|
-
tmuxSessionId,
|
|
298
295
|
projectMetadataDir: AGENT_PROJECT_METADATA_DIR,
|
|
299
296
|
agentRoles,
|
|
300
297
|
skills: Array.from(prompts.values()).filter((p) => p.isSkill),
|
|
@@ -305,12 +302,15 @@ export async function main(argv = process.argv) {
|
|
|
305
302
|
readFileTool,
|
|
306
303
|
writeFileTool,
|
|
307
304
|
createPatchFileTool(),
|
|
308
|
-
createTmuxCommandTool({ sandbox: appConfig.sandbox }),
|
|
309
305
|
createCompactContextTool(),
|
|
310
306
|
createSwitchToSubagentTool(),
|
|
311
307
|
createSwitchToMainAgentTool(),
|
|
312
308
|
];
|
|
313
309
|
|
|
310
|
+
if (appConfig.tools?.tmux?.enabled) {
|
|
311
|
+
builtinTools.push(createTmuxCommandTool({ sandbox: appConfig.sandbox }));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
314
|
if (appConfig.tools?.webSearch) {
|
|
315
315
|
const webSearchConfig = appConfig.tools.webSearch;
|
|
316
316
|
if (webSearchConfig.provider === "command") {
|
|
@@ -369,6 +369,8 @@ export async function main(argv = process.argv) {
|
|
|
369
369
|
defaultAction: appConfig.autoApproval?.defaultAction || "ask",
|
|
370
370
|
patterns: appConfig.autoApproval?.patterns || [],
|
|
371
371
|
allowedPaths: appConfig.autoApproval?.allowedPaths ?? [],
|
|
372
|
+
allowGitUnmanagedFiles:
|
|
373
|
+
appConfig.autoApproval?.allowGitUnmanagedFiles ?? false,
|
|
372
374
|
maskApprovalInput: (toolName, input) => {
|
|
373
375
|
for (const tool of builtinTools) {
|
|
374
376
|
if (tool.def.name === toolName && tool.maskApprovalInput) {
|
package/src/prompt.mjs
CHANGED
|
@@ -7,7 +7,6 @@ import { toOneLine } from "./utils/toOneLine.mjs";
|
|
|
7
7
|
* @property {string} workingDir - The current working directory.
|
|
8
8
|
* @property {string} today - Today's date in YYYY-MM-DD format.
|
|
9
9
|
* @property {string} sessionId
|
|
10
|
-
* @property {string} tmuxSessionId
|
|
11
10
|
* @property {string} projectMetadataDir - The directory where memory files are stored.
|
|
12
11
|
* @property {Map<string, import('./context/loadAgentRoles.mjs').AgentRole>} agentRoles - Available agent roles.
|
|
13
12
|
* @property {{filePath: string, description: string}[]} skills
|
|
@@ -22,7 +21,6 @@ export function createPrompt({
|
|
|
22
21
|
modelName,
|
|
23
22
|
sessionId,
|
|
24
23
|
today,
|
|
25
|
-
tmuxSessionId,
|
|
26
24
|
workingDir,
|
|
27
25
|
projectMetadataDir,
|
|
28
26
|
agentRoles,
|
|
@@ -73,9 +71,8 @@ Always read the target lines with \`read_file\` first to verify line numbers and
|
|
|
73
71
|
|
|
74
72
|
## exec_command
|
|
75
73
|
|
|
76
|
-
- Use relative paths.
|
|
74
|
+
- Use relative paths for files inside the working directory, absolute paths for files outside.
|
|
77
75
|
- Use ${projectMetadataDir}/tmp/ for temporary files.
|
|
78
|
-
- Use bash -c only when pipes (|) or redirection (>, <) are required.
|
|
79
76
|
|
|
80
77
|
Examples:
|
|
81
78
|
- List directories or find files: fd [".", "./", "--max-depth", "3", "--type", "d", "--hidden"]
|
|
@@ -84,18 +81,6 @@ Examples:
|
|
|
84
81
|
Get PR details: gh ["pr", "view", "123", "--json", "title,body,url"]
|
|
85
82
|
Get PR comment: gh ["api", "--method", "GET", "repos/<owner>/<repo>/pulls/comments/<id>", "--jq", "{user: .user.login, path: .path, line: .line, body: .body}"]
|
|
86
83
|
|
|
87
|
-
## tmux_command
|
|
88
|
-
|
|
89
|
-
- Use only when the user explicitly requests it.
|
|
90
|
-
- Create a new session with the given tmux session id.
|
|
91
|
-
|
|
92
|
-
Examples:
|
|
93
|
-
- Start session: new-session ["-d", "-s", "<tmux-session-id>"]
|
|
94
|
-
- Detect window number to send keys: list-windows ["-t", "<tmux-session-id>"]
|
|
95
|
-
- Get output of window before sending keys: capture-pane ["-p", "-t", "<tmux-session-id>:<window>"]
|
|
96
|
-
- Send key to session: send-keys ["-t", "<tmux-session-id>:<window>", "echo hello", "Enter"]
|
|
97
|
-
- Delete line: send-keys ["-t", "<tmux-session-id>:<window>", "C-a", "C-k"]
|
|
98
|
-
|
|
99
84
|
# Project Rules and Skills
|
|
100
85
|
|
|
101
86
|
Discover and apply project-specific rules and reusable skills.
|
|
@@ -119,7 +104,6 @@ ${skillDescriptions}
|
|
|
119
104
|
- Current working directory: ${workingDir}
|
|
120
105
|
- Today's date: ${today}
|
|
121
106
|
- Session id: ${sessionId}
|
|
122
|
-
- Tmux session id: ${tmuxSessionId}
|
|
123
107
|
- Memory file path: ${projectMetadataDir}/memory/${sessionId}--<kebab-case-title>.md
|
|
124
108
|
|
|
125
109
|
Available subagents:
|
package/src/tool.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type ToolUseApproverConfig = {
|
|
|
39
39
|
defaultAction: "deny" | "ask";
|
|
40
40
|
/** Additional absolute paths to allow for auto-approval (outside working directory) */
|
|
41
41
|
allowedPaths?: string[];
|
|
42
|
+
/** Allow access to git-unmanaged files (default: false) */
|
|
43
|
+
allowGitUnmanagedFiles?: boolean;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Mask the input before auto-approval checks and recording.
|
|
@@ -18,19 +18,26 @@ const BUILTIN_ALLOWED_PATHS = [
|
|
|
18
18
|
/**
|
|
19
19
|
* @param {unknown} input
|
|
20
20
|
* @param {string[]} [allowedPaths=[]] - Additional allowed paths (outside working directory)
|
|
21
|
+
* @param {boolean} [allowGitUnmanagedFiles=false] - Allow access to git-unmanaged files
|
|
21
22
|
* @returns {boolean}
|
|
22
23
|
*/
|
|
23
|
-
export function isSafeToolInput(
|
|
24
|
+
export function isSafeToolInput(
|
|
25
|
+
input,
|
|
26
|
+
allowedPaths = [],
|
|
27
|
+
allowGitUnmanagedFiles = false,
|
|
28
|
+
) {
|
|
24
29
|
if (["number", "boolean", "undefined"].includes(typeof input)) {
|
|
25
30
|
return true;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
if (typeof input === "string") {
|
|
29
|
-
return isSafeToolInputItem(input, allowedPaths);
|
|
34
|
+
return isSafeToolInputItem(input, allowedPaths, allowGitUnmanagedFiles);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
if (Array.isArray(input)) {
|
|
33
|
-
return input.every((item) =>
|
|
38
|
+
return input.every((item) =>
|
|
39
|
+
isSafeToolInput(item, allowedPaths, allowGitUnmanagedFiles),
|
|
40
|
+
);
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
if (typeof input === "object") {
|
|
@@ -38,7 +45,7 @@ export function isSafeToolInput(input, allowedPaths = []) {
|
|
|
38
45
|
return true;
|
|
39
46
|
}
|
|
40
47
|
return Object.values(input).every((value) =>
|
|
41
|
-
isSafeToolInput(value, allowedPaths),
|
|
48
|
+
isSafeToolInput(value, allowedPaths, allowGitUnmanagedFiles),
|
|
42
49
|
);
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -48,9 +55,14 @@ export function isSafeToolInput(input, allowedPaths = []) {
|
|
|
48
55
|
/**
|
|
49
56
|
* @param {string} arg
|
|
50
57
|
* @param {string[]} [allowedPaths=[]] - Additional allowed paths (outside working directory)
|
|
58
|
+
* @param {boolean} [allowGitUnmanagedFiles=false] - Allow access to git-unmanaged files
|
|
51
59
|
* @returns {boolean}
|
|
52
60
|
*/
|
|
53
|
-
export function isSafeToolInputItem(
|
|
61
|
+
export function isSafeToolInputItem(
|
|
62
|
+
arg,
|
|
63
|
+
allowedPaths = [],
|
|
64
|
+
allowGitUnmanagedFiles = false,
|
|
65
|
+
) {
|
|
54
66
|
const workingDir = process.cwd();
|
|
55
67
|
|
|
56
68
|
// Note: An argument can be a command option (e.g., '-l').
|
|
@@ -84,18 +96,20 @@ export function isSafeToolInputItem(arg, allowedPaths = []) {
|
|
|
84
96
|
return false;
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
//
|
|
88
|
-
if (
|
|
89
|
-
|
|
99
|
+
// Path must be inside the working directory or in user-configured allowed paths
|
|
100
|
+
if (
|
|
101
|
+
!isInsideWorkingDirectory(realPath, workingDir) &&
|
|
102
|
+
!isInUserAllowedPath(realPath, allowedPaths)
|
|
103
|
+
) {
|
|
104
|
+
return false;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
//
|
|
93
|
-
if (!
|
|
107
|
+
// Deny git-unmanaged files (outside git repo or git-ignored)
|
|
108
|
+
if (!allowGitUnmanagedFiles && !isGitManaged(realPath)) {
|
|
94
109
|
return false;
|
|
95
110
|
}
|
|
96
111
|
|
|
97
|
-
|
|
98
|
-
return !isGitIgnored(realPath);
|
|
112
|
+
return true;
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
/**
|
|
@@ -216,16 +230,33 @@ function isInsideProjectMetadataDir(targetPath) {
|
|
|
216
230
|
}
|
|
217
231
|
|
|
218
232
|
/**
|
|
233
|
+
* Check if the path is managed by git (inside a git repo and not ignored).
|
|
219
234
|
* @param {string} absPath
|
|
220
235
|
* @returns {boolean}
|
|
221
236
|
*/
|
|
222
|
-
function
|
|
237
|
+
function isGitManaged(absPath) {
|
|
238
|
+
const dir = findExistingDirectory(absPath);
|
|
239
|
+
|
|
240
|
+
/** @type {string} */
|
|
241
|
+
let gitRoot;
|
|
223
242
|
try {
|
|
224
|
-
execFileSync("git", ["
|
|
225
|
-
stdio: ["ignore", "
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
243
|
+
gitRoot = execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
|
|
244
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
245
|
+
encoding: "utf-8",
|
|
246
|
+
}).trim();
|
|
247
|
+
} catch {
|
|
248
|
+
// Not inside a git repository
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
execFileSync(
|
|
254
|
+
"git",
|
|
255
|
+
["-C", gitRoot, "check-ignore", "--no-index", "-q", absPath],
|
|
256
|
+
{ stdio: ["ignore", "ignore", "ignore"] },
|
|
257
|
+
);
|
|
258
|
+
// File is git-ignored: not managed
|
|
259
|
+
return false;
|
|
229
260
|
} catch (error) {
|
|
230
261
|
if (
|
|
231
262
|
error instanceof Error &&
|
|
@@ -233,11 +264,28 @@ function isGitIgnored(absPath) {
|
|
|
233
264
|
typeof error.status === "number" &&
|
|
234
265
|
error.status === 1
|
|
235
266
|
) {
|
|
236
|
-
//
|
|
237
|
-
return
|
|
267
|
+
// Not ignored: managed
|
|
268
|
+
return true;
|
|
238
269
|
}
|
|
239
|
-
// Other
|
|
240
|
-
|
|
241
|
-
|
|
270
|
+
// Other error: treat as not managed for safety
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {string} absPath
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
function findExistingDirectory(absPath) {
|
|
280
|
+
const stats = noThrowSync(() => fs.statSync(absPath));
|
|
281
|
+
if (!(stats instanceof Error)) {
|
|
282
|
+
return stats.isDirectory() ? absPath : path.dirname(absPath);
|
|
283
|
+
}
|
|
284
|
+
let dir = absPath;
|
|
285
|
+
while (!fs.existsSync(dir)) {
|
|
286
|
+
const parent = path.dirname(dir);
|
|
287
|
+
if (parent === dir) break;
|
|
288
|
+
dir = parent;
|
|
242
289
|
}
|
|
290
|
+
return dir;
|
|
243
291
|
}
|
package/src/toolUseApprover.mjs
CHANGED
|
@@ -16,6 +16,7 @@ export function createToolUseApprover({
|
|
|
16
16
|
defaultAction,
|
|
17
17
|
maskApprovalInput,
|
|
18
18
|
allowedPaths = [],
|
|
19
|
+
allowGitUnmanagedFiles = false,
|
|
19
20
|
}) {
|
|
20
21
|
const state = {
|
|
21
22
|
approvalCount: 0,
|
|
@@ -65,7 +66,9 @@ export function createToolUseApprover({
|
|
|
65
66
|
|
|
66
67
|
if (action === "allow") {
|
|
67
68
|
const maskedInput = maskApprovalInput(toolUse.toolName, toolUse.input);
|
|
68
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
isSafeToolInput(maskedInput, allowedPaths, allowGitUnmanagedFiles)
|
|
71
|
+
) {
|
|
69
72
|
state.approvalCount += 1;
|
|
70
73
|
return state.approvalCount <= max
|
|
71
74
|
? { action: "allow" }
|
|
@@ -18,7 +18,17 @@ export function createTmuxCommandTool(config) {
|
|
|
18
18
|
return {
|
|
19
19
|
def: {
|
|
20
20
|
name: "tmux_command",
|
|
21
|
-
description:
|
|
21
|
+
description: [
|
|
22
|
+
"Run a tmux command.",
|
|
23
|
+
"The tmux session id is plain-agent-<session-id>.",
|
|
24
|
+
"",
|
|
25
|
+
"Examples:",
|
|
26
|
+
'- Start session: new-session ["-d", "-s", "<tmux-session-id>"]',
|
|
27
|
+
'- Detect window number to send keys: list-windows ["-t", "<tmux-session-id>"]',
|
|
28
|
+
'- Get output of window before sending keys: capture-pane ["-p", "-t", "<tmux-session-id>:<window>"]',
|
|
29
|
+
'- Send key to session: send-keys ["-t", "<tmux-session-id>:<window>", "echo hello", "Enter"]',
|
|
30
|
+
'- Delete line: send-keys ["-t", "<tmux-session-id>:<window>", "C-a", "C-k"]',
|
|
31
|
+
].join("\n"),
|
|
22
32
|
inputSchema: {
|
|
23
33
|
type: "object",
|
|
24
34
|
properties: {
|
|
@@ -40,6 +40,23 @@ export function evalJSONConfig(configItem) {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
if (Object.keys(configItem).length === 1 && "$not" in configItem) {
|
|
44
|
+
const pattern = evalJSONConfig(configItem.$not);
|
|
45
|
+
/** @param {unknown} value */
|
|
46
|
+
return (value) => {
|
|
47
|
+
if (typeof pattern === "string") {
|
|
48
|
+
return value !== pattern;
|
|
49
|
+
}
|
|
50
|
+
if (pattern instanceof RegExp) {
|
|
51
|
+
return typeof value !== "string" || !pattern.test(value);
|
|
52
|
+
}
|
|
53
|
+
if (typeof pattern === "function") {
|
|
54
|
+
return !pattern(value);
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
if (Object.keys(configItem).length === 1 && "$has" in configItem) {
|
|
44
61
|
const pattern = evalJSONConfig(configItem.$has);
|
|
45
62
|
/** @param {unknown} value */
|