Skip to content

feat(code): enforce configured model allowlists - #5649

Merged
Mason Daugherty (mdrxy) merged 14 commits into
mainfrom
mdrxy/code/model-allowlist
Aug 24, 2026
Merged

feat(code): enforce configured model allowlists#5649
Mason Daugherty (mdrxy) merged 14 commits into
mainfrom
mdrxy/code/model-allowlist

Conversation

@mdrxy

@mdrxy Mason Daugherty (mdrxy) commented Aug 19, 2026

Copy link
Copy Markdown
Member

dcode can now be restricted to an approved set of models. Add a models.allowed list to config.toml (or push it via managed config) and every model the app tries to use — the launch default, --model flag, /model selections, saved defaults, and subagent model declarations — is checked against it. Entries are exact provider:model specs or provider:* wildcards that admit a whole provider's lineup. Unset means all models are allowed; an empty list means none are.


Why: organizations rolling out dcode need to guarantee agents only run on approved models (cost, compliance, data routing). Previously any model with a credential was usable, and there was no enforcement point.

How:

  • A new models.allowed config key (user or managed layer) parses into an ordered, deduplicated allowlist of exact provider:model specs and provider:* wildcards. A malformed list fails closed (deny-all) with an error naming the defect, so a typo can't silently disable the guardrail.
  • Every model-resolution path is gated: CLI flags, the model selector (blocked entries are hidden from recommendations; typing a blocked spec explains the policy and lists what's allowed), saved defaults (a blocked stored default is ignored with a log, not advertised as (default)), and subagent declarations (the error names the declaring file).
  • Bare model names are canonicalized the same way create_model resolves them before the policy check, so gpt-5.6-terra matches openai:gpt-5.6-terra — or an openai:* wildcard — instead of being spuriously rejected; a name whose provider can't be established stays unmatchable, so inference is never a bypass.
  • A provider:* wildcard admits that provider's whole lineup where an exact list would prune unlisted models. The wildcard names no model itself, so default resolution expands it to the provider's discovered models — the registry profile lineup merged with any configured list — rather than selecting it literally; a wildcard for a provider with no discovered or configured models fails closed with a "no discoverable models" message instead of a phantom credential prompt.
  • Blocked selections raise ModelNotAllowedError, whose message names the policy layer (user file vs. administrator-managed), quotes the offending spec, and lists the allowed entries — instead of the previous generic I/O-style failure. When the allowlist is active but no allowed model has credentials, a distinct NoAllowedModelCredentialsError keeps /auth from accepting a key and appearing to do nothing.

Edge cases covered: remote no-auth providers (e.g., a LAN Ollama endpoint) remain valid allowlist fallback candidates; bare Bedrock IDs must be written bedrock:<id> in the list since they otherwise split at the version colon and could never match; dcode tools list skips enforcement because it only compiles graphs it never invokes.

@github-actions github-actions Bot added dcode Related to `deepagents-code` feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: L 500-999 LOC labels Aug 19, 2026
Base automatically changed from mdrxy/code/managed-config to main August 19, 2026 22:57
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
@mdrxy
Mason Daugherty (mdrxy) marked this pull request as ready for review August 21, 2026 19:21

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/config.py Outdated
The allowlist fallback in `_get_default_model_spec` rejected candidates
whose provider auth was UNKNOWN, but that state covers remote no-auth
providers (e.g., a LAN/hosted Ollama endpoint) that may legitimately
require no credentials. Only a definitively MISSING credential now
disqualifies a candidate, matching `create_model()`'s deliberate
permission of UNKNOWN.
@github-actions github-actions Bot added size: XL 1000+ LOC and removed size: L 500-999 LOC labels Aug 23, 2026
…estly

A malformed user `[models].allowed` resolved to `None`, which means
unrestricted: a typo silently switched off the guardrail. A declared but
unparseable list is now deny-all at either layer, and the error names the
defect. The managed layer already refused to start.

Policy refusals were also reported as filesystem errors. The `[models]`
writers now raise `ModelNotAllowedError` instead of returning the same
`False` they use for I/O failure, so `dcode --default-model`, `/model
--default`, and Ctrl+S in the selector stop telling the user to check
permissions that are already correct.

Other fixes:

- Check the primary `model` string in `create_cli_agent`. The SDK resolves
  strings through `init_chat_model`, bypassing `create_model`, so this was
  the one model parameter of its shape left unchecked.
- Persist the resolved spec, not `display`, in the `/model` switch. A bare
  Bedrock ID passed construction but failed the save gate.
- Reject bare Bedrock IDs in the allowlist. They split at the version colon
  and could never match, making the list a silent deny-all.
- Only blame `models.allowed` for an empty selector list when the filter is
  a real spec. An empty or mistyped filter is not the administrator's fault.
- Surface `NoAllowedModelCredentialsError` so `/auth` cannot accept a valid
  key and then appear to do nothing.
- Neutralize a policy-blocked Auto classifier instead of warning and then
  aborting construction with it.
- Skip enforcement for tool enumeration, which compiles a graph it never
  invokes; a blocked subagent model crashed `dcode tools list`.
- Drop the `<default>` placeholder from user-facing errors, list the allowed
  models in them, and name the declaring file when a subagent is rejected.
- Guard the `allowed_models`/`allowed_models_source` pair in `__post_init__`
  and add `policy_error` as the single place that builds the error.
- Correct the `providers.py` unreachability comment, the stale `Raises:` and
  `Returns:` contracts on every gated writer, and the README claim that only
  two writers refuse.

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/tui/widgets/model_selector.py
`create_model` checks the resolved `provider:model`, but the selector's Enter
preflight and the Auto-classifier advisory checked the raw text. With
`allowed = ["openai:gpt-5.6-terra"]`, typing the supported bare name
`gpt-5.6-terra` was rejected even though construction infers `openai` and
would allow it.

`ModelConfig.canonical_model_spec` mirrors every branch of `create_model`'s
resolution (registered custom provider, Bedrock, leading colon, bare name), and
`policy_error(..., canonicalize=True)` opts the three preflight callers into it.
The flag is opt-in so provider inference stays off hot paths such as the
recent-models cache, where the caller already holds a canonical spec. A name
whose provider cannot be established stays unmatchable, so inference is not a
bypass, and the message still quotes what the user typed.

Also fixes the CI failure in the enumeration test added by the previous commit.
It passed only where `OPENAI_API_KEY` happened to be set: the subagent's own
model is still resolved by the SDK during graph assembly, which needs a
credential for the provider it names. The test now sets a placeholder key so it
covers the policy question rather than the environment's credentials.
An exact allowlist forces administrators to re-list every model a trusted
provider serves. A 'provider:*' entry now admits that provider's whole
lineup, so a policy can pin one provider to specific models while leaving
another open.

The parser validates the prefix as a canonical provider name and rejects
'*' anywhere else (bare '*', mid-model '*'), so a wildcard cannot match
everything or silently match nothing. is_model_allowed admits a spec when
its provider has a wildcard entry, and default resolution expands a
wildcard to the provider's configured models -- the wildcard names no model
itself, so it is never selected as a default, and a wildcard for a provider
with no configured models fails closed with a 'no discoverable models'
message rather than a phantom credential prompt.

Co-authored-by: Kimi K3 <kimi-k3@moonshot.ai>
…wlist

# Conflicts:
#	libs/code/THREAT_MODEL.md

@open-swe open-swe Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open SWE Review found 1 potential issue.

Open in WebView Open SWE trace

Comment thread libs/code/deepagents_code/config.py Outdated
@mdrxy

Mason Daugherty (mdrxy) commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

This should be merged after rebasing on #5755

A wildcard for a built-in provider only expanded the config's explicit
[models.providers.<name>].models list, which built-ins normally leave
empty -- their lineups are discovered from installed provider profile
modules. So 'allowed = ["openai:*"]' with a valid OPENAI_API_KEY but no
configured models produced no default candidates and failed startup with
'No discoverable models', while the /model selector worked fine on the
same policy.

The get_available_models body is now _discover_available_models with the
allowlist filter factored out, so wildcard expansion can read the
unfiltered discovered lineup without recursing into its own filter. A
wildcard for a provider with no discovered or configured models still
fails closed.
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
@mdrxy
Mason Daugherty (mdrxy) merged commit dd8b8ad into main Aug 24, 2026
57 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/model-allowlist branch August 24, 2026 16:49
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Aug 24, 2026
> [!CAUTION]
> Merging this PR will automatically publish to **PyPI** and create a
**GitHub release**.

For the full release process, see
[`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md).

---

_Release notes preview: keep this section in sync with the package
`CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`,
not this PR description — keep them aligned anyway so the PR stays an
accurate historical record for reviewers and anyone returning later._

---


##
[0.1.61](deepagents-code==0.1.60...deepagents-code==0.1.61)
(2026-08-24)

### Features

- Added `google_anthropic_vertex` provider support for Claude on Vertex
AI ([#5760](#5760)).
- Enforced configured model allowlists
([#5649](#5649)).
- Injected goal and rubric context directly, replacing `get_goal` and
`get_rubric`
([#5041](#5041)).
- Made `/offload` server-owned
([#5261](#5261)).
- Added prompt clipboard support
([#5733](#5733)).
- Show Auto approval review progress
([#5729](#5729)).

### Bug Fixes

- Kept long thread resumes responsive
([#5772](#5772)).
- Render first streamed text immediately
([#5761](#5761)).
- Show the incognito shell command widget
([#5768](#5768)).
- Only highlight actionable tool rows
([#5769](#5769)).
- Warn and ignore `--auto-approve` and `--yolo` in headless mode
([#5750](#5750)).
- Sweep expired history archives at startup
([#5751](#5751)).
- Clarified auth environment setup
([#5767](#5767)).

_End release notes preview._

---

> [!NOTE]
> A **community contributors** list and a **Special thanks** section
(crediting the users who filed the issues this release's PRs closed) are
appended to the GitHub release notes automatically at publish time (see
[Release
Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline),
step 3).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com>
Mason Daugherty (mdrxy) added a commit that referenced this pull request Aug 28, 2026
Remove the managed-configuration section added to the coding agent
README by #5649 while preserving that PR’s implementation and
threat-model updates.

Validation: the README exactly matches its state immediately before
#5649, and `git diff --check` passes.

Made by [Open
SWE](https://openswe.vercel.app/agents/7810e663-aa58-508f-9968-44863b5ff184)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: XL 1000+ LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant