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
5 changes: 5 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ export const managerChannelBindingSchema = z.object({
executor_kind: z.string(),
model: z.string(),
model_source: z.string(),
selection_policy: z.enum(["preferred", "pinned", "flexible"]).default("preferred"),
allocation_reason: z.string().default(""),
configured_endpoint: z.string().nullable().optional(),
eligible_endpoints: z.array(z.string()).default([]),
allocation_configuration_revision: z.string().default(""),
credential_env_var: z.string(),
operator_credential_configured: z.boolean(),
output_token_budget: z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
},
steward_executor: {
displayName: "Steward executor",
description: "Selects the executor, model, and reasoning effort the steward channel answers on for this machine, ahead of the Chat service environment.",
description: "Guides the steward executor, model, and selection boundary for this machine. A pinned route blocks substitution; a flexible pool permits only authorized fallback.",
},
todo_replan_cadence: { displayName: "Goal review cadence", description: "Configures the Goal review cadence." },
change_quality_qualification: {
Expand Down Expand Up @@ -74,7 +74,7 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
},
steward_executor: {
displayName: "管家执行器",
description: "选择本机管家通道使用的执行器、模型与推理档位,优先级高于服务环境变量。",
description: "配置本机管家的执行器、模型与选择边界;锁定路径禁止替代,灵活池只允许在已授权范围内回退。",
},
todo_replan_cadence: { displayName: "Goal 复核周期", description: "配置 Goal 的复核周期。" },
change_quality_qualification: {
Expand Down Expand Up @@ -128,7 +128,9 @@ const capabilityCopy: Record<WorkspaceLocale, Record<string, LocalizedCopy>> = {
const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
en: {
runtime_profile: { label: "Runtime profile", description: "Restricted keeps scoped LoopX reads only. Trusted owner enables normal host tools while protected operations retain separate checks." },
executor_endpoint: { label: "Steward executor", description: "The executor this machine's steward channel answers on. The choice outranks the Chat service environment and the shipped default." },
selection_policy: { label: "Selection policy", description: "Preferred allows an explicit user choice; pinned rejects another executor; flexible permits fallback only inside the eligible pool." },
executor_endpoint: { label: "Primary steward executor", description: "The preferred or pinned executor for this machine. In a flexible pool it is tried first when available." },
eligible_endpoints: { label: "Flexible eligible executors", description: "One authorized executor per line. Use only with flexible selection and include the primary executor." },
executor_model: { label: "Model", description: "Optional model for the selected executor. Leave blank to keep the executor's own default." },
executor_reasoning_effort: { label: "Reasoning effort", description: "Optional reasoning effort for the selected executor. Leave blank to keep the executor's own default." },
completed_todos: { label: "Completed Todos between Goal reviews", description: "Machine default or explicit Goal override, from 1 to 5." },
Expand All @@ -152,7 +154,9 @@ const fieldCopy: Record<WorkspaceLocale, FieldCopy> = {
},
"zh-CN": {
runtime_profile: { label: "运行模式", description: "restricted 仅使用受限 LoopX 读取;trusted_owner 开放常规宿主工具,但受保护操作仍单独校验。" },
executor_endpoint: { label: "管家执行器", description: "本机管家通道使用的执行器;优先级高于 Chat 服务环境变量与出货默认值。" },
selection_policy: { label: "选择策略", description: "preferred 允许用户显式改选;pinned 拒绝其他执行器;flexible 只在已授权资源池内回退。" },
executor_endpoint: { label: "首选管家执行器", description: "本机首选或锁定的执行器;灵活池模式下优先尝试它。" },
eligible_endpoints: { label: "灵活池可用执行器", description: "每行一个已授权执行器,仅用于 flexible;必须包含首选执行器。" },
executor_model: { label: "模型", description: "所选执行器使用的模型,可留空;留空表示沿用执行器自身的默认模型。" },
executor_reasoning_effort: { label: "推理档位", description: "所选执行器使用的推理档位,可留空;留空表示沿用执行器自身的默认档位。" },
completed_todos: { label: "两次 Goal 复核间的已完成 Todo 数", description: "可设置 1–5;机器默认值可被 Goal 显式覆盖。" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ export function ChannelHeader({
&& managerChannelBinding.executor_endpoint_default_reason === "steward_channel_default"
? "header.managerEndpointStewardDefault"
: null;
const managerSelectionPolicyLabel = managerChannelBinding
? t(managerChannelBinding.selection_policy === "pinned"
? "header.managerSelectionPinned"
: managerChannelBinding.selection_policy === "flexible"
? "header.managerSelectionFlexible"
: "header.managerSelectionPreferred")
: null;
const managerAllocationReasonLabel = managerChannelBinding?.allocation_reason
? t(managerChannelBinding.allocation_reason === "user_explicit"
? "header.managerAllocationUser"
: managerChannelBinding.allocation_reason === "pinned_configuration"
? "header.managerAllocationPinned"
: managerChannelBinding.allocation_reason === "flexible_availability_fallback"
? "header.managerAllocationFallback"
: managerChannelBinding.allocation_reason === "flexible_pool_unavailable"
? "header.managerAllocationUnavailable"
: managerChannelBinding.allocation_reason === "flexible_primary_available"
? "header.managerAllocationPrimary"
: managerChannelBinding.allocation_reason === "product_default"
? "header.managerAllocationProductDefault"
: managerChannelBinding.allocation_reason === "service_override"
? "header.managerAllocationService"
: "header.managerAllocationConfigured")
: null;

const runtimeControl = readOnlySourceLabel ? (
<span className="personal-read-only-source" title={t("header.readOnlySourceDescription", { source: readOnlySourceLabel })}><Eye size={15} />{readOnlySourceLabel}<small>{t("common.readOnly")}</small></span>
Expand Down Expand Up @@ -142,6 +166,12 @@ export function ChannelHeader({
sandbox: managerRuntime.sandbox,
})}</p>
) : null}
{managerSelectionPolicyLabel && managerAllocationReasonLabel && managerChannelBinding ? (
<p>{t("header.managerAllocation", {
policy: managerSelectionPolicyLabel,
reason: managerAllocationReasonLabel,
})}</p>
) : null}
{managerExecutionDefaultReason && managerChannelBinding ? (
<span className="personal-execution-rule-note">
{t(managerExecutionDefaultReason, { executor: managerChannelBinding.executor_endpoint })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,18 @@ const en = {
"header.managerExecutionUnavailableOutputBudget": "Selected {executor} has an invalid per-request output-token limit; set a positive integer and reload.",
"header.managerEndpointStewardDefault": "Runs {executor}, the steward channel's shipped default. Select an executor explicitly to move it.",
"header.managerOutputTokenBudget": "{tokens} tok/request",
"header.managerAllocation": "Selection: {policy} · {reason}",
"header.managerSelectionPreferred": "preferred",
"header.managerSelectionPinned": "pinned",
"header.managerSelectionFlexible": "flexible pool",
"header.managerAllocationUser": "explicit user choice",
"header.managerAllocationPinned": "pinned by machine configuration",
"header.managerAllocationFallback": "primary unavailable; used an eligible fallback",
"header.managerAllocationUnavailable": "no eligible route is currently available",
"header.managerAllocationPrimary": "primary route is available",
"header.managerAllocationProductDefault": "product default",
"header.managerAllocationService": "service environment override",
"header.managerAllocationConfigured": "configured route",
"header.managerOverview": "Overview",
"header.managerView": "Manager view",
"header.openGoalNavigation": "Open Goal navigation",
Expand Down Expand Up @@ -1492,6 +1504,18 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"header.managerExecutionUnavailableOutputBudget": "所选执行器 {executor} 的每请求输出 token 上限无效;请设置为正整数后刷新。",
"header.managerEndpointStewardDefault": "当前运行 {executor},管家通道的出货默认值;如需改指请显式选择执行器。",
"header.managerOutputTokenBudget": "每次请求 {tokens} token",
"header.managerAllocation": "选择:{policy} · {reason}",
"header.managerSelectionPreferred": "偏好",
"header.managerSelectionPinned": "锁定",
"header.managerSelectionFlexible": "灵活资源池",
"header.managerAllocationUser": "用户明确选择",
"header.managerAllocationPinned": "由本机配置锁定",
"header.managerAllocationFallback": "首选不可用,使用池内合格替代",
"header.managerAllocationUnavailable": "当前没有可用的合格路径",
"header.managerAllocationPrimary": "首选路径可用",
"header.managerAllocationProductDefault": "产品默认路径",
"header.managerAllocationService": "服务环境覆盖",
"header.managerAllocationConfigured": "按已配置路径",
"header.managerOverview": "总览",
"header.managerView": "管家视图",
"header.openGoalNavigation": "打开 Goal 导航",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,19 @@ function completeMachineConfiguration(
current: Record<string, unknown> | undefined,
draft: Record<string, unknown>,
) {
return {
const complete = {
...configurationObject(capability.default),
...configurationObject(current),
...draft,
};
// The guided steward editor owns the v1 selection-policy fields. Opening an
// installed v0 preference in that form is an explicit migration preview;
// JSON mode can still submit the legacy shape unchanged when needed.
if (capability.capability_id === "steward_executor"
&& (Object.hasOwn(draft, "selection_policy") || Object.hasOwn(draft, "eligible_endpoints"))) {
complete.schema_version = configurationObject(capability.default).schema_version;
}
return complete;
}

function validGuidedDraft(capability: CapabilityDescriptor, value: Record<string, unknown>) {
Expand All @@ -61,6 +69,18 @@ function validGuidedDraft(capability: CapabilityDescriptor, value: Record<string
&& String(value.route_ref ?? "").trim()
&& String(value.timezone ?? "").trim());
}
if (capability.capability_id === "steward_executor") {
const policy = String(value.selection_policy ?? "preferred");
const primary = String(value.executor_endpoint ?? "");
const eligible = Array.isArray(value.eligible_endpoints)
? value.eligible_endpoints.map((item) => String(item))
: [];
if (policy === "flexible") {
return eligible.length > 0 && eligible.includes(primary)
&& new Set(eligible).size === eligible.length;
}
return eligible.length === 0;
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ for (const capabilityId of [
const matches = capabilityLocalization.match(new RegExp(`${capabilityId}:`, "g")) ?? [];
assert.equal(matches.length, 2, `${capabilityId} has English and Simplified Chinese metadata`);
}
for (const fieldKey of ["allowed_domains", "coordinator_agent_id", "enabled", "executor_endpoint", "executor_model", "executor_reasoning_effort", "max_children", "profile", "profile_preset", "review_priority", "route_ref", "safe_fix", "strict_receipt", "timezone"]) {
for (const fieldKey of ["allowed_domains", "coordinator_agent_id", "eligible_endpoints", "enabled", "executor_endpoint", "executor_model", "executor_reasoning_effort", "max_children", "profile", "profile_preset", "review_priority", "route_ref", "safe_fix", "selection_policy", "strict_receipt", "timezone"]) {
const matches = capabilityLocalization.match(new RegExp(`^\\s+${fieldKey}:`, "gm")) ?? [];
assert.equal(matches.length, 2, `${fieldKey} has English and Simplified Chinese field copy`);
}
Expand Down
34 changes: 26 additions & 8 deletions docs/architecture/rfcs/harness-selection-dsh-pi-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,12 +505,15 @@ namespace, `steward_executor`
(`loopx/capabilities/steward_executor/machine_defaults.py`), so a machine's
steward choice is a first-class operator setting.

The namespace holds exactly three fields and no credential:
The current namespace keeps the primary endpoint, its model and effort, and a
closed selection policy. It stores no credential:

```json
{
"schema_version": "steward_executor_machine_defaults_v0",
"schema_version": "steward_executor_machine_defaults_v1",
"selection_policy": "preferred",
"executor_endpoint": "codex",
"eligible_endpoints": [],
"executor_model": null,
"executor_reasoning_effort": null
}
Expand All @@ -519,10 +522,14 @@ The namespace holds exactly three fields and no credential:
`executor_endpoint` is required and restricted to the endpoints LoopX ships as
channel executors; a blank model or reasoning effort means this machine decides
nothing about that field, so the channel keeps resolving it from the lower
layers. Unknown fields, an unknown schema version, an unlisted endpoint, and an
unsupported reasoning effort all fail closed before any effect. An operator who
needs an adapter the namespace does not list still has
`LOOPX_MANAGER_ENDPOINT`.
layers. `preferred` keeps that endpoint as the default while honoring a user's
explicit executor pick. `pinned` rejects a different explicit pick. `flexible`
requires a non-empty `eligible_endpoints` pool that contains the primary and
allows availability fallback only inside that pool. Unknown fields, an unknown
schema version, an unlisted endpoint, an invalid pool, and an unsupported
reasoning effort all fail closed before any effect. Stored v0 documents retain
their former `preferred` behavior. An operator who needs an adapter the
namespace does not list still has `LOOPX_MANAGER_ENDPOINT`.

Precedence is stated once, in the channel owner
(`loopx/chat_manager.py`): machine configuration, then the service environment,
Expand All @@ -531,7 +538,18 @@ so `loopx machine-config describe` publishes the template and the Dashboard
edits the same document through the existing revision-locked transaction; the
channel readback adds `executor_endpoint_source: machine_configuration` plus the
document's `status` and `configuration_revision`, so a machine decision can be
told from a service-environment value without reading the store.
told from a service-environment value without reading the store. The resolved
endpoint, model, effort, policy, allocation reason, eligible pool and source
revision are also persisted on the manager Session. A live Session therefore
keeps the allocation under which it started instead of being reinterpreted
after a configuration edit or process restart. `loopx chat-endpoint
inspect-steward` reads the effective configuration and current Session binding
through the same public projection.

This stage implements the allocation boundary and availability fallback. It
does not infer semantic task fit from chat prose. A later Agent decision can
submit an explicit executor pick, but the same pinned or flexible boundary
still authorizes or rejects it.

What this increment does *not* change: the shipped default stays `codex` on
every machine, a credential still never selects an endpoint, the managed host
Expand All @@ -546,7 +564,7 @@ selection.
Validation: `tests/capabilities/test_steward_executor_machine_defaults.py`,
`tests/test_manager_channel_binding.py`, `tests/test_chat_machine_configuration_api.py`,
`tests/capabilities/test_capability_configuration_ui.py`, and
`examples/loopx-steward-channel-binding-smoke.py`.
`apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs`.

### Steward Answer Identity and Runtime Selection (2026-09-16)

Expand Down
15 changes: 11 additions & 4 deletions docs/reference/operator-model-credential.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ through `/api/chat/operator-credential`.
Two surfaces answer on this machine, and they are selected separately.

- **The steward channel** is the conversation a person talks to: the Dashboard
manager channel and the bound Lark/Feishu manager group. Its executor is one
machine-level choice, `steward_executor.executor_endpoint`, holding a shipped
channel endpoint (`codex`, or `dsh` for the managed host).
manager channel and the bound Lark/Feishu manager group. Its machine setting
names a primary `steward_executor.executor_endpoint` (`codex`, or `dsh` for
the managed host) and a `preferred`, `pinned`, or `flexible` selection policy.
Flexible selection is confined to the configured eligible endpoint pool.
- **A managed Turn or managed agent** is bounded work that runs without a person
in the loop. Its host resolves from the operator credential: with one stored,
the shipped default is the managed host `dsh`, and without one it is the
Expand All @@ -129,6 +130,9 @@ loopx machine-config describe
# Read the stored document and the effective steward resolution.
loopx machine-config inspect

# Read the effective steward binding and the current Session allocation.
loopx chat-endpoint inspect-steward

# Preview an exact change, then apply it with the plan revision it returned.
loopx machine-config preview
loopx machine-config apply
Expand All @@ -146,7 +150,10 @@ the host from the name it resolved: `/api/chat/capabilities` reports the
steward's `executor_endpoint`, its `executor_endpoint_source`
(`machine_configuration`, `explicit_config` or `product_default`), the
`execution_profile` (`deepseek-v4-flash@high` on the shipped managed profile),
`available`, and the bound Session's `session_mode` and `session_status`.
`available`, the selection policy and allocation reason, and the bound
Session's `session_mode` and `session_status`. The Session persists its chosen
endpoint, model, effort, policy, pool and source revision, so later configuration
edits apply through a new allocation rather than rewriting an active conversation.
A connection record stores the resolved endpoint as an observation, so it cannot
outrank the machine setting.

Expand Down
Loading
Loading