@iinm/plain-agent 1.10.27 → 1.10.28

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 CHANGED
@@ -1,13 +1,88 @@
1
1
  # Plain Agent
2
2
 
3
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/iinm/plain-agent)
4
+ [![Socket Badge](https://badge.socket.dev/npm/package/@iinm/plain-agent/1.10.28)](https://socket.dev/npm/package/@iinm/plain-agent)
5
+
3
6
  A lightweight terminal-based coding agent focused on safety and low token cost
4
7
 
8
+ ## Table of Contents
9
+
10
+ - [Design](#design)
11
+ - [Multi-provider support](#multi-provider-support)
12
+ - [Auto-approval](#auto-approval)
13
+ - [Path Validation](#path-validation)
14
+ - [Sandbox](#sandbox)
15
+ - [Memory file](#memory-file)
16
+ - [Token Efficiency](#token-efficiency)
17
+ - [Claude Code Compatibility](#claude-code-compatibility)
18
+ - [Requirements](#requirements)
19
+ - [Quick Start](#quick-start)
20
+ - [Configuration](#configuration)
21
+ - [Available Tools](#available-tools)
22
+ - [Prompts](#prompts)
23
+ - [Subagents](#subagents)
24
+ - [Claude Code Plugin Support](#claude-code-plugin-support)
25
+ - [Voice Input](#voice-input)
26
+ - [Appendix: Creating Least-Privilege Users for Cloud Providers](#appendix-creating-least-privilege-users-for-cloud-providers)
27
+ - [Developer Notes](#developer-notes)
28
+
5
29
  ## Design
6
30
 
7
31
  ### Multi-provider support
8
32
 
9
33
  Supports Claude, OpenAI, Gemini, and any OpenAI-compatible provider. Bedrock, Vertex AI, and Azure are also supported for teams working in environments restricted to managed cloud providers.
10
34
 
35
+ Each model definition has two independent parts:
36
+
37
+ - **`platform`** — where to send the request and how to authenticate (Anthropic, Bedrock, Vertex AI, Azure, etc.)
38
+ - **`model.format`** — which API format to use (`anthropic`, `gemini`, `openai-responses`, `openai-messages`, `bedrock-converse`)
39
+
40
+ Because these are separate, the same API format works across different platforms. For example, Claude models use the `anthropic` format whether you call Anthropic directly or through Bedrock.
41
+
42
+ ```js
43
+ // Anthropic direct
44
+ {
45
+ "name": "claude-sonnet-4-6",
46
+ "variant": "thinking-high",
47
+ "platform": {
48
+ "name": "anthropic",
49
+ "variant": "default"
50
+ },
51
+ "model": {
52
+ "format": "anthropic",
53
+ "config": {
54
+ "model": "claude-sonnet-4-6",
55
+ "max_tokens": 32768,
56
+ "thinking": { "type": "adaptive" },
57
+ "output_config": { "effort": "high" }
58
+ }
59
+ }
60
+ }
61
+
62
+ // Bedrock — same format, different platform
63
+ {
64
+ "name": "claude-sonnet-4-6",
65
+ "variant": "thinking-high-bedrock-jp",
66
+ "platform": {
67
+ "name": "bedrock",
68
+ "variant": "jp"
69
+ },
70
+ "model": {
71
+ "format": "anthropic",
72
+ "config": {
73
+ "model": "jp.anthropic.claude-sonnet-4-6",
74
+ "max_tokens": 32768,
75
+ "thinking": { "type": "adaptive" },
76
+ "output_config": { "effort": "high" }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Models are identified by `name+variant` (e.g., `claude-sonnet-4-6+thinking-high`). You can define multiple variants of the same model with different settings — such as thinking budget or region — and switch between them as needed.
83
+
84
+ You can also add entries to `platforms` and `models` to use any OpenAI-compatible endpoint, such as Ollama or Fireworks. See the Quick Start section for examples.
85
+
11
86
  ### Auto-approval
12
87
 
13
88
  Configure what the agent can do automatically using a small DSL with regex matching. Below is an excerpt from the [default config](https://github.com/iinm/plain-agent/blob/main/config/config.predefined.json).
@@ -64,8 +139,8 @@ Configure what the agent can do automatically using a small DSL with regex match
64
139
  "expectedAction": "allow"
65
140
  },
66
141
  {
67
- "desc": "fd with -I should require approval",
68
- "toolUse": { "toolName": "exec_command", "input": { "command": "fd", "args": ["-I", ".env", "./"] } },
142
+ "desc": "fd with --exec should require approval",
143
+ "toolUse": { "toolName": "exec_command", "input": { "command": "fd", "args": [".env", "./", "--exec", "cat", "{}"] } },
69
144
  "expectedAction": "ask"
70
145
  },
71
146
  {
@@ -84,8 +159,23 @@ String values in tool inputs are treated as file paths and validated against the
84
159
 
85
160
  - The path must be under the working directory or a path listed in `autoApproval.allowedPaths`
86
161
  - No directory traversal (`..` is not allowed)
162
+ - Symlinks are resolved to their real path before validation — a symlink inside the working directory that points outside is rejected. Broken and circular symlinks are also rejected.
87
163
  - The file must be tracked by Git (not ignored)
88
164
 
165
+ Commands are executed without a shell — shell operators like `&&`, `|`, `;`, and redirects are not interpreted unless the agent explicitly uses `bash -c`. This makes each argument a discrete token that can be validated individually.
166
+
167
+ Compound arguments are decomposed before validation — embedded paths are extracted and checked individually:
168
+
169
+ | Pattern | Example | Extracted |
170
+ |---|---|---|
171
+ | `@<path>` | `@data.json` | `data.json` |
172
+ | `--opt=<val>` | `--prefix=/tmp/foo` | `/tmp/foo` |
173
+ | `-X<val>` | `-I/usr/include` | `/usr/include` |
174
+ | `VAR=<val>` | `OUTPUT=/etc/passwd` | `/etc/passwd` |
175
+ | `proto://…` | `file:///etc/passwd` | `/etc/passwd` |
176
+
177
+ `--opt=<val>`, `-X<val>`, and `VAR=<val>` are checked recursively, so chained patterns like `-DINSTALL_DIR=/etc` decompose fully (`-D` → `INSTALL_DIR=/etc` → `/etc`).
178
+
89
179
  **Note**: validation only applies when the agent explicitly passes file paths to tools. It cannot catch file access inside scripts the agent writes — something like `bash -c "rm -rf /"` is beyond its reach. Always use a sandbox when auto-approving script execution.
90
180
 
91
181
  ### Sandbox
@@ -126,12 +216,22 @@ A Docker-based wrapper called `plain-sandbox` is included, but the interface is
126
216
  }
127
217
  ```
128
218
 
219
+ ### Memory file
220
+
221
+ The agent maintains a memory file (`.plain-agent/memory/`) for each session to:
222
+
223
+ - Keep task state human-readable — you can open the file to see exactly where things stand.
224
+ - Resume cleanly — the agent can restart a task from the memory file with a clean context.
225
+ - Pass information between dependent tasks — subagents write their results to the memory file, which the main agent or a follow-up session reads to continue.
226
+
129
227
  ### Token Efficiency
130
228
 
131
229
  A few design choices keep token usage low:
132
230
 
133
231
  - Minimal system prompt — the [system prompt](https://github.com/iinm/plain-agent/blob/main/src/prompt.mjs) contains only what the agent needs to function.
134
232
  - Output truncation — when a command or MCP tool produces large output, it is truncated and saved to a file. The agent can then read only the relevant parts.
233
+ - Context compaction — when the context grows large, you can run `/compact` to discard old messages and reload task state from a memory file.
234
+ - MCP tool filtering — MCP servers often expose many tools. Use `enabledTools` in the server config to enable only the ones you need, which reduces the number of tool definitions sent to the model.
135
235
 
136
236
  ### Claude Code Compatibility
137
237
 
@@ -526,7 +626,7 @@ Files are loaded in the following order. Settings in later files override earlie
526
626
  ]
527
627
  },
528
628
  "sandbox": {
529
- // ⚠️ Build the image before first use: plain-sandbox --verbose echo done
629
+ // Build the image before first use: plain-sandbox --verbose echo done
530
630
  "command": "plain-sandbox",
531
631
  "args": ["--allow-write", "--mount-readonly", ".plain-agent/config.json", "--skip-build", "--keep-alive", "30"],
532
632
  // ↑ --mount-readonly: prevents the agent from overwriting its own config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.10.27",
3
+ "version": "1.10.28",
4
4
  "description": "A lightweight terminal-based coding agent focused on safety and low token cost",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,8 @@
37
37
  "coverage": "node --experimental-test-coverage --test-coverage-exclude='src/**/*.test.mjs' --test",
38
38
  "test:predefined-approval": "bash -c 'set -e; mkdir -p tmp; cd tmp; env HOME=. ../bin/plain test-approval'",
39
39
  "lint": "npx @biomejs/biome check",
40
- "fix": "npx @biomejs/biome check --fix"
40
+ "fix": "npx @biomejs/biome check --fix",
41
+ "version": "sed -i \"s|badge.socket.dev/npm/package/@iinm/plain-agent/[0-9][0-9.]*|badge.socket.dev/npm/package/@iinm/plain-agent/${npm_package_version}|g\" README.md && git add README.md"
41
42
  },
42
43
  "dependencies": {},
43
44
  "devDependencies": {
@@ -62,6 +62,78 @@ export function isSafeToolInputItem(
62
62
  arg,
63
63
  allowedPaths = [],
64
64
  allowGitUnmanagedFiles = false,
65
+ ) {
66
+ // @<path> pattern (e.g., curl -d @file.json)
67
+ if (arg.startsWith("@")) {
68
+ const pathPart = arg.slice(1);
69
+ return (
70
+ isSafeToolInputItemRaw(arg, allowedPaths, allowGitUnmanagedFiles) &&
71
+ isSafeToolInputItemRaw(pathPart, allowedPaths, allowGitUnmanagedFiles)
72
+ );
73
+ }
74
+
75
+ // --opt=val pattern (e.g., npm --prefix=foo)
76
+ const longOptMatch = arg.match(/^--[^=]+=(.+)$/);
77
+ if (longOptMatch) {
78
+ return (
79
+ isSafeToolInputItemRaw(arg, allowedPaths, allowGitUnmanagedFiles) &&
80
+ isSafeToolInputItem(longOptMatch[1], allowedPaths, allowGitUnmanagedFiles)
81
+ );
82
+ }
83
+
84
+ // -X<val> pattern (e.g., gcc -oout, gcc -I/usr/include)
85
+ const shortOptMatch = arg.match(/^-[a-zA-Z](.+)$/);
86
+ if (shortOptMatch) {
87
+ return (
88
+ isSafeToolInputItemRaw(arg, allowedPaths, allowGitUnmanagedFiles) &&
89
+ isSafeToolInputItem(
90
+ shortOptMatch[1],
91
+ allowedPaths,
92
+ allowGitUnmanagedFiles,
93
+ )
94
+ );
95
+ }
96
+
97
+ // VAR=val pattern (e.g., make OUTPUT=/path, env KEY=val)
98
+ // Must not start with - or @ (already handled above)
99
+ const keyValueMatch = arg.match(/^[^-@][^=]*=(.+)$/);
100
+ if (keyValueMatch) {
101
+ return (
102
+ isSafeToolInputItemRaw(arg, allowedPaths, allowGitUnmanagedFiles) &&
103
+ isSafeToolInputItem(
104
+ keyValueMatch[1],
105
+ allowedPaths,
106
+ allowGitUnmanagedFiles,
107
+ )
108
+ );
109
+ }
110
+
111
+ // proto://path pattern (e.g., file:///etc/passwd)
112
+ const protoMatch = arg.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.+)$/);
113
+ if (protoMatch) {
114
+ return (
115
+ isSafeToolInputItemRaw(arg, allowedPaths, allowGitUnmanagedFiles) &&
116
+ isSafeToolInputItemRaw(
117
+ `/${protoMatch[1]}`,
118
+ allowedPaths,
119
+ allowGitUnmanagedFiles,
120
+ )
121
+ );
122
+ }
123
+
124
+ return isSafeToolInputItemRaw(arg, allowedPaths, allowGitUnmanagedFiles);
125
+ }
126
+
127
+ /**
128
+ * @param {string} arg
129
+ * @param {string[]} [allowedPaths=[]] - Additional allowed paths (outside working directory)
130
+ * @param {boolean} [allowGitUnmanagedFiles=false] - Allow access to git-unmanaged files
131
+ * @returns {boolean}
132
+ */
133
+ function isSafeToolInputItemRaw(
134
+ arg,
135
+ allowedPaths = [],
136
+ allowGitUnmanagedFiles = false,
65
137
  ) {
66
138
  const workingDir = process.cwd();
67
139