Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,17 @@ modeldock --help
- **Format:** TOML for the file (human-friendly, stdlib `tomllib` in 3.11+).
- **Model:** a frozen `Settings` dataclass/pydantic model: `default_backend`,
`cache_dir`, `registry_url`, `catalog_source`, `log_level`, `progress_style`,
`auto_install`, `ollama_host`, etc.
`auto_install`, `execution_policy`, `ollama_host`, etc.
- **Cross-platform paths:** resolved via `common/platform.py` using
`platformdirs` (the de-facto standard for user/config/cache dirs across OSes).
- **Validation:** config loaded through a validator; unknown keys warn, invalid
values fall back to defaults with a logged warning (never crash on bad config).
- **Execution policy:** `execution_policy` (`unrestricted` | `warn` | `strict`)
is the one restricted-execution knob. It is applied in `core/execution.py`
(`ExecutionGuard`), which both `LifecycleOrchestrator.load` and
`ModelManager.run` consult, and which gates entry-point plugin discovery in
`RuntimeRegistry`/`CatalogProviderRegistry`. Adapters never implement it.
See SECURITY.md, "Model Execution & Native Code".

---

Expand Down
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,51 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this

## [Unreleased]

### Added

- `execution_policy` setting (`unrestricted` | `warn` | `strict`, default
`warn`) — the restricted-execution context for issue #139. Configurable via
`config.toml`, `MODELDOCK_EXECUTION_POLICY`, and `modeldock config show`.
- `ExecutionGuard` (`core/execution.py`) — the single place the policy is
applied, consulted by both `LifecycleOrchestrator.load` and
`ModelManager.run` so the two execution entry points cannot diverge.
- A one-per-session warning, before a model is executed, that the runtime runs
it as native code with the user's full privileges and that ModelDock does not
sandbox it. Delivered to stderr by the CLI via a new
`cli.console.print_warning`; the SDK stays silent unless a caller passes
`ModelManager(notify=...)`.
- `execution_policy="strict"` refuses third-party entry-point plugins
(`modeldock.runtimes`, `modeldock.model_sources`,
`modeldock.catalog_providers`) — they are not imported or instantiated at
all — and refuses backends that load model weights into ModelDock's own
process, raising the new typed `ExecutionPolicyError`.
- `BaseRuntime.executes_in_process`, declaring whether an adapter loads weights
into ModelDock's interpreter rather than driving a separate server. Shipped
HTTP-backed adapters are `False`; `gpt4all` and `vllm` declare `True`.
- SECURITY.md and `docs/project/security.md` — a "Model Execution & Native
Code" section: threat model for model artifacts and plugins, what
`execution_policy` does and does not enforce, and a concrete container recipe
for confining the runtime itself.

### Changed

- `RuntimeRegistry` and `CatalogProviderRegistry` take `allow_plugins`, and log
plugin provenance: a plugin that shadows a built-in adapter is reported at
WARNING, since nothing else revealed that the shipped adapter was replaced.
- `RuntimeRegistry` imports `entry_points` at module scope, matching
`CatalogProviderRegistry` and making the discovery call site visible.
- `RuntimeRegistry.detect_available` logs why a backend failed to probe instead
of discarding the exception silently.

### Fixed

- `tests/unit/test_security.py` resolved its source root to a directory that
does not exist, so the no-shell-execution audit walked **zero** files and
passed vacuously. It now walks `src/modeldock/{adapters,common}`, and a new
test asserts the file list is non-empty so it cannot silently degrade again.
- `test_model_names_are_treated_as_data` asserted a string literal against
itself; it now routes the hostile name through `ModelRef.parse`.

## [0.2.0] - 2026-08-28

Live GGUF catalogs, composite registry, third-party catalog plugins, and
Expand Down
113 changes: 112 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ Send an email to **opensource@openagenthq.com** with:

- Keep ModelDock and its dependencies up to date (`pip install -U modeldock`).
- Use environment variables for any secrets; never hardcode them.
- Download models only from trusted runtimes/registries.
- Download models only from trusted runtimes/registries. A model file is
executed as native code — see [Model Execution & Native
Code](#model-execution--native-code).
- Review the dynamic catalog source (`ollama.com`) and any bundled registry sources.

### For Contributors
Expand All @@ -53,6 +55,8 @@ Send an email to **opensource@openagenthq.com** with:
- Never commit secrets or `.env` files (see `.gitignore`).
- Raise typed `ModelDockError` subclasses — never swallow errors silently.
- Run `bandit -r src` as part of local checks.
- Never bypass the execution policy in `core/execution.py` from an adapter
or the CLI; see [Model Execution & Native Code](#model-execution--native-code).
- Treat output from adapters as untrusted; see [Prompt-Injection & Untrusted Model Output](#prompt-injection--untrusted-model-output) below.

## Prompt-Injection & Untrusted Model Output
Expand Down Expand Up @@ -106,6 +110,113 @@ If you embed ModelDock in a larger agent, copilot, or automation pipeline:
make network requests without an explicit human-in-the-loop approval step.
- Assume every string originating from an adapter response could be adversarial.

## Model Execution & Native Code

### Threat Model

ModelDock does not perform inference itself. `load()` and `run()` hand a model
to a runtime — Ollama, LM Studio, llama.cpp and others — which loads the
weights and executes them as **native machine code**, in a process owned by
the user who invoked ModelDock, with that user's full privileges.

A model artifact is therefore not inert data:

- **Weight files are parsed by native C/C++ loaders.** A malformed GGUF header
is a memory-safety bug in that loader, not a Python exception you can catch.
- **Some formats carry executable content by construction.** Pickle-based
`.bin` checkpoints deserialize arbitrary Python objects; repositories that
ship custom operators or conversion scripts execute code by design.
- **Model metadata drives the runtime.** Chat templates and tokenizer
configuration embedded in a model file are interpreted by the runtime, not
validated by ModelDock.
- **A runtime is a separate program.** Once ModelDock has asked it to load a
model, ModelDock has no further control over what that process reads,
writes, or connects to.
- **Installed plugins run inside ModelDock itself.** Any distribution that
advertises a `modeldock.runtimes`, `modeldock.model_sources`, or
`modeldock.catalog_providers` entry point is imported *and instantiated* in
ModelDock's own process the moment a registry is built. Installing such a
package is equivalent to granting it arbitrary code execution.

**ModelDock cannot sandbox any of this.** Python cannot confine a native
library already mapped into its address space, and it cannot restrain a server
process it does not supervise. Real containment comes from the operating
system. What ModelDock *can* do is decline to take part, and tell you when it
is about to — which is what the setting below controls.

### The `execution_policy` Setting

Set it in `config.toml`, or as `MODELDOCK_EXECUTION_POLICY`:

| Value | Warns about native execution | Third-party plugins | Backends that load models in-process |
|-------|------------------------------|---------------------|--------------------------------------|
| `unrestricted` | No | Loaded | Allowed |
| `warn` (default) | Once per session | Loaded | Allowed |
| `strict` | Once per session | **Not imported or executed** | **Refused** |

Be clear about what `strict` does and does not buy you. It is not a sandbox.
It restricts what *ModelDock's own process* will execute: no third-party
plugin code, and no backend that maps model weights into that process. A
runtime server such as Ollama or llama-server still runs your model with your
full privileges — `strict` does not change that, and cannot. Confine the
runtime with the operating system, as below.

### Rules

1. **Treat a model file as a program, not a document.** Apply the same
scrutiny to its origin that you would to an executable you downloaded.
2. **Prefer runtimes that execute out-of-process.** A separate server process
can be confined by the OS; a native library inside your own interpreter
cannot.
3. **Never install a ModelDock plugin you would not accept as arbitrary code.**
Entry-point discovery grants it exactly that. Use `execution_policy =
"strict"` when running untrusted or unaudited environments.
4. **Do not rely on ModelDock for isolation.** It reports and refuses; it does
not contain.

### Running a Model in a Restricted Context

Confine the *runtime*, not ModelDock. A reasonable baseline, using Ollama as
the example — the same shape applies to `llama-server` and LM Studio:

```bash
docker run --rm \
--user "$(id -u):$(id -g)" \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-v "$PWD/models:/models:ro" \
-p 127.0.0.1:11434:11434 \
ollama/ollama
```

What each part is for:

- `--user` — never run the runtime as root.
- `--read-only` plus a read-only model mount — the model directory is the only
filesystem the runtime needs, and it does not need to write to it.
- `--cap-drop ALL`, `--security-opt no-new-privileges` — inference needs no
capabilities.
- `-p 127.0.0.1:...` — bind the API to loopback so it is not exposed to the
network. Add `--network none` once the model is downloaded if the runtime
does not need to fetch anything at inference time.

Then point ModelDock at it (`ollama_host`, `lmstudio_host`, or
`MODELDOCK_OLLAMA_HOST`) and set `execution_policy = "strict"` so ModelDock
itself executes nothing beyond its own shipped code.

If you launch `llama-server` directly, note that ModelDock only ever *suggests*
that command in an error hint — it never runs it for you. Apply the same
confinement to the command you actually run.

### Guidance for Contributors

- A runtime adapter must not spawn a model process without documenting it.
Today every shipped adapter is an HTTP client to a server the user started.
- Declare `executes_in_process = True` on any adapter that loads weights into
ModelDock's interpreter, so `strict` can refuse it.
- The execution policy is decided once, in `core/execution.py`. Do not
re-implement or bypass it in an adapter or in the CLI.

## Contact

For security-related questions or concerns, contact:
Expand Down
16 changes: 15 additions & 1 deletion docs/architecture/runtime-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,21 @@ extending a runtime adapter, keep the following in mind:
mislead downstream tooling in agent/copilot pipelines. Treat it as
adversarial user input.

See [SECURITY.md](https://github.com/OpenAgentHQ/modeldock/blob/main/SECURITY.md) for the full prompt-injection guidance.
### Native Code & the Execution Boundary

`get_model_client()` and `run()` are a second, different boundary: past them a
runtime loads model weights and executes them as native code with the invoking
user's full privileges. ModelDock cannot sandbox that. When writing an adapter:

- **Document how the model process is started and confined.** Every shipped
adapter is an HTTP client to a server the user launched; an adapter that
spawns a process itself must say so.
- **Declare `executes_in_process = True`** if the adapter loads weights into
ModelDock's own interpreter, so `execution_policy="strict"` can refuse it.
- **Do not implement the policy yourself.** It is decided once, in
`core/execution.py`, so `load` and `run` cannot diverge.

See [SECURITY.md](https://github.com/OpenAgentHQ/modeldock/blob/main/SECURITY.md) for the full prompt-injection and model-execution guidance.
---

## Next Steps
Expand Down
109 changes: 109 additions & 0 deletions docs/project/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,115 @@ If you embed ModelDock in a larger agent, copilot, or automation pipeline:

---

## Model Execution & Native Code

### Threat Model

ModelDock does not perform inference itself. `load()` and `run()` hand a model
to a runtime — Ollama, LM Studio, llama.cpp and others — which loads the
weights and executes them as **native machine code**, in a process owned by
the user who invoked ModelDock, with that user's full privileges.

A model artifact is therefore not inert data:

- **Weight files are parsed by native C/C++ loaders.** A malformed GGUF header
is a memory-safety bug in that loader, not a Python exception you can catch.
- **Some formats carry executable content by construction.** Pickle-based
`.bin` checkpoints deserialize arbitrary Python objects; repositories that
ship custom operators or conversion scripts execute code by design.
- **Model metadata drives the runtime.** Chat templates and tokenizer
configuration embedded in a model file are interpreted by the runtime, not
validated by ModelDock.
- **A runtime is a separate program.** Once ModelDock has asked it to load a
model, ModelDock has no further control over what that process reads,
writes, or connects to.
- **Installed plugins run inside ModelDock itself.** Any distribution that
advertises a `modeldock.runtimes`, `modeldock.model_sources`, or
`modeldock.catalog_providers` entry point is imported *and instantiated* in
ModelDock's own process the moment a registry is built. Installing such a
package is equivalent to granting it arbitrary code execution.

**ModelDock cannot sandbox any of this.** Python cannot confine a native
library already mapped into its address space, and it cannot restrain a server
process it does not supervise. Real containment comes from the operating
system. What ModelDock *can* do is decline to take part, and tell you when it
is about to — which is what the setting below controls.

### The `execution_policy` Setting

Set it in `config.toml`, or as `MODELDOCK_EXECUTION_POLICY`:

| Value | Warns about native execution | Third-party plugins | Backends that load models in-process |
|-------|------------------------------|---------------------|--------------------------------------|
| `unrestricted` | No | Loaded | Allowed |
| `warn` (default) | Once per session | Loaded | Allowed |
| `strict` | Once per session | **Not imported or executed** | **Refused** |

Be clear about what `strict` does and does not buy you. It is not a sandbox.
It restricts what *ModelDock's own process* will execute: no third-party
plugin code, and no backend that maps model weights into that process. A
runtime server such as Ollama or llama-server still runs your model with your
full privileges — `strict` does not change that, and cannot. Confine the
runtime with the operating system, as below.

### Rules

1. **Treat a model file as a program, not a document.** Apply the same
scrutiny to its origin that you would to an executable you downloaded.
2. **Prefer runtimes that execute out-of-process.** A separate server process
can be confined by the OS; a native library inside your own interpreter
cannot.
3. **Never install a ModelDock plugin you would not accept as arbitrary code.**
Entry-point discovery grants it exactly that. Use `execution_policy =
"strict"` when running untrusted or unaudited environments.
4. **Do not rely on ModelDock for isolation.** It reports and refuses; it does
not contain.

### Running a Model in a Restricted Context

Confine the *runtime*, not ModelDock. A reasonable baseline, using Ollama as
the example — the same shape applies to `llama-server` and LM Studio:

```bash
docker run --rm \
--user "$(id -u):$(id -g)" \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-v "$PWD/models:/models:ro" \
-p 127.0.0.1:11434:11434 \
ollama/ollama
```

What each part is for:

- `--user` — never run the runtime as root.
- `--read-only` plus a read-only model mount — the model directory is the only
filesystem the runtime needs, and it does not need to write to it.
- `--cap-drop ALL`, `--security-opt no-new-privileges` — inference needs no
capabilities.
- `-p 127.0.0.1:...` — bind the API to loopback so it is not exposed to the
network. Add `--network none` once the model is downloaded if the runtime
does not need to fetch anything at inference time.

Then point ModelDock at it (`ollama_host`, `lmstudio_host`, or
`MODELDOCK_OLLAMA_HOST`) and set `execution_policy = "strict"` so ModelDock
itself executes nothing beyond its own shipped code.

If you launch `llama-server` directly, note that ModelDock only ever *suggests*
that command in an error hint — it never runs it for you. Apply the same
confinement to the command you actually run.

### Guidance for Contributors

- A runtime adapter must not spawn a model process without documenting it.
Today every shipped adapter is an HTTP client to a server the user started.
- Declare `executes_in_process = True` on any adapter that loads weights into
ModelDock's interpreter, so `strict` can refuse it.
- The execution policy is decided once, in `core/execution.py`. Do not
re-implement or bypass it in an adapter or in the CLI.

---

## Contact

- **Email**: opensource@openagenthq.com
30 changes: 26 additions & 4 deletions docs/user-guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ ModelDock is zero-config by default. Customize when needed.
## Config File Format

```toml
default_backend = "ollama"
auto_install = true
log_level = "INFO"
progress_style = "rich"
default_backend = "ollama"
auto_install = true
log_level = "INFO"
progress_style = "rich"
execution_policy = "warn"
```

---
Expand All @@ -35,6 +36,7 @@ Override config with `MODELDOCK_*` env vars:
| `MODELDOCK_AUTO_INSTALL` | Auto-download missing models | `false` |
| `MODELDOCK_CACHE_DIR` | Override cache location | platform default |
| `MODELDOCK_CATALOG_SOURCE` | `auto`/`ollama`/`bundled` | `auto` |
| `MODELDOCK_EXECUTION_POLICY` | `unrestricted`/`warn`/`strict` | `warn` |

---

Expand Down Expand Up @@ -101,6 +103,26 @@ Set via config file or `MODELDOCK_CATALOG_SOURCE` env var.

---

## Restricted Execution

Loading a model makes a runtime execute it as native code with your user
account's full privileges. `execution_policy` controls how much ModelDock is
willing to do on your behalf:

| Value | Behavior |
|-------|----------|
| `warn` | Warn once per session before a model is executed (default) |
| `unrestricted` | No warning; previous behavior |
| `strict` | Also refuse third-party plugins and in-process model loading |

`strict` is not a sandbox — it restricts what ModelDock's own process executes,
not what a runtime server does with your model. Set via config file or
`MODELDOCK_EXECUTION_POLICY`. See
[SECURITY.md](https://github.com/OpenAgentHQ/modeldock/blob/main/SECURITY.md)
for how to confine the runtime itself.

---

## Next Steps

- [SDK Reference](../sdk/python-api.md) — full API reference
Expand Down
Loading
Loading