Problem
What forge offers for tools, models, and channels during forge init / forge run / forge try is pre-built / hardcoded, with no configurable settings surface — and no way for an enterprise to centrally set defaults or constrain the offered set on a fleet:
- Channels are a hardcoded factory switch —
createPlugin() (forge-cli/cmd/channel.go:236) + defaultRegistry() (:250) enumerate slack/telegram/msteams; adding/removing an offered channel is a code change.
- Builtin tools offered in the quickstart are a hardcoded list —
quickstartPreset() (forge-cli/cmd/try.go:248): BuiltinTools: []string{"http_request","datetime_now","math_calculate"}.
- Model options come from CLI flags with baked-in provider names —
--provider openai|anthropic|gemini|ollama (try.go:56), and the wizard's provider choices in forge init.
- The model gateway endpoint (
base_url / auth_scheme / auth_header_name, ModelRef, forge-core/types/config.go:281) is set per-agent in forge.yaml, not injectable from a machine/org setting.
There is no user-level or org-level (MDM) settings layer that configures which channels/models/tools are enabled or defaulted, or that injects the gateway endpoint centrally.
What exists today (and what's missing)
Forge already has a three-layer policy system — forge-core/security/platform_policy_layers.go: LayerSystem (/etc/forge/policy.yaml), LayerUser (~/.forge/policy.yaml), LayerWorkspace — with FirstLayerDenyingTool / FirstLayerForbiddingModel / FirstLayerDenyingChannel / egress-max and a one-way tighten ratchet.
But that is a deny/restrict construct (a layer can forbid a tool/model/channel at runtime). It is not an enablement/config/defaults construct: it can't say "these are the channels offered in forge init," "this is the default model," "this is the gateway URL," or "this org's fleet may only use these models." The offered/default surface is hardcoded (above).
Goal
Add a layered settings surface for forge, mirroring Claude Code settings + managed settings: a user layer and a managed (enterprise, MDM-droppable) layer, so enabled channels, enabled model options / providers / gateway endpoint, enabled builtin tools, and defaults all become configurable — and init / run / try read the offered/default/enabled set from settings instead of hardcoding it.
Precedence (mirror Claude Code, highest → lowest)
- Managed settings — org, non-overridable (with a security-stricter-wins exception for restrictive keys). OS system dirs + MDM + drop-in dir:
- macOS:
/Library/Application Support/forge/managed-settings.json (+ MDM config profile domain), Linux/WSL: /etc/forge/managed-settings.json, Windows: C:\Program Files\forge\managed-settings.json; plus managed-settings.d/*.json merged alphabetically.
- Command line —
forge --settings <file> / existing flags for one session.
- Project local —
.forge/settings.local.json.
- Shared project —
.forge/settings.json.
- User —
~/.forge/settings.json.
Configurable surface (initial set)
| Key |
Controls |
Replaces today's hardcode |
channels.enabled (+ available) |
which channel adapters forge init/run/channel add offer/enable |
createPlugin/defaultRegistry switch |
models.availableModels (allowlist) + models.default + models.providers.enabled |
which providers/models are offered + the default; managed layer can lock the allowlist (Claude Code's availableModels semantic) |
--provider flag enum + wizard choices |
models.gateway (base_url, auth_scheme, auth_header_name) |
inject the model gateway endpoint centrally (per-user or org) |
per-agent forge.yaml ModelRef only |
tools.builtins.enabled |
which builtin tools are offered/defaulted |
quickstartPreset list |
env, defaults for egress posture, etc. |
fleet defaults |
— |
Settings vs policy — SEPARATE surfaces (decided)
Settings and the existing policy.yaml layers stay separate constructs — they are different surfaces with different owners and delivery paths:
|
Settings (this issue) |
Policy (platform_policy_layers.go, exists) |
| Role |
Positive: config / enablement / defaults / gateway injection — what's offered |
Negative: deny / restrict / tighten — what's forbidden |
| Surface |
Developer surface management (client-side) |
Server surface equivalent — the runtime deny enforcement |
| Delivery |
user ~/.forge/settings.json → managed managed-settings.json (MDM / system dir) |
injected server-side via the control plane (platform policy, already wired), AND can be delivered client-side alongside managed-settings.json in the same system dir / drop-in |
| Semantics |
list keys merge; managed can additionally lock an allowlist (availableModels-style) |
one-way tighten ratchet; stricter layer wins |
So: do NOT fold policy deny-keys into settings. Settings manages the developer/client surface (offered/enabled/defaulted channels, models, tools, gateway); policy remains the server/control-plane deny layer. They're complementary — an org can ship managed-settings.json (developer-surface constraints + defaults) next to a policy.yaml (deny) on the fleet, while the control plane injects the same policy server-side. The two loaders stay independent; a managed lock in settings (positive allowlist) is the counterpart to a policy deny (negative), and both can be present.
List keys merge across settings layers (like Claude Code's permissions.allow); allowlist/lock keys and the managed layer follow "managed wins / stricter wins." Policy keeps its own existing merge/tighten semantics — unchanged by this issue.
Deliverables
- Settings schema (
$schema-published, like Claude Code) + a loader with the 5-layer precedence, list-merge, and managed-lock semantics; OS system-dir + drop-in + MDM discovery.
- Wire
forge init, forge run, forge try, and forge channel to read the offered/enabled/default sets (channels, models/providers, builtins, gateway endpoint) from resolved settings instead of hardcoded lists/flags.
- Managed non-overridability + security-stricter-wins exceptions; a
forge settings inspect/verify command (mirror Claude Code's precedence-debug affordance).
- Docs: a settings + managed-settings reference under
docs/, and how it relates to docs/security/platform-policy.md (the deny layers).
Acceptance
Related
Problem
What forge offers for tools, models, and channels during
forge init/forge run/forge tryis pre-built / hardcoded, with no configurable settings surface — and no way for an enterprise to centrally set defaults or constrain the offered set on a fleet:createPlugin()(forge-cli/cmd/channel.go:236) +defaultRegistry()(:250) enumerateslack/telegram/msteams; adding/removing an offered channel is a code change.quickstartPreset()(forge-cli/cmd/try.go:248):BuiltinTools: []string{"http_request","datetime_now","math_calculate"}.--provider openai|anthropic|gemini|ollama(try.go:56), and the wizard's provider choices inforge init.base_url/auth_scheme/auth_header_name,ModelRef,forge-core/types/config.go:281) is set per-agent inforge.yaml, not injectable from a machine/org setting.There is no user-level or org-level (MDM) settings layer that configures which channels/models/tools are enabled or defaulted, or that injects the gateway endpoint centrally.
What exists today (and what's missing)
Forge already has a three-layer policy system —
forge-core/security/platform_policy_layers.go:LayerSystem(/etc/forge/policy.yaml),LayerUser(~/.forge/policy.yaml),LayerWorkspace— withFirstLayerDenyingTool/FirstLayerForbiddingModel/FirstLayerDenyingChannel/ egress-max and a one-way tighten ratchet.But that is a deny/restrict construct (a layer can forbid a tool/model/channel at runtime). It is not an enablement/config/defaults construct: it can't say "these are the channels offered in
forge init," "this is the default model," "this is the gateway URL," or "this org's fleet may only use these models." The offered/default surface is hardcoded (above).Goal
Add a layered settings surface for forge, mirroring Claude Code settings + managed settings: a user layer and a managed (enterprise, MDM-droppable) layer, so enabled channels, enabled model options / providers / gateway endpoint, enabled builtin tools, and defaults all become configurable — and
init/run/tryread the offered/default/enabled set from settings instead of hardcoding it.Precedence (mirror Claude Code, highest → lowest)
/Library/Application Support/forge/managed-settings.json(+ MDM config profile domain), Linux/WSL:/etc/forge/managed-settings.json, Windows:C:\Program Files\forge\managed-settings.json; plusmanaged-settings.d/*.jsonmerged alphabetically.forge --settings <file>/ existing flags for one session..forge/settings.local.json..forge/settings.json.~/.forge/settings.json.Configurable surface (initial set)
channels.enabled(+ available)forge init/run/channel addoffer/enablecreatePlugin/defaultRegistryswitchmodels.availableModels(allowlist) +models.default+models.providers.enabledavailableModelssemantic)--providerflag enum + wizard choicesmodels.gateway(base_url,auth_scheme,auth_header_name)forge.yamlModelRefonlytools.builtins.enabledquickstartPresetlistenv, defaults for egress posture, etc.Settings vs policy — SEPARATE surfaces (decided)
Settings and the existing
policy.yamllayers stay separate constructs — they are different surfaces with different owners and delivery paths:platform_policy_layers.go, exists)~/.forge/settings.json→ managedmanaged-settings.json(MDM / system dir)managed-settings.jsonin the same system dir / drop-inavailableModels-style)So: do NOT fold policy deny-keys into settings. Settings manages the developer/client surface (offered/enabled/defaulted channels, models, tools, gateway); policy remains the server/control-plane deny layer. They're complementary — an org can ship
managed-settings.json(developer-surface constraints + defaults) next to apolicy.yaml(deny) on the fleet, while the control plane injects the same policy server-side. The two loaders stay independent; a managed lock in settings (positive allowlist) is the counterpart to a policy deny (negative), and both can be present.List keys merge across settings layers (like Claude Code's
permissions.allow); allowlist/lock keys and the managed layer follow "managed wins / stricter wins." Policy keeps its own existing merge/tighten semantics — unchanged by this issue.Deliverables
$schema-published, like Claude Code) + a loader with the 5-layer precedence, list-merge, and managed-lock semantics; OS system-dir + drop-in + MDM discovery.forge init,forge run,forge try, andforge channelto read the offered/enabled/default sets (channels, models/providers, builtins, gateway endpoint) from resolved settings instead of hardcoded lists/flags.forge settingsinspect/verify command (mirror Claude Code's precedence-debug affordance).docs/, and how it relates todocs/security/platform-policy.md(the deny layers).Acceptance
~/.forge/settings.jsoncan set the default model, enabled channels, enabled builtins, and the model gatewaybase_url/auth_scheme;init/run/tryhonor it.managed-settings.json(system dir / MDM) overrides user/project values and can lock the model allowlist so--model/forge.yamlcan't select outside it.channels.enabled(managed) makesforge run --with <that>refuse it, andforge channel addnot offer it.init/tryis no longer hardcoded — it derives from resolved settings.forge settingscommand shows the effective value + which layer set it.Related
forge-core/security/platform_policy_layers.go,docs/security/platform-policy.md.ModelRef(forge-core/types/config.go:281),AuthScheme(forge-core/llm/client.go:13), issues LLM client: apikey-header auth scheme for Kong AI Gateway (key-auth) #302/feat(llm): apikey_header auth scheme for Kong AI Gateway key-auth #303.