Skip to content

Add a native, enforceable permission system (auto/plan/readonly/ask, JSON-driven, JS + Rust)#272

Merged
konard merged 5 commits into
mainfrom
issue-271-3f282f6e5862
Jun 17, 2026
Merged

Add a native, enforceable permission system (auto/plan/readonly/ask, JSON-driven, JS + Rust)#272
konard merged 5 commits into
mainfrom
issue-271-3f282f6e5862

Conversation

@konard

@konard konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a native, enforceable permission system to @link-assistant/agent, ported from OpenCode and fully controllable over JSON with no TUI. This closes issue #271 in two layers:

  1. Hard layer (first increment, already on the branch): --read-only / --disable-tools remove mutating tools from the model entirely.
  2. Fine-grained layer (this work): an OpenCode-style permission system with auto / plan / readonly / ask modes and a per-command JSON approval protocol.

The default mode is auto (full autonomy, never asks), so existing behavior is unchanged and the full-auto path has zero added overhead.

Fixes #271

Permission modes

--permission-mode <mode> (env LINK_ASSISTANT_AGENT_PERMISSION_MODE):

Mode edit/write/patch bash webfetch Asks?
auto (default) allow allow allow never
plan deny read-only cmds allow, else ask allow yes
readonly deny read-only cmds allow, else deny allow never
ask ask ask (every command) ask yes

--permission '<json>' (env LINK_ASSISTANT_AGENT_PERMISSION) takes an OpenCode-compatible {edit, bash, webfetch} policy (where bash is a string or a {glob: action} map) and is merged on top of the mode (override wins; bash maps merge key-by-key).

JSON protocol (no TUI)

When a tool needs approval the agent emits a permission_request event on stdout; the consumer replies with a permission_response frame on stdin:

{ "type": "permission_response", "permissionID": "per_xyz", "response": "once" }

response is once (allow this call), always (allow + auto-approve later matching requests this session), or reject (deny → RejectedError). Accepted in both text and --input-format stream-json modes. auto/readonly never ask (any input mode); plan/ask emit requests mid-turn and require a streaming input mode.

Full protocol, modes, env vars, and worked examples: docs/permissions.md.

Implementation (both JavaScript and Rust)

JavaScript — full runtime enforcement in the agent loop:

  • js/src/permission/index.tsPermission namespace ported from OpenCode (ask/respond/RejectedError, permission.updated/permission.replied bus events, once/always/reject, per-tool glob rules with "last matching rule wins", read-only bash allowlist, mode/override resolution).
  • Tool enforcement in tool/{bash,edit,write,patch,webfetch}.ts. bash parses the command with tree-sitter and evaluates each command node independently; the parse is skipped entirely in auto mode (Permission.bashEnforced() short-circuit → zero overhead).
  • JSON I/O in cli/{event-handler,input-queue,continuous-mode,run-options}.js and config in config/config.ts.

Rustrust/src/permission.rs mirrors the JS policy core: Mode/Action/Policy, mode policy, --permission parse+merge, read-only bash allowlist, structured wildcard matcher, evaluate_bash, and permission_request/permission_response serde schemas. --permission-mode/--permission clap flags (env-backed) with early validation that fails clearly on an invalid mode or override JSON. The Rust binary has no model agent loop yet, so enforcement there is policy-resolution + JSON-schema parity (documented honestly in the case study).

Case study & research

Per the issue directive, docs/case-studies/issue-271/ contains:

  • README.md — deep analysis, per-requirement (R1–R10) solution table, root cause, architecture, alternatives, prior-art reuse, verification.
  • research-permissions-landscape.md — sourced survey of OpenCode, Claude Code, MCP elicitation, Codex, Gemini/Qwen, and Vercel AI SDK.
  • research-data.json — machine-readable facts.

Tests

  • js/tests/permission.test.ts — 22 tests (mode policy, override parse/merge, policy() resolution, evaluateBash, bashEnforced, check() allow/deny, full ask→respond lifecycle for once/reject/always).
  • rustcargo test --lib permission: 24 tests (mode policy, override parse/merge, bash evaluation, wildcard matching, JSON (de)serialization).
  • js/tests/read-only.test.ts — unchanged, still green (hard tool-removal layer).
  • Full JS suite green; cargo build + cargo fmt --check clean.

Reproduce

# Default — full auto, unchanged behavior:
agent -p "refactor this file"

# Hard read-only that still allows read-only shell, never asks:
agent --permission-mode readonly -p "summarize the repo layout"

# Approve every mutating tool over JSON:
agent --permission-mode ask --input-format stream-json

# OpenCode-compatible fine-grained override:
agent --permission '{"edit":"ask","bash":{"git push*":"ask","*":"allow"}}'

Docs / release

  • docs/permissions.md (new), docs/case-studies/issue-271/* (new).
  • README.md / TOOLS.md document the permission system and link the docs.
  • Changesets: js/.changeset/permission-system.md (minor) + rust/changelog.d/...permission_system.md (minor).

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #271
@konard konard self-assigned this Jun 17, 2026
Add --read-only and --disable-tools flags (plus LINK_ASSISTANT_AGENT_READ_ONLY
and LINK_ASSISTANT_AGENT_DISABLE_TOOLS env vars) that disable filesystem-mutating
and shell tools (bash, edit, write, multiedit, patch).

Enforcement is applied at tool resolution (so the model never sees denied tools)
and inside the batch tool (so denied tools cannot be invoked indirectly). A
read-only note is added to the environment system prompt. This makes
agent-commander's uniform --read-only flag enforceable for the agent tool.
@konard konard changed the title [WIP] Add a native read-only / planning permission mode (enforceable via agent-commander --read-only) Add a native read-only / planning permission mode (enforceable via agent-commander --read-only) Jun 17, 2026
@konard konard marked this pull request as ready for review June 17, 2026 13:12
@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

All CI checks pass. The work is complete.

Summary

I implemented a native, enforceable read-only / planning mode for @link-assistant/agent, resolving issue #271. PR #272 is updated, marked ready, and all CI checks are green.

What was added

  • --read-only flag (env LINK_ASSISTANT_AGENT_READ_ONLY) — disables all filesystem-mutating and shell tools: bash, edit, write, multiedit, patch. Read/search/plan tools stay enabled.
  • --disable-tools <list> flag (env LINK_ASSISTANT_AGENT_DISABLE_TOOLS) — disable an explicit comma-separated set, e.g. --disable-tools bash,write,edit.

How it's enforced (not prompt-only)

A single source of truth, ToolRegistry.deniedTools() (in tool/registry.ts), is consulted in two independent places so the restriction can't be bypassed:

  1. Tool resolution (session/prompt.ts → resolveTools) — denied tools are forced off last, winning over agent defaults and per-message overrides; the model never sees them.
  2. Batch tool (tool/batch.ts) — since batch resolves tools directly from the registry (bypassing the model's tool list), it independently rejects any denied tool with a clear error.

A <read_only_mode> note is also injected into the environment system prompt so the model describes intended changes instead of attempting them.

Verification

  • Demonstrated the enabled tool set drops bash/edit/write/multiedit/patch under --read-only.
  • New js/tests/read-only.test.ts (14 tests) covers flag/env parsing and deniedTools() behavior.
  • Full suite: 576 pass, 0 fail; lint, format, file-size, and changeset checks all pass.

Docs / release

  • TOOLS.md — new "Read-Only / Planning Mode" section.
  • js/README.md — new "Permission Options" block.
  • Root README.md — opt-in note in the security warning.
  • Changeset added (minor bump).

PR: #272

One design note worth surfacing for review: in read-only mode the task tool remains enabled — its subagents run through the same resolveTools path, so they inherit the same read-only restrictions. If you'd prefer task itself to be denied in read-only mode, that's a one-line change to READ_ONLY_DENIED_TOOLS.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $4.063988
  • Calculated by Anthropic: $4.402151
  • Difference: $0.338164 (+8.32%)

📊 Context and tokens usage:

  • 102.9K / 1M (10%) input tokens, 28.8K / 128K (22%) output tokens

Total: (6.2K new + 90.2K cache writes + 5.5M cache reads) input tokens, 28.8K output tokens, $4.063988 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus
  • Model: Claude Opus 4.8 (claude-opus-4-8)

📎 Log file uploaded as Gist (2111KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

I think my comment #271 (comment) and the issue itself are not fully implemented.

@konard konard marked this pull request as draft June 17, 2026 13:51
@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI Work Session Started

Starting automated work session at 2026-06-17T13:51:26.379Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback.

konard added 2 commits June 17, 2026 14:16
Port OpenCode's permission system back into the JS agent, driven entirely
over JSON (no TUI). Default --permission-mode auto preserves full auto
behavior. plan/readonly/ask modes plus an OpenCode-compatible --permission
override enforce read-only / planning / per-command approval.

- js/src/permission/index.ts: Permission namespace (policy, modes, ask/respond)
- tool enforcement in bash/edit/write/patch/webfetch
- JSON I/O: permission_request event + permission_response frame (text & stream-json)
- CLI flags --permission-mode / --permission + config + env vars
- js/tests/permission.test.ts (22 tests)
- docs/permissions.md + docs/case-studies/issue-271 (analysis, research, data)

Refs #271
)

- rust/src/permission.rs: Mode/Action/Policy, mode policy, --permission
  override parse+merge, read-only bash allowlist, structured wildcard
  matcher, evaluate_bash, and permission_request/permission_response JSON
  schemas, with 24 unit tests. Mirrors js/src/permission/index.ts.
- rust/src/cli.rs: --permission-mode / --permission flags (env-backed),
  early policy validation (fail clearly on invalid mode/JSON), verbose
  output line.
- README.md / TOOLS.md: document the permission system + link docs/permissions.md.
- js/.changeset + rust/changelog.d: version-bump fragments.
@konard konard changed the title Add a native read-only / planning permission mode (enforceable via agent-commander --read-only) Add a native, enforceable permission system (auto/plan/readonly/ask, JSON-driven, JS + Rust) Jun 17, 2026
@konard konard marked this pull request as ready for review June 17, 2026 14:37
@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

Everything is complete and verified. Here's a summary of the work finished this session.

Done — PR #272 ready for review

The remaining work from the issue #271 spec is now complete. The earlier session finished the JavaScript implementation and all the docs/case-study; this session completed the Rust half and finalized the PR.

Rust permission system (the last missing piece — "fully implement in both JavaScript and Rust")

  • rust/src/permission.rs (new) mirrors js/src/permission/index.ts: Mode (auto/plan/readonly/ask), Action (allow/ask/deny), Policy, mode_policy, readonly_bash allowlist, parse_override + policy (mode + override merge, key-by-key bash merge), a structured wildcard matcher ported from util/wildcard.ts, evaluate_bash, and serde permission_request / permission_response JSON schemas. 24 unit tests pass.
  • rust/src/cli.rs: --permission-mode / --permission clap flags (env-backed via LINK_ASSISTANT_AGENT_PERMISSION_MODE / LINK_ASSISTANT_AGENT_PERMISSION), early policy validation that fails clearly on an invalid mode or override JSON (issue R6), and a verbose output line. lib.rs exposes the module.
  • Honestly documented in code + case study that the Rust binary has no model agent loop yet, so its enforcement is policy-resolution + JSON-schema parity.

Docs & release

  • README.md and TOOLS.md now document the permission system and link docs/permissions.md.
  • Consolidated into a single changeset (js/.changeset/read-only-mode.md, minor — CI requires exactly one per PR) covering both the hard --read-only layer and the new fine-grained system; added a Rust changelog fragment (rust/changelog.d/...permission_system.md, minor).

Verification

  • Rust: cargo test --lib permission → 24 pass; cargo build + cargo fmt --check clean.
  • JS: full suite 598 pass, 0 fail; permission.test.ts 22 pass.
  • CI green on the final commit (0478144): both JS CI/CD Pipeline and Rust CI/CD Pipeline succeeded.
  • Working tree clean, branch pushed, default branch already contained in the branch.

PR #272 is updated (new title/description) and marked ready for review: #272

The default auto mode is preserved end-to-end, so there is no behavior change for existing consumers; plan / readonly / ask and the OpenCode-compatible --permission override are opt-in and driven entirely over JSON with no TUI.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost estimation:

  • Public pricing estimate: $19.154278
  • Calculated by Anthropic: $20.945173
  • Difference: $1.790895 (+9.35%)

📊 Context and tokens usage:

Claude Opus 4.8: (4 sub-sessions)

  1. 110.6K / 1M (11%) input tokens, 30.8K / 128K (24%) output tokens
  2. 116.3K / 1M (12%) input tokens, 19.9K / 128K (16%) output tokens
  3. 115.9K / 1M (12%) input tokens, 29.9K / 128K (23%) output tokens
  4. 116.3K / 1M (12%) input tokens, 33.4K / 128K (26%) output tokens

Total: (89.9K new + 714.9K cache writes + 16.9M cache reads) input tokens, 191.6K output tokens, $18.146859 cost

Claude Haiku 4.5:

  • 484.0K / 200K (242%) input tokens, 28.6K / 64K (45%) output tokens

Total: (355.5K new + 128.5K cache writes + 2.0M cache reads) input tokens, 28.6K output tokens, $1.007419 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus
  • Main model: Claude Opus 4.8 (claude-opus-4-8)
  • Additional models:
    • Claude Haiku 4.5 (claude-haiku-4-5-20251001)

📎 Log file uploaded as Gist (8553KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

@konard konard merged commit 86d759b into main Jun 17, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a native read-only / planning permission mode (enforceable via agent-commander --read-only)

1 participant