{locale === "zh-CN" ? "运行环境" : "Execution environment"}
{!selectedGoal && managerRuntime ? (
{managerRuntime.status === "ready"
- ? t("header.managerRuntime", {
- profile: managerRuntime.runtime_profile,
- sandbox: managerRuntime.sandbox,
- })
+ ? t(managerRuntime.runtime_profile === "trusted_owner"
+ ? "header.managerRuntimeTrusted"
+ : "header.managerRuntimeRestricted")
: t("header.managerRuntimeFallback", {
profile: managerRuntime.runtime_profile,
sandbox: managerRuntime.sandbox,
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx
index b0ccfa181f..bcd8ca49ef 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/i18n.tsx
@@ -645,7 +645,8 @@ const en = {
"header.statusProjection": "LoopX state projection",
"header.managerModelSettings": "Change steward model and reasoning effort",
"header.managerDescription": "Your personal workspace across Goals",
- "header.managerRuntime": "{profile} · {sandbox}",
+ "header.managerRuntimeTrusted": "Local tools · File edits and commands",
+ "header.managerRuntimeRestricted": "Read-only",
"header.managerRuntimeFallback": "Configuration invalid; fell back to {profile} · {sandbox}. Repair it in Machine capabilities.",
"header.managerExecutorKindIndividual": "individual CLI login",
"header.managerExecutorKindManaged": "operator credential",
@@ -1157,6 +1158,8 @@ const en = {
"machine.editorUnavailableDescription": "The namespace remains visible in the registry. Install or upgrade its Dashboard editor before changing it.",
"machine.editorMode": "Editor mode",
"machine.liveDefault": "Live default; Goal override wins",
+ "machine.stewardDefaults": "Steward settings for this device",
+ "machine.stewardDefaultsDescription": "These settings apply to this device’s steward. Goal conversations and Agent Loops keep their own configuration; active Turns finish with their current binding.",
"machine.liveDefaultDescription": "Goals without an explicit override read the current machine policy at the capability’s next decision point. Changes and removal affect those existing Goals immediately; an explicit Goal override stays pinned.",
"machine.credentialTitle": "Operator model credential",
"machine.credentialDescription": "The key and endpoint the steward channel and the managed host authenticate with on this machine. The key is stored in its own owner-only file, never in the machine configuration that is projected here, and it is never read back — only its fingerprint is.",
@@ -2233,7 +2236,8 @@ const zhCN: Record = {
"header.statusProjection": "LoopX 状态投影",
"header.managerModelSettings": "调整管家模型与思考深度",
"header.managerDescription": "跨 Goal 的个人工作入口",
- "header.managerRuntime": "{profile} · {sandbox}",
+ "header.managerRuntimeTrusted": "本机工具 · 可读写文件和运行命令",
+ "header.managerRuntimeRestricted": "只读",
"header.managerRuntimeFallback": "配置无效,已回退到 {profile} · {sandbox};请在机器能力设置中修复。",
"header.managerExecutorKindIndividual": "个人 CLI 登录",
"header.managerExecutorKindManaged": "operator 凭据",
@@ -2745,6 +2749,8 @@ const zhCN: Record = {
"machine.editorUnavailableDescription": "它仍会显示在 Registry 中;安装或升级对应的 Dashboard 编辑器后才能修改。",
"machine.editorMode": "编辑模式",
"machine.liveDefault": "实时默认值;Goal 显式覆盖优先",
+ "machine.stewardDefaults": "本机管家设置",
+ "machine.stewardDefaultsDescription": "这些设置用于当前设备的管家。Goal 对话和 Agent Loop 保留各自配置;正在执行的回合沿用当前分配。",
"machine.liveDefaultDescription": "没有显式覆盖的 Goal 会在该能力下一次决策时读取当前机器策略。修改或移除策略会立即影响这些已有 Goal;显式 Goal 覆盖保持固定。",
"machine.credentialTitle": "操作者模型凭据",
"machine.credentialDescription": "管家通道与托管宿主在本机认证用的 key 与 endpoint。key 单独存放于仅属主可读的文件,不会进入这里展示的机器配置,也不会被回读——回读的是它的指纹。",
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx
index 24a96d841a..f024b04987 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx
+++ b/apps/presentation/dashboard/src/features/personal-workspace/machine-configuration-settings.tsx
@@ -354,8 +354,8 @@ export function MachineConfigurationSettings({ section, onChanged }: { section:
{diagnosis}
{section === "steward" ?
- {t("machine.liveDefault")}
- {t("machine.liveDefaultDescription")}
+ {t("machine.stewardDefaults")}
+ {t("machine.stewardDefaultsDescription")}
: null}
{section === "other" ?
: null}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs
index 90e8e24c54..e98a81b4aa 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs
+++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs
@@ -444,7 +444,6 @@ assert.match(machineSettings, /selected\.capability_id === "pull_request_review"
assert.match(machineSettings, /previewMachineConfiguration\(/, "Machine settings require a preview before apply");
assert.match(machineSettings, /applyMachineConfiguration\([\s\S]*preview\.plan_revision/, "Machine settings apply the exact reviewed revision");
assert.match(machineSettings, /previewMachineConfigurationRollback\(/, "Machine settings preview rollback before execution");
-assert.match(machineSettings, /liveDefaultDescription/, "Live defaults and Goal overrides are explained together");
assert.match(machineSettings, /inspection\?\.capability_catalog\.capabilities/, "Machine settings discover capabilities from the shared registry catalog");
assert.match(machineSettings, /inspection\?\.invalid_namespaces\[0\]/, "Invalid machine state identifies the affected namespace without reading its stored values");
assert.match(machineSettings, /machine-invalid-repair[\s\S]*role="alert"/, "Invalid machine state exposes a visible guided repair path");
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css
index b8c6c950ff..5559def549 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css
+++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace.css
@@ -1545,6 +1545,7 @@ button.personal-execution-chip:focus-visible { outline: 2px solid #0070f3; outli
stays: it is the only place the operator reads which executor answers for
the steward, and an unavailable selection must not become invisible. */
.personal-channel-title p.personal-manager-execution { display: flex; }
+ .personal-channel-title .personal-runtime-details[open] > p { display: block; white-space: normal; overflow-wrap: anywhere; }
.personal-channel-actions { min-width: 0; }
.personal-chat-model-menu { position: static; }
.personal-chat-model-panel { left: 14px; right: 14px; width: auto; }
diff --git a/apps/presentation/dashboard/src/views/dashboard-page.tsx b/apps/presentation/dashboard/src/views/dashboard-page.tsx
index abee83cca6..5d39cd2637 100644
--- a/apps/presentation/dashboard/src/views/dashboard-page.tsx
+++ b/apps/presentation/dashboard/src/views/dashboard-page.tsx
@@ -599,6 +599,9 @@ function readPersonalAgentSelections() {
return Object.fromEntries(
Object.entries(value).filter((entry): entry is [string, string] =>
typeof entry[0] === "string" && typeof entry[1] === "string"
+ // The steward executor is machine-owned; only its status-only view
+ // remains a browser preference. Goal/workspace picks stay local.
+ && (entry[0] !== "manager" || entry[1] === "status-only")
),
);
} catch {
@@ -1423,8 +1426,8 @@ function PersonalGoalHome({
?? discoveredAgents.find((agent) => agent.available)?.agentId
?? "status-only";
// The manager channel answers on the executor this machine declares for the
- // steward, and the client is expected to send no endpoint until the operator
- // picks one. The picker therefore has to show that same resolution: an
+ // steward, and the client sends no endpoint override. The header therefore
+ // has to show that same resolution: an
// executor merely discovered on this machine is not a reason to present
// itself as the steward's runtime, or the header chip, the composer and the
// answer would tell three different stories.
@@ -1735,10 +1738,9 @@ function PersonalGoalHome({
});
return;
}
- // The steward channel owns its executor default; only a pick the owner
- // actually made for this context is sent.
+ // The steward channel owns its executor; Goal/workspace picks stay local.
const sessionEndpoint =
- targetContext.kind === "manager" ? selectedAgents[targetContextId] : selectedAgent.agentId;
+ targetContext.kind === "manager" ? undefined : selectedAgent.agentId;
const created = await createChatSession(targetContext, sessionEndpoint, "resume_latest", undefined,
requestedAgentModels.current[agentModelSelectionKey(targetContextId, sessionEndpoint ?? selectedAgent.agentId)],
requestedAgentEfforts.current[agentModelSelectionKey(targetContextId, sessionEndpoint ?? selectedAgent.agentId)]);
@@ -2296,7 +2298,7 @@ function PersonalGoalHome({
if (!sessionId) {
const mode = newSessionRequired.current.has(sessionKey) ? "new" : "resume_latest";
const sessionEndpoint =
- targetContext.kind === "manager" ? selectedAgents[targetContextId] : selectedRoute.agentId;
+ targetContext.kind === "manager" ? undefined : selectedRoute.agentId;
const session = await createChatSession(targetContext, sessionEndpoint, mode, preparationController.signal,
requestedAgentModels.current[agentModelSelectionKey(targetContextId, sessionEndpoint ?? selectedRoute.agentId)],
requestedAgentEfforts.current[agentModelSelectionKey(targetContextId, sessionEndpoint ?? selectedRoute.agentId)]);
@@ -2670,7 +2672,12 @@ function PersonalGoalHome({
if (!agentOptions.some((agent) => agent.agentId === agentId && agent.available)) {
return;
}
- setSelectedAgents((current) => ({ ...current, [contextId]: agentId }));
+ setSelectedAgents((current) => {
+ const next = { ...current };
+ if (contextId === "manager" && agentId !== "status-only") delete next[contextId];
+ else next[contextId] = agentId;
+ return next;
+ });
}
function chooseAgentModel(agentId: string, modelId: string) {
diff --git a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md
index a5925829c5..b9cd91d9d9 100644
--- a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md
+++ b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.md
@@ -107,7 +107,7 @@ The baseline already has substantial reusable machinery:
| Existing owner | Source / fact | Consequence |
| --- | --- | --- |
-| Manager identity/config | `loopx/chat_manager.py`: `open_manager_session`, `manager_workspace`, `manager_model_config` | Stable global role, audience-derived private workspace, `resume_latest`; default Codex model is `gpt-6-astra` / `high` |
+| Manager identity/config | `loopx/chat_manager.py`: `open_manager_session`, `manager_workspace`, `manager_model_config` | Stable global role, audience-derived private workspace, `resume_latest`; default Codex model is `gpt-6.1-sol` / `xhigh` |
| Host execution | `loopx/chat_agent.py`: `_turn_prompt`, `CodexChatAgentSession.start`; `loopx/chat_runtime.py`: adapter creation/restoration | Codex app-server start/resume exists. Planning uses read-only sandbox and never approvals; only the manager read dynamic tool is added by this path |
| Manager behavioral restriction | `MANAGER_AGENT_OBJECTIVE` and the planning prompt | Shell/arbitrary repository reads and mutations are restricted; outside intent delegation, durable changes become preview/apply proposals |
| Session restoration | `loopx/chat_runtime.py` | Saved upstream identity is resumed when compatible; context-version and audience changes can force a new thread. This is not evidence that every deployed request resumes successfully |
diff --git a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md
index 367807056d..8a4bfc1384 100644
--- a/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md
+++ b/docs/architecture/rfcs/capable-manager-semantic-handoff-v0.zh-CN.md
@@ -83,7 +83,7 @@ lease 转交或 shared-intent 修改权限。这是 R3 的有界切片,不代
| 现有 owner | 源码 / 事实 | 含义 |
| --- | --- | --- |
-| 管家身份与配置 | `loopx/chat_manager.py`:`open_manager_session`、`manager_workspace`、`manager_model_config` | 稳定全局角色、按受众派生的私人工作区、`resume_latest`;Codex 默认 `gpt-6-astra` / `high` |
+| 管家身份与配置 | `loopx/chat_manager.py`:`open_manager_session`、`manager_workspace`、`manager_model_config` | 稳定全局角色、按受众派生的私人工作区、`resume_latest`;Codex 默认 `gpt-6.1-sol` / `xhigh` |
| 宿主执行 | `loopx/chat_agent.py`:`_turn_prompt`、`CodexChatAgentSession.start`;`loopx/chat_runtime.py` 的 adapter 创建与恢复 | 已有 app-server start/resume;planning 使用 read-only sandbox、never approval;该路径仅额外注入 manager read 动态工具 |
| 管家行为限制 | `MANAGER_AGENT_OBJECTIVE` 和 planning prompt | 限制 shell、任意仓库阅读和写操作;除意图委托之外,持久修改走 preview/apply 提案 |
| 会话恢复 | `loopx/chat_runtime.py` | 兼容时恢复保存的 upstream 身份;上下文版本、受众变化可迫使新建线程。不能据此宣称每次线上请求都成功 resume |
diff --git a/docs/architecture/rfcs/manager-runtime-profile-v0.md b/docs/architecture/rfcs/manager-runtime-profile-v0.md
index 74567c9531..283453555f 100644
--- a/docs/architecture/rfcs/manager-runtime-profile-v0.md
+++ b/docs/architecture/rfcs/manager-runtime-profile-v0.md
@@ -13,7 +13,7 @@
The original LoopX manager was a restricted planning conversation. It could read scoped,
structured Goal context but could not use the host's filesystem, shell, Git, web access, or
-configured connectors. That is a safe installation default, but it also turns ordinary work
+configured connectors. That former installation default also turns ordinary work
already authorized by the owner into delegation, waiting, or manual context transfer.
Removing a prompt sentence is insufficient. The host sandbox, prompt, managed workspace
@@ -23,11 +23,14 @@ read-only.
### Contract
-`manager_runtime_profile_v0` is an explicit, persistent machine-level grant:
+`manager_runtime_profile_v0` is the machine-level host-tool policy. The private owner manager
+ships with host tools; an explicit machine choice overrides that default. The Codex steward
+ships with `gpt-6.1-sol` and `xhigh`; machine and service overrides retain their precedence.
+These defaults do not change ordinary Goal Chat models or grant other Agents tools:
-- `restricted` is the default. The manager keeps scoped LoopX reads and the Codex sandbox is
+- An explicit `restricted` choice keeps scoped LoopX reads and the Codex sandbox is
`read-only`.
-- `trusted_owner` lets the Codex manager use normal host filesystem, shell, Git, web, and
+- The default `trusted_owner` lets the Codex manager use normal host filesystem, shell, Git, web, and
configured-connector tools. Its Codex sandbox is `danger-full-access`.
Ordinary authorized requests now execute in the current conversation using the relevant
project's instructions and skills, verifying file edits by readback and durable LoopX
@@ -45,14 +48,15 @@ read-only.
- `trusted_owner` applies to the local private owner-manager and a native private steward
binding freshly verified by the existing Core conversation owner. That owner checks the
App, owner principal, private source, current resource grant and frozen Session identity.
- The audience proof alone grants no tools: the same persistent machine grant is also required.
+ The audience proof alone grants no tools: the effective machine policy still applies,
+ including an explicit restricted choice.
Other external audiences, including Lark groups and workspace-only resources, remain
`restricted`. Project assistants retain their separate workspace grant.
Configuration reuses the existing capability workbench and its
`preview -> apply -> readback` transaction. There is no second configuration source. Missing
-configuration defaults to `restricted`; invalid configuration also falls back safely and
-projects `configuration_invalid` plus a repair action.
+configuration uses `trusted_owner` for the private owner; invalid configuration falls back to
+`restricted` and projects `configuration_invalid` plus a repair action.
### Session consistency
@@ -60,7 +64,7 @@ Each manager Session records the profile, sandbox, standing grant, tool classes,
revision, and status used to start its upstream thread. A change to the effective manager
namespace closes the old upstream thread and starts a new one with visible history, preventing
stale sandbox or prompt state. Changes to unrelated machine capabilities do not rotate a healthy
-manager. A healthy legacy Session already equivalent to the restricted default is backfilled
+manager. A healthy legacy Session already equivalent to an explicit restricted policy is backfilled
without an unnecessary restart.
Dashboard shows both machine configuration and current Session readback. CLI/managed Turn,
@@ -79,13 +83,16 @@ as work, request, or delivery authority.
`43d362532` introduced the machine profile. Native private steward bindings now carry a fresh
Core owner-audience proof into that existing profile at open, resume and authorization-scope
-refresh. The installation default stays `restricted`; selecting DSH does not inherit the capable
-profile. Source/RPC tests do not certify installed live-model work or desktop notification
+refresh. The private owner installation default is `trusted_owner`. Selecting DSH does not make
+that endpoint support the capable profile; its existing actionable refusal remains. Source/RPC
+tests do not certify installed live-model work or desktop notification
delivery. Follow [roadmap](loopx-overall-roadmap-v0.md) R2 for those remaining qualifications.
### Acceptance
-1. A default installation starts `restricted` with no implicit grant.
+1. A default private owner installation starts `trusted_owner`, `gpt-6.1-sol` and `xhigh`.
+ Explicit `restricted` and invalid-configuration fallback remain read-only; other audiences
+ do not inherit the default tools.
2. Machine configuration preview/apply/readback persists `trusted_owner`.
3. A new Codex manager thread sends `danger-full-access` to app-server, and its Turn prompt and
managed `AGENTS.md` no longer contain the read-only restriction.
@@ -98,6 +105,6 @@ delivery. Follow [roadmap](loopx-overall-roadmap-v0.md) R2 for those remaining q
fallback state.
8. An external audience without an existing scoped grant remains `read-only` and exposes the
effective downgrade accurately.
-9. A verified native private steward uses the explicit machine grant, preserves its Session
+9. A verified native private steward uses the effective machine policy, preserves its Session
and history across profile changes/restarts, and rechecks binding/provider identity before
healthy-host reuse. Other Apps, groups and workspace-only resources do not inherit it.
diff --git a/docs/architecture/rfcs/manager-runtime-profile-v0.zh-CN.md b/docs/architecture/rfcs/manager-runtime-profile-v0.zh-CN.md
index eef5996ca4..196e14f044 100644
--- a/docs/architecture/rfcs/manager-runtime-profile-v0.zh-CN.md
+++ b/docs/architecture/rfcs/manager-runtime-profile-v0.zh-CN.md
@@ -11,19 +11,21 @@
### 问题
LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的结构化 Goal
-上下文,却不能使用安装宿主已有的文件、Shell、Git、Web 或已配置连接器。这适合
-默认安装,但会把已经由 Owner 授权的常规工程工作再次退化为转交、等待或人工搬运。
+上下文,却不能使用安装宿主已有的文件、Shell、Git、Web 或已配置连接器。此前的
+受限默认会把已经由 Owner 授权的常规工程工作再次退化为转交、等待或人工搬运。
单纯删除提示词限制不够。宿主 sandbox、提示词、工作区说明、持久配置和会话回读
必须表达同一个有效模式,否则 UI 显示“已开放”时,旧会话仍可能运行在只读线程中。
### 契约
-`manager_runtime_profile_v0` 是机器级、显式且持久的授权选择:
+`manager_runtime_profile_v0` 是机器级宿主工具策略。本人私有管家默认使用本机工具,
+显式机器选择优先。Codex 管家默认模型为 `gpt-6.1-sol`、推理为 `xhigh`;机器配置与服务
+覆盖仍保留原优先级。这不改变普通 Goal 对话模型或其他 Agent 的工具权限:
-- `restricted` 是默认值。管家保留作用域化的 LoopX 读取,Codex sandbox 为
+- 显式选择 `restricted` 时,管家保留作用域化的 LoopX 读取,Codex sandbox 为
`read-only`。
-- `trusted_owner` 允许 Codex 管家使用宿主正常提供的文件、Shell、Git、Web 和已配置
+- 默认 `trusted_owner` 允许 Codex 管家使用宿主正常提供的文件、Shell、Git、Web 和已配置
连接器,Codex sandbox 为 `danger-full-access`。
已授权的普通请求现在在当前对话中按相关项目的指令与 skills 执行;文件改动以读回
核验,持久 LoopX 状态改动仍以既有类型化回执核验。
@@ -38,11 +40,12 @@ LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的
typed error,不能把受限执行伪装成已开放。
- `trusted_owner` 适用于本地私有 Owner 管家和由既有 Core conversation owner 重新核验的
原生本人管家私聊。Core 核验 App、Owner 身份、私聊来源、当前资源授权和冻结的 Session
- 身份;受众证明本身不授予工具,仍须同一份显式机器授权。其他外部受众(包括 Lark 群)
- 和 `workspace_only` 资源继续受限;个人助手保留独立的工作区授权。
+ 身份;受众证明本身不授予工具,仍按实际机器策略执行,包括显式受限选择。其他外部
+ 受众(包括 Lark 群)和 `workspace_only` 资源继续受限;个人助手保留独立的工作区授权。
机器配置沿用现有 capability workbench 的 `preview -> apply -> readback` 流程,不建立第二份
-配置源。配置缺失时安全回退 `restricted`;配置损坏时也回退,并在能力投影中显示
+配置源。配置缺失时本人私有管家使用 `trusted_owner`;配置损坏时回退 `restricted`,
+并在能力投影中显示
`configuration_invalid` 与修复入口。
### 会话一致性
@@ -50,7 +53,7 @@ LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的
每个 manager Session 保存实际启动时的 profile、sandbox、standing grant、tool classes、
配置 revision 和状态。有效 manager namespace 发生变化时,LoopX 关闭旧上游线程,并用
可见历史启动新线程,避免旧 sandbox 或旧提示词继续生效。其他机器能力的修改不会旋转
-健康的管家会话。旧版、默认受限的健康会话只补齐 readback,不做无意义重启。
+健康的管家会话。旧版、与显式受限策略相同的健康会话只补齐 readback,不做无意义重启。
Dashboard 同时显示机器配置和当前会话 readback。CLI/managed Turn、Dashboard 与 Lark
继续调用同一个 manager runtime controller;Lark 是同源会话的入口和投影,不拥有独立
@@ -65,13 +68,15 @@ outbox,也不把管家 session 字段当成工作、请求或送达权威。
### 实现与后继
`43d362532` 引入机器模式。原生本人管家私聊在打开、续接和授权范围刷新时,将 Core
-重新核验的 Owner 受众证明传给既有模式。安装默认仍为 `restricted`;DSH 通道选择不继承
-该强能力模式。源码与 RPC 测试不证明已部署的真实模型工作或桌面通知送达;按
+重新核验的 Owner 受众证明传给既有模式。本人私有管家的安装默认为 `trusted_owner`。
+选择 DSH 不会使该 endpoint 获得强能力模式支持,仍保留既有明确、可恢复的拒绝。
+源码与 RPC 测试不证明已部署的真实模型工作或桌面通知送达;按
[统一路线](loopx-overall-roadmap-v0.zh-CN.md) R2 完成这些剩余验收。
### 验收
-1. 默认安装启动 `restricted`,没有隐式授权。
+1. 默认本人私有管家启动 `trusted_owner`、`gpt-6.1-sol` 与 `xhigh`。显式受限选择和
+ 损坏配置回退仍为只读,其他受众不继承默认工具权限。
2. 机器配置 preview/apply/readback 可把 profile 持久设置为 `trusted_owner`。
3. 新 Codex manager thread 的 app-server 请求携带 `danger-full-access`,Turn 提示词和托管
`AGENTS.md` 不再包含只读限制。
@@ -81,6 +86,6 @@ outbox,也不把管家 session 字段当成工作、请求或送达权威。
`restricted` 的动作提示。
7. 桌面和移动 Dashboard 显示有效 profile;配置损坏时显示回退状态。
8. 外部 audience 在没有既有 scoped grant 时继续 `read-only`,并显示真实降级状态。
-9. 已核验的原生本人管家私聊沿用显式机器授权,模式改变或服务重启保留 Session 与历史;
+9. 已核验的原生本人管家私聊沿用实际机器策略,模式改变或服务重启保留 Session 与历史;
复用健康宿主前重新核验绑定和 Provider 身份。其他 App、群聊和 `workspace_only` 资源
不继承该授权。
diff --git a/docs/reference/protocols/manager-evidence-and-continuity-v0.md b/docs/reference/protocols/manager-evidence-and-continuity-v0.md
index 41b6b270c3..12e586d9cb 100644
--- a/docs/reference/protocols/manager-evidence-and-continuity-v0.md
+++ b/docs/reference/protocols/manager-evidence-and-continuity-v0.md
@@ -82,7 +82,7 @@ never the inventory. Normal manager questions no longer silently use a limited
frontend projection; explicitly choosing status-only still uses that projection.
The manager channel's model and reasoning effort follow its executor. On the
-interactive CLI endpoint the defaults are `gpt-6-astra` with `high` reasoning; on
+interactive CLI endpoint the defaults are `gpt-6.1-sol` with `xhigh` reasoning; on
the managed host they are the managed execution profile
(`deepseek-official` / `deepseek-flash` / `high`). The machine's
`steward_executor` machine configuration decides the executor, the model and the
diff --git a/examples/loopx-steward-channel-binding-smoke.py b/examples/loopx-steward-channel-binding-smoke.py
index b0577d30c8..808a3124ac 100644
--- a/examples/loopx-steward-channel-binding-smoke.py
+++ b/examples/loopx-steward-channel-binding-smoke.py
@@ -79,7 +79,7 @@ def _assert_the_shipped_default_ignores_the_credential() -> dict[str, object]:
"the one shipped decision must be named, and it is not a credential branch",
)
_assert(
- without_credential["model"] == with_credential["model"] == "gpt-6-astra"
+ without_credential["model"] == with_credential["model"] == "gpt-6.1-sol"
and with_credential["model_source"] == MANAGER_MODEL_SOURCE_VENDOR_DEFAULT
and with_credential["execution_profile"] is None,
"a credential must not hand the CLI endpoint a managed model or profile",
@@ -98,7 +98,7 @@ def _assert_the_shipped_default_ignores_the_credential() -> dict[str, object]:
manager_model_config(
{CREDENTIAL_ENV: CREDENTIAL_VALUE, "LOOPX_MANAGER_MODEL": "fixture-model"}
)
- == {"model": "fixture-model", "reasoning_effort": "high"},
+ == {"model": "fixture-model", "reasoning_effort": "xhigh"},
"an explicit model override must win over the shipped default",
)
return {
diff --git a/examples/personal-workspace-browser/agent-model-selection.mjs b/examples/personal-workspace-browser/agent-model-selection.mjs
index a2678312a1..dc5cfd3220 100644
--- a/examples/personal-workspace-browser/agent-model-selection.mjs
+++ b/examples/personal-workspace-browser/agent-model-selection.mjs
@@ -6,10 +6,18 @@ export const agentModelSelectionScenario = {
id: "agent-model-selection",
async run({ browser, collectCoverage, url }) {
let createdCount = 0;
- let managerModelKnown = false;
const requests = [];
let discoveryCount = 0;
+ const managerBinding = {
+ schema_version: "manager_channel_binding_v0", executor_endpoint: "codex",
+ executor_endpoint_source: "explicit_config", executor_endpoint_default_reason: "",
+ executor_kind: "individual", model: "declared-manager-model", model_source: "executor_configuration",
+ reasoning_effort: "xhigh", selection_policy: "preferred", allocation_reason: "user_explicit",
+ configured_endpoint: "codex", eligible_endpoints: [], allocation_configuration_revision: "fixture-model-selection",
+ credential_env_var: "", operator_credential_configured: false, available: true, unavailable_reason: null,
+ };
const context = await openWorkspacePage(browser, url, {
+ apiOptions: { managerChannelBinding: managerBinding },
collectCoverage,
beforeGoto: async (_api, page) => {
await page.route("**/api/chat/agent-model-options", route => { discoveryCount += 1; return route.fulfill({ json: {
@@ -29,7 +37,7 @@ export const agentModelSelectionScenario = {
session.channel_id === channel && session.agent_id === (body.agent_id ?? "codex")) : null;
const session = existing ?? {
session_id: `model-session-${++createdCount}`, goal_id: body.goal_id || "loopx-manager", agent_id: body.agent_id ?? "codex",
- agent_model_id: body.context_kind === "manager" && !managerModelKnown ? null : body.agent_model_id ?? "runtime-default", agent_reasoning_effort: body.agent_reasoning_effort ?? "high", agent_service_tier: "priority", adapter_kind: "codex_app_server", channel_id: channel,
+ agent_model_id: body.agent_model_id ?? "runtime-default", agent_reasoning_effort: body.agent_reasoning_effort ?? "high", agent_service_tier: "priority", adapter_kind: "codex_app_server", channel_id: channel,
status: "ready", active_turn_id: null, last_error_code: null, resumable: true,
created_at: "2026-08-13T01:00:00Z", updated_at: "2026-08-13T01:00:00Z",
};
@@ -57,15 +65,18 @@ export const agentModelSelectionScenario = {
});
assert.deepEqual(layout, { title: true, runtime: true, fits: true }, "The manager title and runtime label remain readable beside known or unknown model readback");
};
- await page.locator(".personal-model-current").filter({hasText: "模型设置"}).waitFor();
+ const managerControl = page.getByRole("button", {name: "调整管家模型与思考深度", exact: true});
+ await managerControl.filter({hasText: managerBinding.model}).waitFor();
+ assert.equal(await page.locator(".personal-chat-model-menu").count(), 0, "The steward has one machine-owned model entry");
await page.setViewportSize({width: 390, height: 844});
await assertReadableHeader();
- assert.equal(discoveryCount, 0, "Unknown current facts do not start catalog discovery");
- managerModelKnown = true;
- await page.evaluate(() => fetch("/api/chat/sessions", { method: "POST", headers: {"Content-Type": "application/json"},
- body: JSON.stringify({ context_kind: "manager", agent_id: "codex", mode: "new" }) }));
+ assert.equal(discoveryCount, 0, "The steward binding does not start optional catalog discovery");
+ await managerControl.click();
+ assert.equal(await page.locator(".personal-settings-tabs").getByRole("button", {name: "管家", exact: true}).getAttribute("aria-current"), "page");
+ await page.getByRole("button", {name: "返回工作区", exact: true}).click();
await page.reload({waitUntil: "networkidle"});
- await page.locator(".personal-model-current").filter({hasText: "runtime-default"}).waitFor();
+ await managerControl.filter({hasText: managerBinding.model}).waitFor();
+ assert.equal(await page.locator(".personal-chat-model-menu").count(), 0, "Refresh retains the machine-owned steward entry");
await assertReadableHeader();
await page.setViewportSize({width: 1512, height: 982});
await assertReadableHeader();
diff --git a/examples/personal-workspace-browser/execution-chip.mjs b/examples/personal-workspace-browser/execution-chip.mjs
index 08e8b48547..29d89caaf7 100644
--- a/examples/personal-workspace-browser/execution-chip.mjs
+++ b/examples/personal-workspace-browser/execution-chip.mjs
@@ -112,64 +112,6 @@ async function chipText(page) {
return (await page.locator(".personal-execution-chip").innerText()).replace(/\s+/g, " ").trim();
}
-const pickerSelector = "div.personal-agent-select button.personal-select-trigger";
-
-async function readPickerLabel(page) {
- return (await page.locator(pickerSelector).innerText()).replace(/\s+/g, " ").trim();
-}
-
-// What the page resolved, phrased so a failed CI run is diagnosable without a
-// local reproduction: the declared endpoint, the adapters the page actually
-// received and the label it rendered are the three facts that decide the label.
-async function pickerResolution(page) {
- const capabilities = await page.evaluate(async () => {
- try {
- const response = await fetch("/api/chat/capabilities");
- const body = await response.json();
- return {
- status: response.status,
- declaredEndpoint: body.manager?.channel_binding?.executor_endpoint ?? null,
- adapters: (body.adapters ?? []).map(
- (adapter) => `${adapter.agent_id}:${adapter.available ? "available" : "unavailable"}`,
- ),
- };
- } catch (error) {
- return { status: "unavailable", declaredEndpoint: null, adapters: [], error: String(error) };
- }
- });
- return [
- `label=${await readPickerLabel(page)}`,
- `declared=${capabilities.declaredEndpoint ?? ""}`,
- `adapters=[${capabilities.adapters.join(", ")}]`,
- `capabilities=${capabilities.status}${capabilities.error ? ` (${capabilities.error})` : ""}`,
- ].join(" ");
-}
-
-// The picker resolves from the same capabilities response that carries the
-// channel binding, and the execution chip only renders once that binding
-// lands. A single read therefore asserts on a state the page never promised was
-// settled, and it can still hold the pre-fetch Codex fallback. Wait for the
-// declared value instead, and name what the page really resolved if the wait
-// runs out.
-async function waitForPickerLabel(page, settled, timeoutMs = 15_000) {
- const deadline = Date.now() + timeoutMs;
- let label = await readPickerLabel(page);
- while (!settled(label) && Date.now() < deadline) {
- await page.waitForTimeout(100);
- label = await readPickerLabel(page);
- }
- if (settled(label)) {
- return label;
- }
- const resolution = await pickerResolution(page);
- await page.screenshot({
- animations: "disabled",
- fullPage: false,
- path: resolve(outputDir, "execution-chip-picker-unresolved.png"),
- });
- throw new Error(`Chat runtime picker never settled: ${resolution}`);
-}
-
async function assertHairlineRow(page, maxHeight) {
const headerBox = await page.locator(".personal-channel-header").boundingBox();
const chip = page.locator(".personal-execution-chip");
@@ -289,6 +231,9 @@ export const executionChipScenario = {
path: resolve(outputDir, "execution-chip-dsh-budget-desktop.png") });
await managed.page.setViewportSize({ width: 390, height: 844 });
await assertHairlineRow(managed.page, 44);
+ if (!await managed.page.getByText("选择:锁定 · 由本机配置锁定", { exact: false }).isVisible()) {
+ throw new Error("Expanded runtime details were hidden by the narrow subtitle styles");
+ }
const budget = managed.page.locator(".personal-execution-chip-budget");
if (!await budget.isVisible()) throw new Error("Mobile header lost the output budget");
const box = await budget.boundingBox();
@@ -320,10 +265,8 @@ export const executionChipScenario = {
} finally { coverageEntries.push(...await english.close()); }
}
- // The chat-runtime picker has to report the same resolution as the chip: a
- // managed host declared for this machine is what will answer, so neither
- // the header trigger nor the composer may keep advertising the CLI the
- // machine merely happens to have installed.
+ // The machine-owned steward binding is the only executor/model control.
+ // A discovered CLI and stale browser preference cannot override that owner.
const managedHostAdapters = [{
adapter_kind: "deepseek_harness_segment",
agent_id: "dsh",
@@ -336,15 +279,22 @@ export const executionChipScenario = {
const stewardPicker = await openWorkspacePage(browser, url, {
apiOptions: { managerChannelBinding: selectedManagedBinding, runtimeAgents: managedHostAdapters },
collectCoverage,
+ beforeGoto: async (_api, page) => page.addInitScript(() => {
+ localStorage.setItem("loopx.personal-agent-selection.v1", JSON.stringify({ manager: "codex" }));
+ }),
});
try {
- const pickerLabel = await waitForPickerLabel(
- stewardPicker.page,
- (label) => label.includes("DeepSeek Harness (managed)"),
- );
- if (pickerLabel.includes("Codex")) {
- throw new Error(`Chat runtime picker advertised a discovered CLI as the steward: ${pickerLabel}`);
+ await stewardPicker.page.locator(".personal-execution-chip").waitFor({ state: "visible" });
+ if (!(await chipText(stewardPicker.page)).includes("dsh")) {
+ throw new Error("The steward chip lost its machine-owned endpoint");
}
+ if (await stewardPicker.page.getByRole("combobox", { name: "选择聊天 Runtime" }).count()
+ || await stewardPicker.page.locator(".personal-chat-model-menu").count()) {
+ throw new Error("The steward offered duplicate Goal executor/model controls");
+ }
+ const saved = await stewardPicker.page.evaluate(() =>
+ JSON.parse(localStorage.getItem("loopx.personal-agent-selection.v1") ?? "{}"));
+ if (saved.manager) throw new Error("A legacy browser executor still overrides steward settings");
if (await stewardPicker.page.locator(".personal-channel-composer > span").count()) {
throw new Error("Composer repeated the executor already identified by the runtime picker");
}
@@ -352,21 +302,6 @@ export const executionChipScenario = {
coverageEntries.push(...await stewardPicker.close());
}
- // A machine that declares no steward executor keeps the shipped default,
- // so the parity above is a resolution the control plane asked for and not
- // a new hard-coded preference.
- const undeclaredSteward = await openWorkspacePage(browser, url, {
- apiOptions: { runtimeAgents: managedHostAdapters },
- collectCoverage,
- });
- try {
- // The shipped default is the settled value a machine with no declared
- // steward executor must keep.
- await waitForPickerLabel(undeclaredSteward.page, (label) => label === "Chat Codex");
- } finally {
- coverageEntries.push(...await undeclaredSteward.close());
- }
-
// A managed host that cannot launch here names the missing fact, and never
// claims the channel lacks a transport it now has.
for (const [binding, expected] of [
@@ -477,7 +412,7 @@ export const executionChipScenario = {
}
return {
coverageEntries,
- note: "execution chip reports the selected executor, the credential it is billed to and the resolved model, names the steward channel's shipped default without claiming a credential branch, keeps a configured credential from moving it, keeps the chat-runtime picker on the executor the machine declares for the steward, names why a selected managed host cannot launch, and stays absent without a binding",
+ note: "execution chip reports the selected executor, the credential it is billed to and the resolved model, names the steward channel's shipped default without claiming a credential branch, keeps a configured credential from moving it, keeps the machine binding as the single steward executor/model control, names why a selected managed host cannot launch, and stays absent without a binding",
};
},
};
diff --git a/examples/personal-workspace-browser/steward-model-settings.mjs b/examples/personal-workspace-browser/steward-model-settings.mjs
index de2fb70ba0..54574c1c53 100644
--- a/examples/personal-workspace-browser/steward-model-settings.mjs
+++ b/examples/personal-workspace-browser/steward-model-settings.mjs
@@ -16,15 +16,21 @@ export const stewardModelSettingsScenario = {
let capabilityReads = 0;
const context = await openWorkspacePage(browser, url, {
apiOptions: { managerChannelBinding: binding }, collectCoverage,
- beforeGoto: async (_api, page) => page.on("request", request => {
- if (new URL(request.url()).pathname === "/api/chat/capabilities") capabilityReads += 1;
- }),
+ beforeGoto: async (_api, page) => {
+ await page.addInitScript(() => localStorage.setItem("loopx.personal-agent-selection.v1", JSON.stringify({ manager: "retired-endpoint" })));
+ page.on("request", request => {
+ if (new URL(request.url()).pathname === "/api/chat/capabilities") capabilityReads += 1;
+ });
+ },
});
const { api, page } = context;
try {
const modelControl = page.getByRole("button", { name: "调整管家模型与思考深度", exact: true });
await modelControl.waitFor();
if (!(await modelControl.innerText()).includes("xhigh")) throw new Error("Current binding effort is missing");
+ if (await page.getByRole("combobox", { name: "选择聊天 Runtime" }).count()) throw new Error("Steward exposed a second executor owner");
+ const savedAgent = await page.evaluate(() => JSON.parse(localStorage.getItem("loopx.personal-agent-selection.v1") ?? "{}").manager);
+ if (savedAgent) throw new Error("Legacy browser executor preference survived machine-owned settings");
await page.screenshot({ path: resolve(outputDir, "steward-model-entry.png"), animations: "disabled" });
await modelControl.focus();
await page.keyboard.press("Enter");
@@ -33,6 +39,13 @@ export const stewardModelSettingsScenario = {
if (await stewardTab.getAttribute("aria-current") !== "page") {
throw new Error("Steward must be the primary settings destination");
}
+ const scopeNote = page.locator(".personal-capability-scope-note");
+ await scopeNote.getByText("本机管家设置", { exact: true }).click();
+ const scopeText = await scopeNote.innerText();
+ for (const meaning of ["当前设备的管家", "Goal 对话和 Agent Loop 保留各自配置", "正在执行的回合沿用当前分配"]) {
+ if (!scopeText.includes(meaning)) throw new Error(`Steward settings lost their scope or adoption boundary: ${scopeText}`);
+ }
+ await scopeNote.getByText("本机管家设置", { exact: true }).click();
const capabilityList = page.locator(".personal-capability-list");
await capabilityList.getByRole("button", { name: "运行环境" }).waitFor();
if (await capabilityList.getByRole("button").count() !== 2) {
diff --git a/loopx/capabilities/manager_runtime/machine_profile.py b/loopx/capabilities/manager_runtime/machine_profile.py
index 4509ca64c9..522c1f33f2 100644
--- a/loopx/capabilities/manager_runtime/machine_profile.py
+++ b/loopx/capabilities/manager_runtime/machine_profile.py
@@ -17,6 +17,7 @@
MANAGER_RUNTIME_EFFECTIVE_SCHEMA = "manager_runtime_effective_profile_v0"
RESTRICTED_PROFILE = "restricted"
TRUSTED_OWNER_PROFILE = "trusted_owner"
+DEFAULT_MANAGER_RUNTIME_PROFILE = TRUSTED_OWNER_PROFILE
SUPPORTED_MANAGER_RUNTIME_PROFILES = frozenset(
{RESTRICTED_PROFILE, TRUSTED_OWNER_PROFILE}
)
@@ -51,7 +52,8 @@ def manager_runtime_machine_configuration_namespace() -> MachineConfigurationNam
title="Manager runtime",
description=(
"Selects the effective host-tool profile for owner manager conversations. "
- "The trusted profile is an explicit persistent machine grant; protected "
+ "Owner conversations use normal host tools by default. An explicit machine "
+ "choice can restrict them; protected "
"operations and external provider permissions remain separately governed."
),
documentation={
@@ -63,7 +65,7 @@ def manager_runtime_machine_configuration_namespace() -> MachineConfigurationNam
},
default_configuration={
"schema_version": MANAGER_RUNTIME_PROFILE_SCHEMA,
- "runtime_profile": RESTRICTED_PROFILE,
+ "runtime_profile": DEFAULT_MANAGER_RUNTIME_PROFILE,
},
)
@@ -75,7 +77,7 @@ def _projection(*, profile: str, source: str, revision: str) -> dict[str, Any]:
"runtime_profile": profile,
"source": source,
"configuration_revision": revision,
- "standing_grant": "machine_configuration" if trusted else "none",
+ "standing_grant": source if trusted else "none",
"sandbox": "danger-full-access" if trusted else "read-only",
"approval_policy": "never",
"tool_classes": (
@@ -100,7 +102,7 @@ def effective_manager_runtime_profile(
if machine_configuration is None:
return _projection(
- profile=RESTRICTED_PROFILE,
+ profile=DEFAULT_MANAGER_RUNTIME_PROFILE,
source="capability_default",
revision="absent",
)
@@ -119,7 +121,7 @@ def effective_manager_runtime_profile(
raw = namespaces.get("manager_runtime")
if raw is None:
return _projection(
- profile=RESTRICTED_PROFILE,
+ profile=DEFAULT_MANAGER_RUNTIME_PROFILE,
source="capability_default",
revision="absent",
)
@@ -147,7 +149,7 @@ def load_effective_manager_runtime_profile(
The machine choice is sufficient for the local owner conversation. Native
bindings may also supply a freshly checked Core owner-audience proof. This
- proof alone grants no host tools; the persistent machine grant is required.
+ proof alone grants no host tools; the effective machine profile still applies.
Other external audiences always remain restricted.
"""
diff --git a/loopx/chat_manager.py b/loopx/chat_manager.py
index 9d031cca65..a8f78f3325 100644
--- a/loopx/chat_manager.py
+++ b/loopx/chat_manager.py
@@ -246,7 +246,7 @@ def is_manager_channel(value: Any) -> bool:
}
MANAGER_MODEL_ENV_VAR = "LOOPX_MANAGER_MODEL"
-MANAGER_MODEL_DEFAULT = "gpt-6-astra"
+MANAGER_MODEL_DEFAULT = "gpt-6.1-sol"
MANAGER_MODEL_SOURCE_ENV_OVERRIDE = "env_override"
MANAGER_MODEL_SOURCE_VENDOR_DEFAULT = "vendor_default"
# The machine configuration can name the model too, so an operator can select
@@ -257,7 +257,7 @@ def is_manager_channel(value: Any) -> bool:
# two different managed models.
MANAGER_MODEL_SOURCE_MANAGED_PROFILE = "managed_execution_profile"
MANAGER_REASONING_EFFORT_ENV_VAR = "LOOPX_MANAGER_REASONING_EFFORT"
-MANAGER_REASONING_EFFORT_DEFAULT = "high"
+MANAGER_REASONING_EFFORT_DEFAULT = "xhigh"
MANAGER_REASONING_EFFORTS = REASONING_EFFORTS
def steward_machine_defaults(controller: Any) -> Mapping[str, Any] | None:
diff --git a/tests/capabilities/test_manager_runtime_profile.py b/tests/capabilities/test_manager_runtime_profile.py
index b15d916c67..d0b6b5b5b9 100644
--- a/tests/capabilities/test_manager_runtime_profile.py
+++ b/tests/capabilities/test_manager_runtime_profile.py
@@ -47,7 +47,7 @@ def _apply(runtime_root: Path, profile: str) -> None:
)
-def test_manager_runtime_profile_is_explicit_and_fail_closed() -> None:
+def test_manager_runtime_default_uses_host_tools_and_invalid_choices_are_rejected() -> None:
assert normalize_manager_runtime_profile(
{
"schema_version": "manager_runtime_profile_v0",
@@ -62,9 +62,10 @@ def test_manager_runtime_profile_is_explicit_and_fail_closed() -> None:
}
)
default = effective_manager_runtime_profile(None)
- assert default["runtime_profile"] == "restricted"
- assert default["sandbox"] == "read-only"
- assert default["standing_grant"] == "none"
+ assert default["runtime_profile"] == "trusted_owner"
+ assert default["sandbox"] == "danger-full-access"
+ assert default["standing_grant"] == "capability_default"
+ assert {"filesystem", "shell", "git", "web"} <= set(default["tool_classes"])
def test_trusted_owner_profile_roundtrips_through_machine_configuration(
@@ -84,8 +85,10 @@ def test_trusted_owner_profile_roundtrips_through_machine_configuration(
assert str(effective["configuration_revision"]).startswith("sha256:")
-def test_external_audience_does_not_inherit_owner_host_tools(tmp_path: Path) -> None:
- _apply(tmp_path, "trusted_owner")
+@pytest.mark.parametrize("configured", [False, True])
+def test_external_audience_does_not_inherit_owner_host_tools(tmp_path: Path, configured) -> None:
+ if configured:
+ _apply(tmp_path, "trusted_owner")
effective = load_effective_manager_runtime_profile(
tmp_path,
@@ -101,13 +104,16 @@ def test_external_audience_does_not_inherit_owner_host_tools(tmp_path: Path) ->
@pytest.mark.parametrize("profile", [None, "restricted", "trusted_owner"])
-def test_owner_audience_proof_does_not_replace_the_machine_grant(tmp_path: Path, profile) -> None:
+def test_owner_audience_uses_default_but_respects_an_explicit_restriction(tmp_path: Path, profile) -> None:
if profile is not None:
_apply(tmp_path, profile)
effective = load_effective_manager_runtime_profile(tmp_path,
channel_id="manager.external.native.fixture", owner_manager_audience=True)
- assert effective["runtime_profile"] == (profile or "restricted")
- assert effective["standing_grant"] == ("machine_configuration" if profile == "trusted_owner" else "none")
+ assert effective["runtime_profile"] == (profile or "trusted_owner")
+ assert effective["standing_grant"] == (
+ "capability_default" if profile is None
+ else "machine_configuration" if profile == "trusted_owner" else "none"
+ )
def test_unrelated_machine_configuration_does_not_change_manager_revision() -> None:
@@ -167,3 +173,16 @@ def test_invalid_stored_profile_falls_back_visibly_to_restricted(
assert effective["sandbox"] == "read-only"
assert effective["source"] == "invalid_configuration_fallback"
assert effective["repair"].startswith("Open machine capability settings")
+
+
+def test_explicit_restricted_choice_keeps_the_read_only_boundary(tmp_path: Path) -> None:
+ _apply(tmp_path, "restricted")
+ for channel, owner in [("manager", False), ("manager.external.owner", True)]:
+ effective = load_effective_manager_runtime_profile(
+ tmp_path, channel_id=channel, owner_manager_audience=owner,
+ )
+ assert effective["runtime_profile"] == "restricted"
+ assert effective["sandbox"] == "read-only"
+ assert effective["standing_grant"] == "none"
+ assert effective["tool_classes"] == ["loopx_core"]
+ assert effective["source"] == "machine_configuration"
diff --git a/tests/test_chat_executor_profile.py b/tests/test_chat_executor_profile.py
index 00fcba1f74..6f0679b179 100644
--- a/tests/test_chat_executor_profile.py
+++ b/tests/test_chat_executor_profile.py
@@ -19,7 +19,7 @@ def test_explicit_endpoint_selects_defaults_without_changing_overrides(endpoint)
expected = (
{"model": "managed-fixture-model", "reasoning_effort": "low"}
if endpoint == "dsh"
- else {"model": "gpt-6-astra", "reasoning_effort": "high"}
+ else {"model": "gpt-6.1-sol", "reasoning_effort": "xhigh"}
)
assert manager_model_config(environment, endpoint=endpoint) == expected
assert manager_model_config(
diff --git a/tests/test_chat_manager_context.py b/tests/test_chat_manager_context.py
index 3bda5f6df7..b85d1ce35b 100644
--- a/tests/test_chat_manager_context.py
+++ b/tests/test_chat_manager_context.py
@@ -54,8 +54,8 @@ def _apply_manager_runtime_profile(runtime_root, profile):
def test_manager_defaults_are_independent_of_worker_configuration():
assert manager_model_config({}) == {
- "model": "gpt-6-astra",
- "reasoning_effort": "high",
+ "model": "gpt-6.1-sol",
+ "reasoning_effort": "xhigh",
}
assert manager_model_config(
{"LOOPX_MANAGER_MODEL": "fixture-model", "LOOPX_MANAGER_REASONING_EFFORT": "low"}
@@ -1012,9 +1012,11 @@ def test_trusted_manager_profile_rejects_endpoint_that_cannot_enforce_it(
assert "Select the Codex Agent" in caught.value.gate["next_action"]
+@pytest.mark.parametrize("initial_profile,next_profile", [("restricted", "trusted_owner"), ("trusted_owner", "restricted")])
def test_manager_profile_change_rotates_healthy_upstream_without_losing_session(
- monkeypatch, tmp_path
+ monkeypatch, tmp_path, initial_profile, next_profile
):
+ _apply_manager_runtime_profile(tmp_path / "runtime", initial_profile)
store = ChatSessionStore(tmp_path / "runtime")
runtime = ChatRuntimeController(store=store, codex_bin="codex")
monkeypatch.setattr(
@@ -1045,9 +1047,10 @@ def start(**kwargs):
mode="new",
channel_id="manager",
)
- assert starts[0][0]["manager_runtime"]["runtime_profile"] == "restricted"
+ assert starts[0][0]["manager_runtime"]["runtime_profile"] == initial_profile
+ store.append_message(session["session_id"], role="user", text="Keep this conversation.")
- _apply_manager_runtime_profile(tmp_path / "runtime", "trusted_owner")
+ _apply_manager_runtime_profile(tmp_path / "runtime", next_profile)
restored = runtime._ensure_adapter(
session,
work_dir=tmp_path,
@@ -1057,9 +1060,13 @@ def start(**kwargs):
assert starts[0][1].closed is True
assert restored is starts[1][1]
assert starts[1][0]["resume_thread_id"] is None
- assert starts[1][0]["manager_runtime"]["runtime_profile"] == "trusted_owner"
+ assert starts[1][0]["manager_runtime"]["runtime_profile"] == next_profile
+ assert starts[1][0]["manager_runtime"]["sandbox"] == (
+ "danger-full-access" if next_profile == "trusted_owner" else "read-only"
+ )
updated = store.load_session(session["session_id"])
- assert updated["manager_runtime_profile"] == "trusted_owner"
+ assert updated["manager_runtime_profile"] == next_profile
+ assert [m["text"] for m in store.messages(session["session_id"])] == ["Keep this conversation."]
assert updated["upstream_thread_id"] == "upstream-1"
diff --git a/tests/test_manager_channel_binding.py b/tests/test_manager_channel_binding.py
index 1f48775c30..cb83438db1 100644
--- a/tests/test_manager_channel_binding.py
+++ b/tests/test_manager_channel_binding.py
@@ -120,7 +120,7 @@ def test_the_shipped_default_is_the_cli_endpoint_on_every_machine():
assert binding.get("output_token_budget") is None
assert binding["available"] is None
assert binding["unavailable_reason"] is None
- assert binding["model"] == "gpt-6-astra"
+ assert binding["model"] == "gpt-6.1-sol"
assert binding["model_source"] == MANAGER_MODEL_SOURCE_VENDOR_DEFAULT
@@ -151,7 +151,7 @@ def test_the_operator_credential_authenticates_without_selecting_the_executor():
# The model follows the executor the channel runs, so a configured
# credential cannot leave a managed model pointed at an individual CLI.
assert with_credential["executor_kind"] == "individual"
- assert with_credential["model"] == "gpt-6-astra"
+ assert with_credential["model"] == "gpt-6.1-sol"
assert with_credential["model_source"] == MANAGER_MODEL_SOURCE_VENDOR_DEFAULT
assert with_credential["execution_profile"] is None
assert with_credential.get("output_token_budget") is None
@@ -200,7 +200,7 @@ def test_an_explicit_endpoint_selection_reports_no_default_reason():
assert binding["executor_endpoint"] == "codex"
assert binding["executor_endpoint_source"] == MANAGER_ENDPOINT_SOURCE_EXPLICIT_CONFIG
assert binding["executor_endpoint_default_reason"] == ""
- assert binding["model"] == "gpt-6-astra"
+ assert binding["model"] == "gpt-6.1-sol"
assert binding["execution_profile"] is None
@@ -294,7 +294,7 @@ def test_a_machine_that_selects_only_an_executor_keeps_the_lower_layers(monkeypa
"executor_model": None,
"executor_reasoning_effort": None,
},
- ) == {"model": "gpt-6-astra", "reasoning_effort": "low"}
+ ) == {"model": "gpt-6.1-sol", "reasoning_effort": "low"}
# An unconfigured machine keeps resolving exactly as it did before the
# namespace existed, and an unread machine is named as such.
assert manager_channel_binding({"DEEPSEEK_API_KEY": "fixture"})[
@@ -362,7 +362,7 @@ def test_an_unknown_explicit_endpoint_makes_no_availability_claim():
assert binding["executor_kind"] == ""
assert binding["available"] is None
- assert binding["model"] == "gpt-6-astra"
+ assert binding["model"] == "gpt-6.1-sol"
def test_explicit_model_override_wins_on_either_endpoint():
@@ -374,9 +374,9 @@ def test_explicit_model_override_wins_on_either_endpoint():
assert manager_model_config(
{"DEEPSEEK_API_KEY": "fixture", "LOOPX_MANAGER_MODEL": "fixture-model"}
- ) == {"model": "fixture-model", "reasoning_effort": "high"}
+ ) == {"model": "fixture-model", "reasoning_effort": "xhigh"}
assert manager_model_config({"LOOPX_MANAGER_REASONING_EFFORT": "low"}) == {
- "model": "gpt-6-astra",
+ "model": "gpt-6.1-sol",
"reasoning_effort": "low",
}
# The managed effort is the same field the governed Turn surface resolves,
@@ -392,14 +392,14 @@ def test_explicit_model_override_wins_on_either_endpoint():
# the interactive endpoint on its own vendor model and effort.
assert manager_model_config(
{"DEEPSEEK_API_KEY": "fixture", "LOOPX_TURN_REASONING_EFFORT": "max"}
- ) == {"model": "gpt-6-astra", "reasoning_effort": "high"}
+ ) == {"model": "gpt-6.1-sol", "reasoning_effort": "xhigh"}
def test_manager_model_config_reads_the_process_environment(monkeypatch):
monkeypatch.delenv("LOOPX_MANAGER_MODEL", raising=False)
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
- assert manager_model_config()["model"] == "gpt-6-astra"
+ assert manager_model_config()["model"] == "gpt-6.1-sol"
def test_open_manager_session_resolves_the_endpoint_only_when_unset(tmp_path):
@@ -489,8 +489,8 @@ def open_session(self, **kwargs):
allocation = calls[-1]["manager_executor_allocation"]
assert allocation["allocation_reason"] == "flexible_availability_fallback"
assert allocation["eligible_endpoints"] == ["dsh", "codex"]
- assert allocation["model"] == "gpt-6-astra"
- assert allocation["reasoning_effort"] == "high"
+ assert allocation["model"] == "gpt-6.1-sol"
+ assert allocation["reasoning_effort"] == "xhigh"
with pytest.raises(CodexChatAgentError) as raised:
open_manager_session(
diff --git a/tests/test_native_steward_runtime.py b/tests/test_native_steward_runtime.py
index 76d0e03a05..8dcb6be091 100644
--- a/tests/test_native_steward_runtime.py
+++ b/tests/test_native_steward_runtime.py
@@ -1,4 +1,4 @@
-"""Private owner audience and explicit host grant meet at the native Session.
+"""Private owner audience and machine host policy meet at the native Session.
The real Core, configuration store and app-server RPC path are exercised with
synthetic provider/model fixtures, not live credentials or model acceptance.
@@ -34,7 +34,14 @@ def admit(name, text):
return store.load_session(row["session_id"])
first = admit("default", "Remember the owner's initial request.")
- assert first["manager_runtime_profile"] == "restricted"
+ first_readback = store.public_session(first)["manager_runtime"]
+ assert first_readback["runtime_profile"] == "trusted_owner"
+ assert first_readback["standing_grant"] == "capability_default"
+ apply_profile(store.root.parent, "restricted")
+ restricted = admit("restricted", "Respect the owner's explicit read-only choice.")
+ assert restricted["session_id"] == first["session_id"]
+ assert restricted["manager_runtime_sandbox"] == "read-only"
+ assert restricted["manager_runtime_standing_grant"] == "none"
apply_profile(store.root.parent, "trusted_owner")
trusted = admit("authorized", "Use the available tools for this authorized request.")
assert trusted["session_id"] == first["session_id"]
@@ -48,7 +55,9 @@ def admit(name, text):
assert followup["upstream_thread_id"] == trusted["upstream_thread_id"]
calls = [json.loads(line) for line in capture.read_text().splitlines()]
starts = [call["params"] for call in calls if call.get("method") == "thread/start"]
- assert [call["sandbox"] for call in starts] == ["read-only", "danger-full-access"]
+ assert [call["sandbox"] for call in starts] == [
+ "danger-full-access", "read-only", "danger-full-access",
+ ]
prompts = [call["params"]["input"][0]["text"] for call in calls if call.get("method") == "turn/start"]
assert "normal tools and skills" in prompts[-1]
assert "Remember the owner's initial request." in prompts[1]