Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ jobs:
timeout-minutes: 10
strategy:
matrix:
node-version: [20.19.0, 22.x]
node-version: [22.12.0, 24.x]

steps:
- name: Check out source
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

All notable changes to OpenCode Model Control are recorded here. The project follows [Semantic Versioning](https://semver.org/).

## 0.2.0 - 2026-08-31

- Added attachment-aware Omc-Router media switching through the bundled local OpenCode plugin, with capability, modality, availability, enablement, and cost-policy gates that fail closed. Media-only analysis runs as a tool-free vision worker, while only explicit user-authored text classified as a code change may retain Omc-Router for the vision-to-code-to-review workflow.
- Added automatic code-worker and independent read-only reviewer delegation with at most one prompt-governed review repair pass.
- Added safe optional Omc-Router default-agent management that preserves an existing user default and removes only receipt-owned values.
- Added managed-surface version receipts so an installed 0.1.x connection is reported as requiring an update before the new plugin and agent definitions are used.
- Added a manual, isolated runtime access check that remains separate from benchmark qualification and discloses possible provider retries, quota use, charges, and retention.
- Added full-width stacked dashboard modules, a collapsible desktop sidebar, and a mobile navigation drawer.
- Hardened specialist permissions, attachment-as-untrusted-data handling, media-turn authorization, runtime-check configuration isolation, generated-config preview accuracy, and recovery from malformed optional runtime history.
- Expanded integration, security, benchmark, support, and release documentation for the new routing and qualification boundaries.

## 0.1.2 - 2026-08-30

- Published the verified package as an immutable artifact in the public Git tag, providing a one-command install that does not depend on npm registry publication or npm's Git-dependency packaging lifecycle.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Thank you for helping improve OpenCode Model Control. Contributions should keep

## Development setup

Install Node.js `^20.19.0` or `>=22.12.0`, npm, and the project dependencies:
Install Node.js `>=22.12.0` (prefer a currently supported Node.js 22 or 24 LTS release), npm, and the project dependencies:

```sh
npm ci
Expand Down
96 changes: 73 additions & 23 deletions README.md

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,24 @@ Maintainers should acknowledge a complete private report within seven days. A re

## Security boundaries

The control service is intended to bind only to `127.0.0.1`. It is not an authentication boundary and must not be exposed to a LAN, tunnel, container network, or the public internet. The project does not need or request model-provider keys. OpenCode remains responsible for its own provider credentials and provider usage.
The control service is intended to bind only to `127.0.0.1` and must not be exposed to a LAN, tunnel, container network, or the public internet. Each server process creates a new high-entropy token for local API mutations. The automatic browser launch receives it through a private query URL; the UI stores it in the tab's `sessionStorage` and immediately removes it from the address bar. A tab opened from the bare URL is read-only. Every `POST`, `PUT`, `PATCH`, or `DELETE` API request requires a same-origin `Origin`, JSON, `X-OMC-Request: 1`, and the matching `X-OMC-Session` token.

The pure config generator operates in memory. The connector changes only its documented, receipt-owned OpenCode paths after isolated parser verification, writes a mode-`0600` backup and receipt, and refuses ownership conflicts. To preserve unrelated settings, the connector reads and parses the local OpenCode config. It does not request, extract, log, or transmit provider credentials. Its backup is a full copy of that config and can contain credentials if the user embedded them there; mode `0600` protects against other local accounts, not a compromised account.
With `--no-open`, the private write-enabled URL is printed only to an interactive terminal and is marked keep-private; non-interactive output contains only the public read-only URL. Do not share, bookmark, log, or paste the private URL or its token. Restarting the service rotates the token. This capability protects local mutations but is not user identity or a hardened remote/multi-user authentication boundary. The project does not need or request model-provider keys. OpenCode remains responsible for its own provider credentials and provider usage.

OpenCode Model Control does not include telemetry or remote analytics. Its Usage view executes a fixed aggregate query through OpenCode's plugin-free local database command. The query projects model IDs, token counters, timestamps, recorded cost, and session IDs solely for a distinct-session count. The API returns aggregates and model IDs, never session identifiers, prompts, responses, titles, projects, paths, raw message JSON, or credentials. Query windows are allowlisted, process time/output are bounded, malformed accounting fails closed, and API responses are not cached.
The pure config generator operates in memory. The connector changes only its documented, receipt-owned OpenCode paths after isolated parser verification, writes a mode-`0600` backup and receipt, and refuses ownership conflicts. It preserves unrelated plugins and adds the Omc-Router default only when no user default exists. To preserve unrelated settings, the connector reads and parses the local OpenCode config. It does not request, extract, log, or transmit provider secret material. Its backup is a full copy of that config and can contain credentials if the user embedded them there; mode `0600` protects against other local accounts, not a compromised account. The receipt records exact managed values and a managed-surface version so stale or divergent connections fail closed, but it is not a signature or content-authenticity proof for package files at recorded paths.

Catalog refresh can still cause OpenCode, configured providers, or plugins to access the network. OpenCode and model providers may process prompts and report usage under their own policies. A future provider proxy, remote-control feature, or credential-handling feature requires a separate threat review before release.
The bundled local routing plugin applies only to media turns that enter through `omc-router`. It reads attachment part type/MIME metadata to choose a compatible saved model. It also reads only nonsynthetic, nonignored user text, bounded to 4,000 characters, for a local authorization classification: unless that text clearly requests a code/workspace change, the turn becomes `omc-vision-worker`, all permission requests are denied, and all tool execution is hard-blocked. Empty, synthetic-only, ignored-only, oversized, or unclassifiable text fails closed to this tool-free path. The text is not logged, stored, or separately transmitted by the plugin, which never reads attachment content, filenames, URLs, data URLs, or payloads.

Every media turn receives a fixed instruction that attachment content is untrusted data. Instructions embedded in an image, audio file, video, or PDF cannot authorize tools, delegation, or workspace changes. Only explicit user-authored text outside the attachment can authorize the path that retains Omc-Router for vision-assisted code delegation. OpenCode and the selected provider still receive the original prompt and supported attachment under their own security and privacy boundaries.

Generated specialists cannot access Model Control MCP tools or recursively delegate. The code worker retains bounded implementation tools. The independent reviewer is read-only and has no shell, edit, or write permission. These permissions reduce accidental authority, but prompt-governed delegation and repair limits are not a substitute for user review of consequential model actions.

OpenCode Model Control does not include telemetry or remote analytics. Its Usage view executes a fixed aggregate query through OpenCode's plugin-free local database command. The query projects model IDs, token counters, timestamps, recorded cost, and session IDs solely for a distinct-session count. The API returns aggregates and model IDs, never individual session identifiers, prompts, responses, titles, projects, paths, raw message JSON, or credentials. Query windows are allowlisted, process time/output are bounded, malformed accounting fails closed, and API responses are not cached.

The manual runtime access check is never automatic. It requires explicit provider-request and cost/data acknowledgements and starts one bounded, isolated, plugin-free OpenCode run with a fixed text-only sentinel. OpenCode may retry retryable provider failures, so the run can make more than one provider attempt; every attempt can consume quota, incur charges, and be retained by OpenCode or the provider under their own terms. Model Control bounds time and output, discards raw output, and stores only redacted mode-`0600` result metadata. Before launch, its local isolation guard parses OpenCode's credential store only to inspect credential-type metadata and fails closed when the store cannot be safely interpreted or a type can load remote configuration. It does not extract individual secret fields, log them, copy them into the isolated configuration, or transmit them. A pass is not benchmark or quality evidence.

`OMC_OPENCODE_CONFIG_PATH` and `OMC_CONFIG_DIR` are advanced/testing overrides. The first changes the connector's target but does not make an ordinary OpenCode process load a nonstandard file. The second must be propagated unchanged to the panel and every OpenCode launch so the MCP subprocess and media plugin use the same private policy directory. Misaligned launch environments are outside the supported easy path; use the defaults when consistent propagation is not guaranteed.

Catalog refresh can still cause OpenCode, configured providers, or plugins to access the network. OpenCode and model providers may process prompts and report usage under their own policies. A remote-control or credential-handling feature requires a separate threat review before release.

See the full [threat model](docs/threat-model.md).
12 changes: 11 additions & 1 deletion benchmarks/fixtures/routing-cases.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@
"complexity": "medium",
"modalities": ["text"],
"access": "write",
"requiresReview": true,
"delegationDepth": 0
},
"expected": {
"route": "code-worker",
"route": "orchestrator",
"assignments": [
{
"role": "orchestrator",
Expand All @@ -40,6 +41,10 @@
{
"role": "code-worker",
"modelId": "opencode/ling-3.0-flash-fin-free"
},
{
"role": "reviewer",
"modelId": "opencode/nemotron-3-ultra-free"
}
]
}
Expand Down Expand Up @@ -129,6 +134,7 @@
"complexity": "medium",
"modalities": ["image", "text"],
"access": "write",
"requiresReview": true,
"delegationDepth": 0
},
"expected": {
Expand All @@ -145,6 +151,10 @@
{
"role": "vision-worker",
"modelId": "opencode/mimo-v2.5-free"
},
{
"role": "reviewer",
"modelId": "opencode/nemotron-3-ultra-free"
}
]
}
Expand Down
20 changes: 14 additions & 6 deletions benchmarks/schemas/route-plan.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
"costPreference": { "enum": ["free-first", "paid-first"] },
"costPolicy": { "enum": ["free-only", "known-cost"] },
"maxDelegationDepth": { "type": "integer", "minimum": 0, "maximum": 1 },
"maxFallbacksPerAssignment": { "type": "integer", "minimum": 0, "maximum": 1 },
"maxFallbacksPerAssignment": {
"type": "integer",
"minimum": 0,
"maximum": 1,
"description": "Legacy settings key for the maximum review-driven repair passes; it does not enable alternate-model fallback."
},
"recursiveDelegation": { "const": false }
}
},
Expand Down Expand Up @@ -56,12 +61,15 @@
"role": { "enum": ["orchestrator", "code-worker", "vision-worker", "reviewer"] },
"modelId": { "$ref": "#/$defs/modelId" },
"fallbackModelId": {
"oneOf": [
{ "$ref": "#/$defs/modelId" },
{ "type": "null" }
]
"type": "null",
"deprecated": true,
"description": "Legacy contract field retained for compatibility; alternate-model fallback is not executed."
},
"fallbackCount": {
"const": 0,
"deprecated": true,
"description": "Legacy contract field retained for compatibility; alternate-model fallback is not executed."
},
"fallbackCount": { "type": "integer", "minimum": 0, "maximum": 1 },
"selection": { "enum": ["auto", "explicit"] },
"access": { "enum": ["read", "write"] },
"modalities": {
Expand Down
9 changes: 8 additions & 1 deletion benchmarks/schemas/router-settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"roleAssignments",
"maxDelegationDepth",
"maxFallbacksPerAssignment",
"makeRouterDefault",
"modelControls"
],
"properties": {
Expand All @@ -29,7 +30,13 @@
}
},
"maxDelegationDepth": { "type": "integer", "minimum": 0, "maximum": 1 },
"maxFallbacksPerAssignment": { "type": "integer", "minimum": 0, "maximum": 1 },
"maxFallbacksPerAssignment": {
"type": "integer",
"minimum": 0,
"maximum": 1,
"description": "Legacy persisted name for the maximum review-driven code repair passes after independent review; it does not enable alternate-model fallback."
},
"makeRouterDefault": { "type": "boolean" },
"modelControls": {
"type": "object",
"minProperties": 1,
Expand Down
7 changes: 5 additions & 2 deletions bin/opencode-model-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ Usage:
opencode-model-control disconnect --yes [--json]

Environment:
OMC_PORT Local loopback port (default: 47821)
OMC_CONFIG_DIR Override the private settings directory
OMC_PORT Local loopback port (default: 47821)
OMC_CONFIG_DIR Override the private settings directory; use the same
value for the panel, MCP, and OpenCode plugin process
OMC_OPENCODE_CONFIG_PATH Advanced/testing override for the exact OpenCode
config file managed by Connect and Disconnect

The panel starts without editing OpenCode. Connect and disconnect require explicit
confirmation and use a mode-0600 backup plus an ownership receipt.
Expand Down
3 changes: 3 additions & 0 deletions data/model-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"enabledByDefault": true,
"available": true,
"contextWindowTokens": 200000,
"toolCall": true,
"free": {
"verified": true,
"inputUsdPerMillion": 0,
Expand Down Expand Up @@ -61,6 +62,7 @@
"enabledByDefault": true,
"available": true,
"contextWindowTokens": 200000,
"toolCall": true,
"free": {
"verified": true,
"inputUsdPerMillion": 0,
Expand All @@ -87,6 +89,7 @@
"enabledByDefault": false,
"available": true,
"contextWindowTokens": 1048576,
"toolCall": true,
"free": {
"verified": true,
"inputUsdPerMillion": 0,
Expand Down
Loading