feat(code): enforce configured model allowlists - #5649
Merged
Conversation
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Mason Daugherty (mdrxy)
force-pushed
the
mdrxy/code/model-allowlist
branch
from
August 21, 2026 19:07
273f659 to
be544b5
Compare
Mason Daugherty (mdrxy)
marked this pull request as ready for review
August 21, 2026 19:21
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.
…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.
`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
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dcodecan now be restricted to an approved set of models. Add amodels.allowedlist toconfig.toml(or push it via managed config) and every model the app tries to use — the launch default,--modelflag,/modelselections, saved defaults, and subagent model declarations — is checked against it. Entries are exactprovider:modelspecs orprovider:*wildcards that admit a whole provider's lineup. Unset means all models are allowed; an empty list means none are.Why: organizations rolling out
dcodeneed 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:
models.allowedconfig key (user or managed layer) parses into an ordered, deduplicated allowlist of exactprovider:modelspecs andprovider:*wildcards. A malformed list fails closed (deny-all) with an error naming the defect, so a typo can't silently disable the guardrail.(default)), and subagent declarations (the error names the declaring file).create_modelresolves them before the policy check, sogpt-5.6-terramatchesopenai:gpt-5.6-terra— or anopenai:*wildcard — instead of being spuriously rejected; a name whose provider can't be established stays unmatchable, so inference is never a bypass.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.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 distinctNoAllowedModelCredentialsErrorkeeps/authfrom 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 listskips enforcement because it only compiles graphs it never invokes.