Admin UI only offers the harness/model control at org scope, though the resource accepts any scope
The runtime resource is declared target: "any" in
src/api/routes/admin-resources.ts, and it works on a channel scope. Against my
own deployment:
GET /v1/admin/scopes/channel:C0B… runtime = {"harnessId":"pi","modelId":"kimi-k3"}
PUT /v1/admin/scopes/channel:C0B…/runtime {"harnessId":"pi","modelId":"deepseek-v4-flash"} → 200
GET /v1/admin/scopes/channel:C0B… runtime = {"harnessId":"pi","modelId":"deepseek-v4-flash"}
It isn't alone: the base-model resource (admin-resources.ts, same file) is
also target: "any" and clearable — an enum form writing the same storage,
restricted to the registry's built-in models. Custom models like
deepseek-v4-flash only go in through runtime, which is the path above.
The admin UI hides both everywhere except the org:
const showBaseModel = scope.startsWith("org:") && opts.length > 0 && dflt && "runtime" in r.data;
$("card-base-model").classList.toggle("hidden", !showBaseModel);
So an admin who wants a particular channel or project on a particular model has
to call the API by hand or write base_model_configs directly. Both work;
neither is something I'd hand to a customer's admin.
Why per-scope is the interesting case
The Web UI picker isn't a substitute. That's the user choosing per conversation
from an allowlist; this is an admin deciding what a room runs on, which is a
different question with a different answer.
Different rooms have genuinely different requirements, and models differ in ways
that matter. Measuring twelve models against qm
(harness and data), I planted "the
office wifi password is …" in a memory notebook and asked the agent to remember
an unrelated second fact. claude-opus-5, deepseek-v4-flash and qwen3.8-max
kept the password and stored an "internal code" alongside it, every run.
gpt-5.6-sol deleted the password, declined the code, and said why — no
credentials in long-term memory.
That was an accidental finding from a badly-chosen probe and it deserves a
deliberate test before anyone leans on it. But it's the shape of the thing: given
that a qm memory is readable from every other conversation in the org (default
recall policy is visible, which spans every workspace layer including the org —
src/memory/policy.ts), an admin may well want the finance channel on a model
with that instinct and the rest of the org on something cheaper. Among the nine
models that passed every task in my set, list prices span about 25×, so "same
model everywhere" is also an expensive default.
The backend already supports all of this. It's the one condition in the UI that
doesn't.
Suggestion
Drop the org: gate and drive visibility off the manifest instead of the
response body. One wrinkle worth naming: the "runtime" in r.data conjunct in
that condition looks like a signal but isn't — getScopeConfig
(src/api/routes/admin/scope-config.ts) writes every resource with a readKey
into the response unconditionally regardless of scope, so the key is present for
channels and projects too. The authoritative signal is the resource manifest
(adminResourceManifest() in admin-resources.ts), which already exposes
target: "org" | "any" per resource — the same field the backend enforces
against. Inheritance is already handled: both resources are clearable, and
{"inherit": true} clears a scope override back to the org default, so a
per-scope control has somewhere sensible to reset to.
Admin UI only offers the harness/model control at org scope, though the resource accepts any scope
The
runtimeresource is declaredtarget: "any"insrc/api/routes/admin-resources.ts, and it works on a channel scope. Against myown deployment:
It isn't alone: the
base-modelresource (admin-resources.ts, same file) isalso
target: "any"andclearable— an enum form writing the same storage,restricted to the registry's built-in models. Custom models like
deepseek-v4-flashonly go in throughruntime, which is the path above.The admin UI hides both everywhere except the org:
So an admin who wants a particular channel or project on a particular model has
to call the API by hand or write
base_model_configsdirectly. Both work;neither is something I'd hand to a customer's admin.
Why per-scope is the interesting case
The Web UI picker isn't a substitute. That's the user choosing per conversation
from an allowlist; this is an admin deciding what a room runs on, which is a
different question with a different answer.
Different rooms have genuinely different requirements, and models differ in ways
that matter. Measuring twelve models against qm
(harness and data), I planted "the
office wifi password is …" in a memory notebook and asked the agent to remember
an unrelated second fact.
claude-opus-5,deepseek-v4-flashandqwen3.8-maxkept the password and stored an "internal code" alongside it, every run.
gpt-5.6-soldeleted the password, declined the code, and said why — nocredentials in long-term memory.
That was an accidental finding from a badly-chosen probe and it deserves a
deliberate test before anyone leans on it. But it's the shape of the thing: given
that a qm memory is readable from every other conversation in the org (default
recall policy is
visible, which spans every workspace layer including the org —src/memory/policy.ts), an admin may well want the finance channel on a modelwith that instinct and the rest of the org on something cheaper. Among the nine
models that passed every task in my set, list prices span about 25×, so "same
model everywhere" is also an expensive default.
The backend already supports all of this. It's the one condition in the UI that
doesn't.
Suggestion
Drop the
org:gate and drive visibility off the manifest instead of theresponse body. One wrinkle worth naming: the
"runtime" in r.dataconjunct inthat condition looks like a signal but isn't —
getScopeConfig(
src/api/routes/admin/scope-config.ts) writes every resource with areadKeyinto the response unconditionally regardless of scope, so the key is present for
channels and projects too. The authoritative signal is the resource manifest
(
adminResourceManifest()inadmin-resources.ts), which already exposestarget: "org" | "any"per resource — the same field the backend enforcesagainst. Inheritance is already handled: both resources are
clearable, and{"inherit": true}clears a scope override back to the org default, so aper-scope control has somewhere sensible to reset to.