[Feature Request] Add ability to whitelist tools #434

Open
opened 2026-07-29 20:16:06 +00:00 by maks1ms · 3 comments

I would like to run agents in a fairly isolated environment, so it would be nice to be able to limit the available tools to a white list at the configuration level.
I know that it is possible to configure permissions at the Forgejo token level, but in my case it will not be as flexible or convenient.

I would like to run agents in a fairly isolated environment, so it would be nice to be able to limit the available tools to a white list at the configuration level. I know that it is possible to configure permissions at the Forgejo token level, but in my case it will not be as flexible or convenient.
Owner

🤖 Finished Intake · Success · Started 8:03 AM UTC · Completed 8:03 AM UTC

<!-- castra:agent-status:local-intake-434-01 --> <!-- castra:status:terminal --> 🤖 Finished Intake · ✅ Success · Started 8:03 AM UTC · Completed 8:03 AM UTC
Owner

intake · assessment

verdict: needs_scope · agent_doable: yes · delivery_path: spec_first

scope: Add a config-level tool allowlist that restricts which MCP tools the server exposes. Proposed bounded first slice: an --enabled-tools flag + FORGEJO_ENABLED_TOOLS env var (comma-separated tool names; empty = all, preserving current behaviour) applied after RegisterTool in operation.Run via ListTools()/DeleteTools(); exact granularity/mechanism to be fixed by the spec_first proposal.

  • files: operation/operation.go, cmd/cmd.go, pkg/flag/flag.go, operation/operation_test.go
  • acceptance: With no allowlist configured, all tools are registered exactly as today (regression-safe default); With an allowlist set, only listed tools are exposed and the rest are absent from tools/list; Unknown tool names in the allowlist are logged (warn) and ignored, not fatal; go test ./... and go vet ./... pass; README/docs document the new option

canon: concerns: Config granularity (per-tool name vs domain group) and mechanism (flag/env vs new config file) must be agreed in an OpenSpec proposal before code; Default (no config) must preserve current behaviour — expose all tools — to avoid a silent breaking change

RICE inputs: impact 1 · confidence 0.7 · effort 1 (inputs only — no score; the ranking is the portfolio layer's job)

clarity: overall 0.8

decision needed: At what granularity and via what mechanism should the allowlist work — per individual tool name or per existing domain group (user/repo/issue/...), and configured via a new flag/env var (e.g. --enabled-tools) or a new config file? Allowlist-only, or also a denylist?

why: Feature request: expose a config-level allowlist so an operator can restrict which MCP tools are served, for running agents in an isolated/least-privilege environment (author notes Forgejo token-level permissions exist but are too coarse/inflexible). Grounded in code: operation.Run (operation/operation.go:150-154) unconditionally calls RegisterTool, which registers ~140 tools across 15 fixed domains (cmd/cli.go:45-61); configuration today is only CLI flags + env vars (cmd/cmd.go initFlags/initConfig, pkg/flag/flag.go) with no config file and no filtering seam. Implementation is feasible: mcp-go v0.17.0 exposes ListTools() and DeleteTools(names...), so an allowlist could be applied after RegisterTool in operation.Run, or wired into registration. None of the landing files are harness paths, so it is agent-doable. Not a duplicate (no existing tool-filtering issue/PR found) and not blocked. It is real and worth doing, but the approach is an open design fork — granularity (individual tool name vs the existing domain groups), the config mechanism (new flag/env var vs introducing a config file, which the repo does not yet have), and allow- vs deny-list semantics — where two reasonable engineers would build it differently, so it needs an agreed OpenSpec proposal before code.


Applied (suggest-only): added stage:intake, needs-scope, Kind/Feature.
Recommended next (human): scope the first slice (see above), then apply propose (spec_first).

generated by castra post-run --persona intake · the agent never mutated this issue

<!-- castra-intake:verdict --> ## intake · assessment **verdict:** `needs_scope` · agent_doable: yes · delivery_path: `spec_first` **scope:** Add a config-level tool allowlist that restricts which MCP tools the server exposes. Proposed bounded first slice: an --enabled-tools flag + FORGEJO_ENABLED_TOOLS env var (comma-separated tool names; empty = all, preserving current behaviour) applied after RegisterTool in operation.Run via ListTools()/DeleteTools(); exact granularity/mechanism to be fixed by the spec_first proposal. - files: `operation/operation.go`, `cmd/cmd.go`, `pkg/flag/flag.go`, `operation/operation_test.go` - acceptance: With no allowlist configured, all tools are registered exactly as today (regression-safe default); With an allowlist set, only listed tools are exposed and the rest are absent from tools/list; Unknown tool names in the allowlist are logged (warn) and ignored, not fatal; `go test ./...` and `go vet ./...` pass; README/docs document the new option **canon:** concerns: Config granularity (per-tool name vs domain group) and mechanism (flag/env vs new config file) must be agreed in an OpenSpec proposal before code; Default (no config) must preserve current behaviour — expose all tools — to avoid a silent breaking change **RICE inputs:** impact 1 · confidence 0.7 · effort 1 _(inputs only — no score; the ranking is the portfolio layer's job)_ **clarity:** overall 0.8 **decision needed:** At what granularity and via what mechanism should the allowlist work — per individual tool name or per existing domain group (user/repo/issue/...), and configured via a new flag/env var (e.g. --enabled-tools) or a new config file? Allowlist-only, or also a denylist? **why:** Feature request: expose a config-level allowlist so an operator can restrict which MCP tools are served, for running agents in an isolated/least-privilege environment (author notes Forgejo token-level permissions exist but are too coarse/inflexible). Grounded in code: operation.Run (operation/operation.go:150-154) unconditionally calls RegisterTool, which registers ~140 tools across 15 fixed domains (cmd/cli.go:45-61); configuration today is only CLI flags + env vars (cmd/cmd.go initFlags/initConfig, pkg/flag/flag.go) with no config file and no filtering seam. Implementation is feasible: mcp-go v0.17.0 exposes ListTools() and DeleteTools(names...), so an allowlist could be applied after RegisterTool in operation.Run, or wired into registration. None of the landing files are harness paths, so it is agent-doable. Not a duplicate (no existing tool-filtering issue/PR found) and not blocked. It is real and worth doing, but the approach is an open design fork — granularity (individual tool name vs the existing domain groups), the config mechanism (new flag/env var vs introducing a config file, which the repo does not yet have), and allow- vs deny-list semantics — where two reasonable engineers would build it differently, so it needs an agreed OpenSpec proposal before code. --- **Applied (suggest-only):** added `stage:intake`, `needs-scope`, `Kind/Feature`. **Recommended next (human):** scope the first slice (see above), then apply `propose` (spec_first). <sub>generated by `castra post-run --persona intake` · the agent never mutated this issue</sub>
Owner

Thanks — this is a real gap, and agreed on the motivation: token scopes are per-permission, not per-tool, so they can't express "this agent may read issues but never touch webhooks or branch protection".

Before anyone writes code, there are three decisions to settle. Answers here shape the whole surface, so I'd rather agree them now than review them in a PR.

1. Granularity: per tool, or per domain?

The codebase already groups every tool into a domain — registerToolsWithDomains in cmd/cli.go builds a toolDomains map over 15 groups (user, repo, issue, pull, search, version, actions, org, tracking, attachment, release, branch-protection, webhook, wiki), and the --cli listing already renders tools grouped by it.

That's a strong argument for making domain the primary unit: it's an existing first-class concept rather than a new one, and it's what an operator actually reasons about ("no webhook access"). With ~140 tools, a per-tool allow-list is long, and it silently goes stale — a new tool added upstream is absent from an operator's list, so it is off by default. Whether that default is a feature (fail-closed) or a footgun depends on your view.

My suggestion: domain-level, with per-tool as a refinement later if anyone asks. One caveat — toolDomains currently lives in the --cli layer; the server path would need it hoisted to be usable there. Small, but not zero.

2. Mechanism: flag/env, or a config file?

Today configuration is stdlib flag in cmd.initFlags plus a handful of os.Getenv reads (FORGEJO_URL, FORGEJO_ACCESS_TOKEN, GITEA_HOST). There is no config file anywhere in the project, so introducing one is a genuine architectural first, not an incremental change — it brings format choice, precedence rules, and a discovery path with it.

A comma-separated flag plus matching env var stays inside the existing pattern:

--enabled-domains issue,repo,search
FORGEJO_ENABLED_DOMAINS=issue,repo,search

That also matters for how this actually gets deployed — agents are usually launched as a subprocess or a container, where an env var is the natural knob and a config file is another mount to manage.

My suggestion: flag + env now. If a config file lands later for other reasons, this folds into it.

3. Allow-list only, or also a deny-list?

A deny-list is the more ergonomic form for the common case ("everything except webhook and branch-protection"), but two mechanisms mean precedence rules and a way to specify both at once.

My suggestion: allow-list only to start. It is the fail-closed direction, and it is the one the isolation use case actually needs. Add the deny-list later only if the ergonomics genuinely bite.

Implementation sketch

mcp-go v0.44.0 (the version in go.mod) exposes ListTools() and DeleteTools(names ...string) on MCPServer, so filtering after registration works without touching any Register*Tool function:

  • operation.Run calls RegisterTool(mcpServer) unconditionally — apply the filter immediately after.
  • Unset ⇒ everything registered, exactly as today. This must stay true; a silent behaviour change on upgrade would be worse than the missing feature.
  • Unknown domain names: log a warning and continue, rather than failing to start. An operator's typo shouldn't take the server down — though if you'd rather fail fast on an allow-list typo, say so, since it's a security-adjacent surface and there's a real argument the other way.

Likely files: operation/operation.go, cmd/cmd.go, cmd/cli.go (hoisting toolDomains), pkg/flag/flag.go, plus tests.

What I need from you

@maks1ms — for your isolation setup, is domain-level granularity enough, or do you specifically need individual tool names? That's the one answer that changes the design rather than just the wording.

Once the three are settled I'll write it up as an OpenSpec proposal before any code.

Thanks — this is a real gap, and agreed on the motivation: token scopes are per-permission, not per-tool, so they can't express "this agent may read issues but never touch webhooks or branch protection". Before anyone writes code, there are three decisions to settle. Answers here shape the whole surface, so I'd rather agree them now than review them in a PR. ### 1. Granularity: per tool, or per domain? The codebase already groups every tool into a domain — `registerToolsWithDomains` in `cmd/cli.go` builds a `toolDomains` map over 15 groups (`user`, `repo`, `issue`, `pull`, `search`, `version`, `actions`, `org`, `tracking`, `attachment`, `release`, `branch-protection`, `webhook`, `wiki`), and the `--cli` listing already renders tools grouped by it. That's a strong argument for making **domain the primary unit**: it's an existing first-class concept rather than a new one, and it's what an operator actually reasons about ("no webhook access"). With ~140 tools, a per-tool allow-list is long, and it silently goes stale — a new tool added upstream is absent from an operator's list, so it is off by default. Whether that default is a feature (fail-closed) or a footgun depends on your view. My suggestion: **domain-level, with per-tool as a refinement later** if anyone asks. One caveat — `toolDomains` currently lives in the `--cli` layer; the server path would need it hoisted to be usable there. Small, but not zero. ### 2. Mechanism: flag/env, or a config file? Today configuration is stdlib `flag` in `cmd.initFlags` plus a handful of `os.Getenv` reads (`FORGEJO_URL`, `FORGEJO_ACCESS_TOKEN`, `GITEA_HOST`). **There is no config file anywhere in the project**, so introducing one is a genuine architectural first, not an incremental change — it brings format choice, precedence rules, and a discovery path with it. A comma-separated flag plus matching env var stays inside the existing pattern: ``` --enabled-domains issue,repo,search FORGEJO_ENABLED_DOMAINS=issue,repo,search ``` That also matters for how this actually gets deployed — agents are usually launched as a subprocess or a container, where an env var is the natural knob and a config file is another mount to manage. My suggestion: **flag + env now.** If a config file lands later for other reasons, this folds into it. ### 3. Allow-list only, or also a deny-list? A deny-list is the more ergonomic form for the common case ("everything except `webhook` and `branch-protection`"), but two mechanisms mean precedence rules and a way to specify both at once. My suggestion: **allow-list only to start.** It is the fail-closed direction, and it is the one the isolation use case actually needs. Add the deny-list later only if the ergonomics genuinely bite. ### Implementation sketch `mcp-go v0.44.0` (the version in `go.mod`) exposes `ListTools()` and `DeleteTools(names ...string)` on `MCPServer`, so filtering after registration works without touching any `Register*Tool` function: - `operation.Run` calls `RegisterTool(mcpServer)` unconditionally — apply the filter immediately after. - Unset ⇒ everything registered, exactly as today. This must stay true; a silent behaviour change on upgrade would be worse than the missing feature. - Unknown domain names: log a warning and continue, rather than failing to start. An operator's typo shouldn't take the server down — though if you'd rather fail fast on an allow-list typo, say so, since it's a security-adjacent surface and there's a real argument the other way. Likely files: `operation/operation.go`, `cmd/cmd.go`, `cmd/cli.go` (hoisting `toolDomains`), `pkg/flag/flag.go`, plus tests. ### What I need from you @maks1ms — for your isolation setup, is **domain-level** granularity enough, or do you specifically need individual tool names? That's the one answer that changes the design rather than just the wording. Once the three are settled I'll write it up as an OpenSpec proposal before any code.
goern self-assigned this 2026-07-31 09:03:37 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
agentic-forges/forgejo-mcp#434
No description provided.