Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitkeep
Original file line number Diff line number Diff line change
@@ -0,0 +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
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Language-specific package documentation:
- `opencode` - OpenCode CLI
- `qwen` - Qwen Code CLI (Alibaba's AI coding agent)
- `gemini` - Gemini CLI (Google's AI coding agent)
- `agent` - @link-assistant/agent (unrestricted OpenCode fork)
- `agent` - @link-assistant/agent (OpenCode fork with native permission modes)
- **Multiple Isolation Modes**:
- No isolation (direct execution)
- Screen sessions (detached terminal sessions)
Expand Down Expand Up @@ -79,7 +79,7 @@ bun add agent-commander
| `opencode` | OpenCode CLI | ✅ | ❌ | ✅ `OPENCODE_PERMISSION` deny rules | `grok`, `gemini`, `sonnet` |
| `qwen` | Qwen Code CLI | ✅ (stream-json) | ✅ (stream-json) | ✅ `--approval-mode plan` | `qwen3-coder`, `coder`, `gpt-4o` |
| `gemini` | Gemini CLI | ✅ (stream-json) | ❌ | ✅ `--approval-mode plan` | `flash`, `pro`, `lite` |
| `agent` | @link-assistant/agent | ✅ | ✅ | ❌ not enforceable | `nemotron-3-super-free` (default), `grok`, `sonnet`, `haiku` |
| `agent` | @link-assistant/agent | ✅ | ✅ | ✅ `--permission-mode readonly/plan` | `nemotron-3-super-free` (default), `grok`, `sonnet`, `haiku` |

### Claude-specific Features

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

- **JSON Input/Output**: Accepts JSON via stdin, outputs JSON event streams (OpenCode-compatible)
- **Unrestricted access**: No sandbox, no permissions system - full autonomous execution
- **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
- **Autonomous by default**: Runs with `--permission-mode auto` (full auto, never asks) unless a read-only/planning restriction is requested
- **13 built-in tools**: Including websearch, codesearch, batch - all enabled by default
- **MCP support**: Model Context Protocol for extending functionality with MCP servers
- **OpenCode compatibility**: 100% compatible with OpenCode's JSON event streaming format
Expand All @@ -135,8 +136,11 @@ Use `--read-only` or `--plan-only` when the selected agent should inspect and pl
- `opencode`: `OPENCODE_PERMISSION='{"edit":"deny","bash":"deny","task":"deny"}'`
- `qwen`: `--approval-mode plan`
- `gemini`: `--approval-mode plan`
- `agent`: `--permission-mode readonly` for `--read-only`, `--permission-mode plan` for `--plan-only`

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.
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`.

If a tool cannot enforce the requested restrictions, `start-agent` fails before starting the agent.

## CLI Usage

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

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

- [hive-mind](https://github.com/link-assistant/hive-mind) - Multi-agent GitHub issue solver
- [@link-assistant/agent](https://github.com/link-assistant/agent) - Unrestricted OpenCode fork for autonomous agents
- [@link-assistant/agent](https://github.com/link-assistant/agent) - OpenCode fork for autonomous agents with a native permission system
- [test-anywhere](https://github.com/link-foundation/test-anywhere) - Universal JavaScript testing
4 changes: 2 additions & 2 deletions docs/common-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Both packages support these tool names:
| `opencode` | OpenCode CLI | permission deny rules |
| `qwen` | Qwen Code CLI | `--approval-mode plan` |
| `gemini` | Gemini CLI | `--approval-mode plan` |
| `agent` | @link-assistant/agent | not enforceable |
| `agent` | @link-assistant/agent | `--permission-mode readonly` (`--plan-only` → `plan`) |

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.

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

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.

Claude and Codex builders also expose typed `permissionMode` / `permission_mode`, `sandboxMode` / `sandbox_mode`, and `approvalMode` / `approval_mode` fields for callers that build commands directly.
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`.

## Claude Options

Expand Down
5 changes: 5 additions & 0 deletions js/.changeset/agent-permission-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'agent-commander': minor
---

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.
1 change: 1 addition & 0 deletions js/bin/start-agent.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ async function main() {
model: options.model,
resume: options.resume,
readOnly: options.readOnly,
planOnly: options.planOnly,
isolation: options.isolation,
screenName: options.screenName,
containerName: options.containerName,
Expand Down
5 changes: 3 additions & 2 deletions js/src/cli-parser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function parseStartAgentArgs(args) {
verbose: parsed.verbose || false,
replayUserMessages: parsed['replay-user-messages'] || false,
readOnly: parsed['read-only'] || parsed['plan-only'] || false,
planOnly: parsed['plan-only'] || false,
resume: parsed.resume,
sessionId: parsed['session-id'],
forkSession: parsed['fork-session'] || false,
Expand Down Expand Up @@ -141,8 +142,8 @@ Options:
--model <model> Model to use (e.g., 'sonnet', 'opus', 'haiku')
--fallback-model <model> Fallback model when default is overloaded
--verbose Enable verbose mode
--read-only Enforce native read-only/planning mode
--plan-only Alias for --read-only
--read-only Enforce native read-only mode (agent: --permission-mode readonly)
--plan-only Enforce native planning mode (agent: --permission-mode plan)
--resume <sessionId> Resume a previous session by ID
--session-id <uuid> Use a specific session ID (must be valid UUID)
--fork-session Create new session ID when resuming
Expand Down
15 changes: 11 additions & 4 deletions js/src/command-builder.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { isToolSupported, getTool } from './tools/index.mjs';
* @param {string} [options.containerName] - Container name (for docker isolation)
* @param {boolean} [options.detached] - Run in detached mode
* @param {boolean} [options.readOnly] - Enforce native read-only/planning mode
* @param {boolean} [options.planOnly] - Enforce native planning mode (where the tool distinguishes it)
* @returns {string} The command string
*/
export function buildAgentCommand(options) {
Expand All @@ -37,15 +38,20 @@ export function buildAgentCommand(options) {
containerName,
detached = false,
readOnly = false,
planOnly = false,
...toolOptions
} = options;

// A planning request implies a read-only restriction for tools that do not
// distinguish the two modes.
const readOnlyRequested = readOnly || planOnly;

// Build base command using tool-specific builder if available
let baseCommand;

if (isToolSupported({ toolName: tool })) {
const toolConfig = getTool({ toolName: tool });
if (readOnly && !toolConfig.supportsReadOnly) {
if (readOnlyRequested && !toolConfig.supportsReadOnly) {
throw new Error(readOnlyUnsupportedError(tool));
}
if (toolConfig.buildCommand) {
Expand All @@ -58,7 +64,8 @@ export function buildAgentCommand(options) {
model,
json,
resume,
readOnly,
readOnly: readOnlyRequested,
planOnly,
...toolOptions,
});
} else {
Expand All @@ -71,7 +78,7 @@ export function buildAgentCommand(options) {
});
}
} else {
if (readOnly) {
if (readOnlyRequested) {
throw new Error(readOnlyUnsupportedError(tool));
}
// Unknown tool, use generic command builder
Expand Down Expand Up @@ -111,7 +118,7 @@ export function buildAgentCommand(options) {
* @returns {string} Error message
*/
function readOnlyUnsupportedError(tool) {
return `Tool "${tool}" does not support enforceable read-only mode. Choose one of: claude, codex, opencode, gemini, qwen; or run without --read-only.`;
return `Tool "${tool}" does not support enforceable read-only mode. Choose one of: claude, codex, opencode, gemini, qwen, agent; or run without --read-only.`;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion js/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* - claude: Anthropic Claude Code CLI
* - codex: OpenAI Codex CLI
* - opencode: OpenCode CLI
* - agent: @link-assistant/agent (unrestricted OpenCode fork)
* - agent: @link-assistant/agent (OpenCode fork with native permission modes)
* - qwen: Qwen Code CLI
* - gemini: Gemini CLI
*/
Expand Down Expand Up @@ -150,10 +150,11 @@
* @param {boolean} [options.json=false] - Enable JSON output mode
* @param {string} [options.resume] - Resume a previous session (tool-specific)
* @param {boolean} [options.readOnly=false] - Enforce native read-only/planning mode
* @param {boolean} [options.planOnly=false] - Enforce native planning mode (where the tool distinguishes it)
* @param {Object} [options.toolOptions] - Additional tool-specific options
* @returns {Object} Agent controller with start, stop, and utility methods
*/
export function agent(options) {

Check warning on line 157 in js/src/index.mjs

View workflow job for this annotation

GitHub Actions / JS Lint

Function 'agent' has too many lines (278). Maximum allowed is 150

Check warning on line 157 in js/src/index.mjs

View workflow job for this annotation

GitHub Actions / JS Lint

Function 'agent' has too many lines (278). Maximum allowed is 150
const {
tool,
workingDirectory,
Expand All @@ -167,6 +168,7 @@
json = false,
resume,
readOnly = false,
planOnly = false,
toolOptions = {},
} = options;

Expand Down Expand Up @@ -239,7 +241,7 @@
* @param {Function} [startOptions.onOutput] - Callback for raw output chunks
* @returns {Promise<void>} Resolves when process is started (not when it exits)
*/
const start = async (startOptions = {}) => {

Check warning on line 244 in js/src/index.mjs

View workflow job for this annotation

GitHub Actions / JS Lint

Async arrow function has a complexity of 22. Maximum allowed is 15

Check warning on line 244 in js/src/index.mjs

View workflow job for this annotation

GitHub Actions / JS Lint

Async arrow function has a complexity of 22. Maximum allowed is 15
const {
dryRun = false,
detached = false,
Expand Down Expand Up @@ -274,6 +276,7 @@
containerName,
detached,
readOnly,
planOnly,
};

// Add tool-specific options if tool is known
Expand Down Expand Up @@ -344,7 +347,7 @@
* @param {boolean} [stopOptions.dryRun] - If true, just show the command
* @returns {Promise<Object>} Result with exitCode, output, session info, usage, and metadata
*/
const stop = async (stopOptions = {}) => {

Check warning on line 350 in js/src/index.mjs

View workflow job for this annotation

GitHub Actions / JS Lint

Async arrow function has a complexity of 21. Maximum allowed is 15

Check warning on line 350 in js/src/index.mjs

View workflow job for this annotation

GitHub Actions / JS Lint

Async arrow function has a complexity of 21. Maximum allowed is 15
const { dryRun = false } = stopOptions;

// For isolation modes, send stop command
Expand Down
32 changes: 30 additions & 2 deletions js/src/tools/agent.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/**
* Agent CLI tool configuration (@link-assistant/agent)
* Based on hive-mind's agent.lib.mjs implementation
* Agent is a fork of OpenCode with unrestricted permissions for autonomous execution
* Agent is a fork of OpenCode that ships a native, enforceable permission
* system (agent v0.24.0, PR #272) exposed through `--permission-mode`
* (auto | plan | readonly | ask) and an OpenCode-compatible `--permission`
* JSON policy.
*/

import { buildCommandHead, escapeArg, normalizeExtraArgs } from './shell.mjs';
Expand Down Expand Up @@ -67,6 +70,10 @@ export function mapModelToId(options) {
* @param {string} [options.model] - Model to use
* @param {boolean} [options.compactJson] - Use compact JSON output
* @param {boolean} [options.useExistingClaudeOAuth] - Use existing Claude OAuth credentials
* @param {boolean} [options.readOnly] - Enforce hard read-only mode (`--permission-mode readonly`)
* @param {boolean} [options.planOnly] - Enforce planning mode (`--permission-mode plan`)
* @param {string} [options.permissionMode] - Explicit agent permission mode (auto | plan | readonly | ask)
* @param {string} [options.permission] - OpenCode-compatible `--permission` JSON policy
* @param {string[]} [options.extraArgs] - Extra raw CLI args appended after typed args
* @returns {string[]} Array of CLI arguments
*/
Expand All @@ -75,11 +82,28 @@ export function buildArgs(options) {
model,
compactJson = false,
useExistingClaudeOAuth = false,
readOnly = false,
planOnly = false,
permissionMode,
permission,
extraArgs = [],
} = options;

const args = [];

// Native, enforceable permission system (agent v0.24.0, PR #272).
// --plan-only maps to `plan`, --read-only maps to the harder `readonly`,
// matching agent's own distinction between the two modes.
const resolvedPermissionMode =
permissionMode || (planOnly ? 'plan' : readOnly ? 'readonly' : undefined);
if (resolvedPermissionMode) {
args.push('--permission-mode', resolvedPermissionMode);
}

if (permission) {
args.push('--permission', permission);
}

if (model) {
const mappedModel = mapModelToId({ model });
args.push('--model', mappedModel);
Expand Down Expand Up @@ -109,6 +133,10 @@ export function buildArgs(options) {
* @param {string} [options.model] - Model to use
* @param {boolean} [options.compactJson] - Use compact JSON output
* @param {boolean} [options.useExistingClaudeOAuth] - Use existing Claude OAuth
* @param {boolean} [options.readOnly] - Enforce hard read-only mode (`--permission-mode readonly`)
* @param {boolean} [options.planOnly] - Enforce planning mode (`--permission-mode plan`)
* @param {string} [options.permissionMode] - Explicit agent permission mode (auto | plan | readonly | ask)
* @param {string} [options.permission] - OpenCode-compatible `--permission` JSON policy
* @param {string} [options.executable='agent'] - Executable path/name
* @param {Object|Array} [options.extraEnv] - Environment variables for the tool
* @param {string[]} [options.extraArgs] - Extra raw CLI args appended after typed args
Expand Down Expand Up @@ -281,7 +309,7 @@ export const agentTool = {
supportsJsonInput: true, // Agent supports full JSON streaming input
supportsSystemPrompt: false, // System prompt is combined with user prompt
supportsResume: false, // Agent doesn't have explicit resume like Claude
supportsReadOnly: false, // No native enforceable read-only mode
supportsReadOnly: true, // Native --permission-mode readonly/plan (agent v0.24.0, PR #272)
defaultModel: 'nemotron-3-super-free', // hive-mind issue #1563, agent PR #243
modelMap,
mapModelToId,
Expand Down
29 changes: 27 additions & 2 deletions js/test/command-builder.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,35 @@ test('buildAgentCommand - read-only still works with screen isolation', () => {
assert.ok(command.includes('plan'));
});

test('buildAgentCommand - read-only rejects unsupported agent tool', () => {
test('buildAgentCommand - agent read-only uses readonly permission mode', () => {
const command = buildAgentCommand({
tool: 'agent',
workingDirectory: '/tmp/test',
readOnly: true,
isolation: 'none',
});

assert.ok(command.includes('--permission-mode'));
assert.ok(command.includes('readonly'));
});

test('buildAgentCommand - agent plan-only uses plan permission mode', () => {
const command = buildAgentCommand({
tool: 'agent',
workingDirectory: '/tmp/test',
planOnly: true,
isolation: 'none',
});

assert.ok(command.includes('--permission-mode'));
assert.ok(command.includes('plan'));
assert.ok(!command.includes('readonly'));
});

test('buildAgentCommand - read-only rejects unsupported tool', () => {
assert.throws(() => {
buildAgentCommand({
tool: 'agent',
tool: 'unknown-tool',
workingDirectory: '/tmp/test',
readOnly: true,
isolation: 'none',
Expand Down
Loading
Loading