Skip to content

Admin UI only offers the harness/model control at org scope, though the resource accepts any scope #589

Description

@ianTPE

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions