Skip to content

Commit f4af4ce

Browse files
authored
Merge pull request #41 from link-assistant/issue-39-15508d68b466
Map --read-only/--plan-only for the `agent` tool to its native --permission-mode (agent v0.24.0)
2 parents f3814ff + de0cfbf commit f4af4ce

17 files changed

Lines changed: 246 additions & 38 deletions

.gitkeep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# .gitkeep file auto-generated at 2026-06-17T15:20:23.237Z for PR creation at branch issue-39-15508d68b466 for issue https://github.com/link-assistant/agent-commander/issues/39

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Language-specific package documentation:
2424
- `opencode` - OpenCode CLI
2525
- `qwen` - Qwen Code CLI (Alibaba's AI coding agent)
2626
- `gemini` - Gemini CLI (Google's AI coding agent)
27-
- `agent` - @link-assistant/agent (unrestricted OpenCode fork)
27+
- `agent` - @link-assistant/agent (OpenCode fork with native permission modes)
2828
- **Multiple Isolation Modes**:
2929
- No isolation (direct execution)
3030
- Screen sessions (detached terminal sessions)
@@ -79,7 +79,7 @@ bun add agent-commander
7979
| `opencode` | OpenCode CLI |||`OPENCODE_PERMISSION` deny rules | `grok`, `gemini`, `sonnet` |
8080
| `qwen` | Qwen Code CLI | ✅ (stream-json) | ✅ (stream-json) |`--approval-mode plan` | `qwen3-coder`, `coder`, `gpt-4o` |
8181
| `gemini` | Gemini CLI | ✅ (stream-json) ||`--approval-mode plan` | `flash`, `pro`, `lite` |
82-
| `agent` | @link-assistant/agent ||| ❌ not enforceable | `nemotron-3-super-free` (default), `grok`, `sonnet`, `haiku` |
82+
| `agent` | @link-assistant/agent ||| `--permission-mode readonly/plan` | `nemotron-3-super-free` (default), `grok`, `sonnet`, `haiku` |
8383

8484
### Claude-specific Features
8585

@@ -121,7 +121,8 @@ The [Gemini CLI](https://github.com/google-gemini/gemini-cli) supports additiona
121121
The [@link-assistant/agent](https://github.com/link-assistant/agent) supports additional features:
122122

123123
- **JSON Input/Output**: Accepts JSON via stdin, outputs JSON event streams (OpenCode-compatible)
124-
- **Unrestricted access**: No sandbox, no permissions system - full autonomous execution
124+
- **Native permission system**: Enforceable `--permission-mode` (`auto` | `plan` | `readonly` | `ask`) plus an OpenCode-compatible `--permission` JSON policy (agent v0.24.0); `--read-only`/`--plan-only` map to it directly
125+
- **Autonomous by default**: Runs with `--permission-mode auto` (full auto, never asks) unless a read-only/planning restriction is requested
125126
- **13 built-in tools**: Including websearch, codesearch, batch - all enabled by default
126127
- **MCP support**: Model Context Protocol for extending functionality with MCP servers
127128
- **OpenCode compatibility**: 100% compatible with OpenCode's JSON event streaming format
@@ -135,8 +136,11 @@ Use `--read-only` or `--plan-only` when the selected agent should inspect and pl
135136
- `opencode`: `OPENCODE_PERMISSION='{"edit":"deny","bash":"deny","task":"deny"}'`
136137
- `qwen`: `--approval-mode plan`
137138
- `gemini`: `--approval-mode plan`
139+
- `agent`: `--permission-mode readonly` for `--read-only`, `--permission-mode plan` for `--plan-only`
138140

139-
If a tool cannot enforce the requested restrictions, `start-agent` fails before starting the agent. For example, `--tool agent --read-only` is rejected because @link-assistant/agent has no native permission system.
141+
Most tools treat `--plan-only` as an alias for `--read-only`. The `agent` tool honors its own distinction: `--read-only` selects the hard `readonly` mode (deny every edit and any non read-only shell command, never asks) while `--plan-only` selects `plan` (deny edits, allow read-only shell, ask before anything else). For fine-grained OpenCode-style policies, pass `agent`'s native `--permission '<json>'` through `--tool-arg`.
142+
143+
If a tool cannot enforce the requested restrictions, `start-agent` fails before starting the agent.
140144

141145
## CLI Usage
142146

@@ -160,7 +164,7 @@ start-agent --tool claude --working-directory "/tmp/dir" --prompt "Solve the iss
160164
- `--fallback-model <name>` - Fallback model when default is overloaded (Claude only)
161165
- `--verbose` - Enable verbose mode (Claude only)
162166
- `--read-only` - Enforce native read-only/planning mode for supported tools
163-
- `--plan-only` - Alias for `--read-only`
167+
- `--plan-only` - Alias for `--read-only` for most tools; the `agent` tool maps it to its softer `--permission-mode plan`
164168
- `--resume <sessionId>` - Resume a previous session by ID
165169
- `--session-id <uuid>` - Use a specific session ID (Claude only, must be valid UUID)
166170
- `--fork-session` - Create new session ID when resuming (Claude only)
@@ -552,6 +556,7 @@ Creates an agent controller.
552556
- `options.json` (boolean, optional) - Enable JSON output mode
553557
- `options.resume` (string, optional) - Resume session ID (tool-specific)
554558
- `options.readOnly` (boolean, optional) - Enforce native read-only/planning mode
559+
- `options.planOnly` (boolean, optional) - Enforce native planning mode; the `agent` tool maps it to `--permission-mode plan` while other tools treat it like `readOnly`
555560
- `options.isolation` (string, optional) - 'none', 'screen', or 'docker' (default: 'none')
556561
- `options.screenName` (string, optional) - Screen session name (required for screen isolation)
557562
- `options.containerName` (string, optional) - Container name (required for docker isolation)
@@ -560,7 +565,8 @@ Creates an agent controller.
560565
- `extraEnv` (object or `KEY=VALUE` / `[key, value]` array, optional) - Environment variables applied to the native tool process
561566
- `extraArgs` (string array, optional) - Raw native tool arguments appended after typed agent-commander arguments
562567
- `skipDefaultSafetyFlags` (boolean, optional) - Do not add default autonomous safety bypass flags such as Claude/Codex bypass flags or Qwen/Gemini `--yolo`
563-
- `permissionMode` (string, optional) - Explicit Claude permission mode
568+
- `permissionMode` (string, optional) - Explicit permission mode for Claude or `agent` (agent: `auto` | `plan` | `readonly` | `ask`)
569+
- `permission` (string, optional) - OpenCode-compatible `--permission` JSON policy for the `agent` tool
564570
- `sandboxMode` (string, optional) - Explicit Codex sandbox mode
565571
- `approvalMode` (string, optional) - Explicit Codex approval mode
566572

@@ -752,5 +758,5 @@ This is free and unencumbered software released into the public domain. See [LIC
752758
## Related Projects
753759

754760
- [hive-mind](https://github.com/link-assistant/hive-mind) - Multi-agent GitHub issue solver
755-
- [@link-assistant/agent](https://github.com/link-assistant/agent) - Unrestricted OpenCode fork for autonomous agents
761+
- [@link-assistant/agent](https://github.com/link-assistant/agent) - OpenCode fork for autonomous agents with a native permission system
756762
- [test-anywhere](https://github.com/link-foundation/test-anywhere) - Universal JavaScript testing

docs/common-concepts.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Both packages support these tool names:
1313
| `opencode` | OpenCode CLI | permission deny rules |
1414
| `qwen` | Qwen Code CLI | `--approval-mode plan` |
1515
| `gemini` | Gemini CLI | `--approval-mode plan` |
16-
| `agent` | @link-assistant/agent | not enforceable |
16+
| `agent` | @link-assistant/agent | `--permission-mode readonly` (`--plan-only``plan`) |
1717

1818
Unsupported tools can still be executed through the generic command builder, but read-only planning mode is rejected unless the tool has an enforceable native restriction.
1919

@@ -46,7 +46,7 @@ Both packages expose raw passthrough controls for the native `claude`, `codex`,
4646

4747
Passthrough environment variables are attached to the native tool side of prompt pipelines, so `cat prompt.txt | env KEY=value codex exec ...` applies `KEY` to `codex` without altering prompt-file reads. Raw arguments are appended after typed arguments, allowing callers to override or extend native CLI behavior such as MCP config, reasoning config, permission modes, sandbox modes, approval modes, and custom config paths.
4848

49-
Claude and Codex builders also expose typed `permissionMode` / `permission_mode`, `sandboxMode` / `sandbox_mode`, and `approvalMode` / `approval_mode` fields for callers that build commands directly.
49+
Claude and Codex builders also expose typed `permissionMode` / `permission_mode`, `sandboxMode` / `sandbox_mode`, and `approvalMode` / `approval_mode` fields for callers that build commands directly. The `agent` builder exposes typed `permissionMode` / `permission_mode` (`auto` | `plan` | `readonly` | `ask`) and an OpenCode-compatible `permission` / `permission` JSON policy. `--read-only` maps to `readonly` and `--plan-only` maps to `plan` for `agent`.
5050

5151
## Claude Options
5252

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'agent-commander': minor
3+
---
4+
5+
Map `--read-only` and `--plan-only` for the `agent` tool to its native `--permission-mode` (agent v0.24.0): `--read-only``readonly` and `--plan-only``plan`. The `agent` tool now supports enforceable read-only/planning mode instead of being rejected, and exposes typed `permissionMode` and `permission` (OpenCode-compatible JSON policy) passthrough options.

js/bin/start-agent.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ async function main() {
4242
model: options.model,
4343
resume: options.resume,
4444
readOnly: options.readOnly,
45+
planOnly: options.planOnly,
4546
isolation: options.isolation,
4647
screenName: options.screenName,
4748
containerName: options.containerName,

js/src/cli-parser.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export function parseStartAgentArgs(args) {
9090
verbose: parsed.verbose || false,
9191
replayUserMessages: parsed['replay-user-messages'] || false,
9292
readOnly: parsed['read-only'] || parsed['plan-only'] || false,
93+
planOnly: parsed['plan-only'] || false,
9394
resume: parsed.resume,
9495
sessionId: parsed['session-id'],
9596
forkSession: parsed['fork-session'] || false,
@@ -141,8 +142,8 @@ Options:
141142
--model <model> Model to use (e.g., 'sonnet', 'opus', 'haiku')
142143
--fallback-model <model> Fallback model when default is overloaded
143144
--verbose Enable verbose mode
144-
--read-only Enforce native read-only/planning mode
145-
--plan-only Alias for --read-only
145+
--read-only Enforce native read-only mode (agent: --permission-mode readonly)
146+
--plan-only Enforce native planning mode (agent: --permission-mode plan)
146147
--resume <sessionId> Resume a previous session by ID
147148
--session-id <uuid> Use a specific session ID (must be valid UUID)
148149
--fork-session Create new session ID when resuming

js/src/command-builder.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { isToolSupported, getTool } from './tools/index.mjs';
2020
* @param {string} [options.containerName] - Container name (for docker isolation)
2121
* @param {boolean} [options.detached] - Run in detached mode
2222
* @param {boolean} [options.readOnly] - Enforce native read-only/planning mode
23+
* @param {boolean} [options.planOnly] - Enforce native planning mode (where the tool distinguishes it)
2324
* @returns {string} The command string
2425
*/
2526
export function buildAgentCommand(options) {
@@ -37,15 +38,20 @@ export function buildAgentCommand(options) {
3738
containerName,
3839
detached = false,
3940
readOnly = false,
41+
planOnly = false,
4042
...toolOptions
4143
} = options;
4244

45+
// A planning request implies a read-only restriction for tools that do not
46+
// distinguish the two modes.
47+
const readOnlyRequested = readOnly || planOnly;
48+
4349
// Build base command using tool-specific builder if available
4450
let baseCommand;
4551

4652
if (isToolSupported({ toolName: tool })) {
4753
const toolConfig = getTool({ toolName: tool });
48-
if (readOnly && !toolConfig.supportsReadOnly) {
54+
if (readOnlyRequested && !toolConfig.supportsReadOnly) {
4955
throw new Error(readOnlyUnsupportedError(tool));
5056
}
5157
if (toolConfig.buildCommand) {
@@ -58,7 +64,8 @@ export function buildAgentCommand(options) {
5864
model,
5965
json,
6066
resume,
61-
readOnly,
67+
readOnly: readOnlyRequested,
68+
planOnly,
6269
...toolOptions,
6370
});
6471
} else {
@@ -71,7 +78,7 @@ export function buildAgentCommand(options) {
7178
});
7279
}
7380
} else {
74-
if (readOnly) {
81+
if (readOnlyRequested) {
7582
throw new Error(readOnlyUnsupportedError(tool));
7683
}
7784
// Unknown tool, use generic command builder
@@ -111,7 +118,7 @@ export function buildAgentCommand(options) {
111118
* @returns {string} Error message
112119
*/
113120
function readOnlyUnsupportedError(tool) {
114-
return `Tool "${tool}" does not support enforceable read-only mode. Choose one of: claude, codex, opencode, gemini, qwen; or run without --read-only.`;
121+
return `Tool "${tool}" does not support enforceable read-only mode. Choose one of: claude, codex, opencode, gemini, qwen, agent; or run without --read-only.`;
115122
}
116123

117124
/**

js/src/index.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* - claude: Anthropic Claude Code CLI
77
* - codex: OpenAI Codex CLI
88
* - opencode: OpenCode CLI
9-
* - agent: @link-assistant/agent (unrestricted OpenCode fork)
9+
* - agent: @link-assistant/agent (OpenCode fork with native permission modes)
1010
* - qwen: Qwen Code CLI
1111
* - gemini: Gemini CLI
1212
*/
@@ -150,6 +150,7 @@ function parseJsonMessages(options) {
150150
* @param {boolean} [options.json=false] - Enable JSON output mode
151151
* @param {string} [options.resume] - Resume a previous session (tool-specific)
152152
* @param {boolean} [options.readOnly=false] - Enforce native read-only/planning mode
153+
* @param {boolean} [options.planOnly=false] - Enforce native planning mode (where the tool distinguishes it)
153154
* @param {Object} [options.toolOptions] - Additional tool-specific options
154155
* @returns {Object} Agent controller with start, stop, and utility methods
155156
*/
@@ -167,6 +168,7 @@ export function agent(options) {
167168
json = false,
168169
resume,
169170
readOnly = false,
171+
planOnly = false,
170172
toolOptions = {},
171173
} = options;
172174

@@ -274,6 +276,7 @@ export function agent(options) {
274276
containerName,
275277
detached,
276278
readOnly,
279+
planOnly,
277280
};
278281

279282
// Add tool-specific options if tool is known

js/src/tools/agent.mjs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
/**
22
* Agent CLI tool configuration (@link-assistant/agent)
33
* Based on hive-mind's agent.lib.mjs implementation
4-
* Agent is a fork of OpenCode with unrestricted permissions for autonomous execution
4+
* Agent is a fork of OpenCode that ships a native, enforceable permission
5+
* system (agent v0.24.0, PR #272) exposed through `--permission-mode`
6+
* (auto | plan | readonly | ask) and an OpenCode-compatible `--permission`
7+
* JSON policy.
58
*/
69

710
import { buildCommandHead, escapeArg, normalizeExtraArgs } from './shell.mjs';
@@ -67,6 +70,10 @@ export function mapModelToId(options) {
6770
* @param {string} [options.model] - Model to use
6871
* @param {boolean} [options.compactJson] - Use compact JSON output
6972
* @param {boolean} [options.useExistingClaudeOAuth] - Use existing Claude OAuth credentials
73+
* @param {boolean} [options.readOnly] - Enforce hard read-only mode (`--permission-mode readonly`)
74+
* @param {boolean} [options.planOnly] - Enforce planning mode (`--permission-mode plan`)
75+
* @param {string} [options.permissionMode] - Explicit agent permission mode (auto | plan | readonly | ask)
76+
* @param {string} [options.permission] - OpenCode-compatible `--permission` JSON policy
7077
* @param {string[]} [options.extraArgs] - Extra raw CLI args appended after typed args
7178
* @returns {string[]} Array of CLI arguments
7279
*/
@@ -75,11 +82,28 @@ export function buildArgs(options) {
7582
model,
7683
compactJson = false,
7784
useExistingClaudeOAuth = false,
85+
readOnly = false,
86+
planOnly = false,
87+
permissionMode,
88+
permission,
7889
extraArgs = [],
7990
} = options;
8091

8192
const args = [];
8293

94+
// Native, enforceable permission system (agent v0.24.0, PR #272).
95+
// --plan-only maps to `plan`, --read-only maps to the harder `readonly`,
96+
// matching agent's own distinction between the two modes.
97+
const resolvedPermissionMode =
98+
permissionMode || (planOnly ? 'plan' : readOnly ? 'readonly' : undefined);
99+
if (resolvedPermissionMode) {
100+
args.push('--permission-mode', resolvedPermissionMode);
101+
}
102+
103+
if (permission) {
104+
args.push('--permission', permission);
105+
}
106+
83107
if (model) {
84108
const mappedModel = mapModelToId({ model });
85109
args.push('--model', mappedModel);
@@ -109,6 +133,10 @@ export function buildArgs(options) {
109133
* @param {string} [options.model] - Model to use
110134
* @param {boolean} [options.compactJson] - Use compact JSON output
111135
* @param {boolean} [options.useExistingClaudeOAuth] - Use existing Claude OAuth
136+
* @param {boolean} [options.readOnly] - Enforce hard read-only mode (`--permission-mode readonly`)
137+
* @param {boolean} [options.planOnly] - Enforce planning mode (`--permission-mode plan`)
138+
* @param {string} [options.permissionMode] - Explicit agent permission mode (auto | plan | readonly | ask)
139+
* @param {string} [options.permission] - OpenCode-compatible `--permission` JSON policy
112140
* @param {string} [options.executable='agent'] - Executable path/name
113141
* @param {Object|Array} [options.extraEnv] - Environment variables for the tool
114142
* @param {string[]} [options.extraArgs] - Extra raw CLI args appended after typed args
@@ -281,7 +309,7 @@ export const agentTool = {
281309
supportsJsonInput: true, // Agent supports full JSON streaming input
282310
supportsSystemPrompt: false, // System prompt is combined with user prompt
283311
supportsResume: false, // Agent doesn't have explicit resume like Claude
284-
supportsReadOnly: false, // No native enforceable read-only mode
312+
supportsReadOnly: true, // Native --permission-mode readonly/plan (agent v0.24.0, PR #272)
285313
defaultModel: 'nemotron-3-super-free', // hive-mind issue #1563, agent PR #243
286314
modelMap,
287315
mapModelToId,

js/test/command-builder.test.mjs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,35 @@ test('buildAgentCommand - read-only still works with screen isolation', () => {
368368
assert.ok(command.includes('plan'));
369369
});
370370

371-
test('buildAgentCommand - read-only rejects unsupported agent tool', () => {
371+
test('buildAgentCommand - agent read-only uses readonly permission mode', () => {
372+
const command = buildAgentCommand({
373+
tool: 'agent',
374+
workingDirectory: '/tmp/test',
375+
readOnly: true,
376+
isolation: 'none',
377+
});
378+
379+
assert.ok(command.includes('--permission-mode'));
380+
assert.ok(command.includes('readonly'));
381+
});
382+
383+
test('buildAgentCommand - agent plan-only uses plan permission mode', () => {
384+
const command = buildAgentCommand({
385+
tool: 'agent',
386+
workingDirectory: '/tmp/test',
387+
planOnly: true,
388+
isolation: 'none',
389+
});
390+
391+
assert.ok(command.includes('--permission-mode'));
392+
assert.ok(command.includes('plan'));
393+
assert.ok(!command.includes('readonly'));
394+
});
395+
396+
test('buildAgentCommand - read-only rejects unsupported tool', () => {
372397
assert.throws(() => {
373398
buildAgentCommand({
374-
tool: 'agent',
399+
tool: 'unknown-tool',
375400
workingDirectory: '/tmp/test',
376401
readOnly: true,
377402
isolation: 'none',

0 commit comments

Comments
 (0)