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
27 changes: 19 additions & 8 deletions docs/architecture/rfcs/manager-runtime-profile-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ read-only.
not change.
- Codex is currently the only endpoint that enforces `trusted_owner`. Other endpoints fail with
an actionable typed error instead of pretending to provide the selected profile.
- `trusted_owner` currently applies only to the private owner-manager conversation. An external
audience, including a Lark group, is a separate trust boundary and resolves the same machine
choice to `restricted` until an existing audience/resource grant can be verified.
- `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.
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
Expand All @@ -55,18 +58,23 @@ without an unnecessary restart.

Dashboard shows both machine configuration and current Session readback. CLI/managed Turn,
Dashboard, and Lark all use the same manager runtime controller. Lark remains an entry point and
projection of that Session; it does not own a separate profile or permission state, but an external
audience currently degrades visibly to `restricted`. Future Lark host-tool access must reuse an
existing audience/resource authority instead of adding a manager-specific ACL here.
projection of that Session; it does not own a separate profile or permission state. Native
owner-private bindings reuse Core's fresh audience/resource verification before starting or
reusing a host; no channel-name inference, persisted proof or manager-specific ACL replaces it.
A revoked binding or changed App/owner stops admission.

This slice implements only the private-owner M1 journey in
[capable-manager-semantic-handoff-v0](capable-manager-semantic-handoff-v0.md), targeting A1–A3/A12.
It does not implement the M2 collaboration request, the M3 outbox, or treat manager Session fields
as work, request, or delivery authority.

### Implementation and successor (2026-09-16)
### Implementation and successor

`43d362532` contains the machine profile, controller integration and focused tests; passing them here is not deployment or full M1 qualification. The `restricted` default, Codex-only private `trusted_owner` and external-audience downgrade remain. Selecting DSH does not inherit that capable profile. Follow [roadmap](loopx-overall-roadmap-v0.md) R2 for actual tool/session/continued-execution and settings readback, without a second machine configuration.
`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
delivery. Follow [roadmap](loopx-overall-roadmap-v0.md) R2 for those remaining qualifications.

### Acceptance

Expand All @@ -83,3 +91,6 @@ as work, request, or delivery authority.
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
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.
22 changes: 15 additions & 7 deletions docs/architecture/rfcs/manager-runtime-profile-v0.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的
外部 provider 权限、受众边界和 LoopX durable state owner 不被改写。
- 当前只有 Codex endpoint 能执行 `trusted_owner`。选择其他 endpoint 时必须返回可恢复的
typed error,不能把受限执行伪装成已开放。
- `trusted_owner` 当前只对私有 Owner 管家会话生效。外部 audience(包括 Lark 群)是独立
信任边界;在既有的 audience/resource grant 能被核验前,同一机器配置在那里仍解析为
`restricted`,不能仅凭“同一管家”继承宿主资源权限。
- `trusted_owner` 适用于本地私有 Owner 管家和由既有 Core conversation owner 重新核验的
原生本人管家私聊。Core 核验 App、Owner 身份、私聊来源、当前资源授权和冻结的 Session
身份;受众证明本身不授予工具,仍须同一份显式机器授权。其他外部受众(包括 Lark 群)
和 `workspace_only` 资源继续受限;个人助手保留独立的工作区授权。

机器配置沿用现有 capability workbench 的 `preview -> apply -> readback` 流程,不建立第二份
配置源。配置缺失时安全回退 `restricted`;配置损坏时也回退,并在能力投影中显示
Expand All @@ -47,16 +48,20 @@ LoopX 管家最初只有受限的规划会话:它可以读取 LoopX 提供的

Dashboard 同时显示机器配置和当前会话 readback。CLI/managed Turn、Dashboard 与 Lark
继续调用同一个 manager runtime controller;Lark 是同源会话的入口和投影,不拥有独立
profile 或权限状态,但当前外部 audience 会明确降级为 `restricted`。后续若开放 Lark
宿主工具,必须复用已有的 audience/resource authority,不在这里新增管家 ACL。
profile 或权限状态。原生本人私聊在启动或复用宿主前重新核验 Core 的受众与资源授权;
不依据 channel 名字、持久化证明或新管家 ACL 推断权限。绑定撤销或 App/Owner 改变时
停止准入。

本切片只实现 [capable-manager-semantic-handoff-v0](capable-manager-semantic-handoff-v0.zh-CN.md)
的 M1 私有 Owner 旅程,目标验收为 A1–A3/A12。它不实现 M2 collaboration request、M3
outbox,也不把管家 session 字段当成工作、请求或送达权威。

### 实现与后继(2026-09-16)
### 实现与后继

`43d362532` 已有 machine profile、controller 集成与聚焦测试;本次复跑通过不等于本机部署或完整 M1 资格。`restricted` 默认、仅 Codex 支持私人 `trusted_owner` 及外部受众降级保持。DSH 通道选择不继承该强能力 profile。按[统一路线](loopx-overall-roadmap-v0.zh-CN.md) R2 补真实工具/会话/连续执行与设置读回;不得新建第二份机器配置。
`43d362532` 引入机器模式。原生本人管家私聊在打开、续接和授权范围刷新时,将 Core
重新核验的 Owner 受众证明传给既有模式。安装默认仍为 `restricted`;DSH 通道选择不继承
该强能力模式。源码与 RPC 测试不证明已部署的真实模型工作或桌面通知送达;按
[统一路线](loopx-overall-roadmap-v0.zh-CN.md) R2 完成这些剩余验收。

### 验收

Expand All @@ -70,3 +75,6 @@ outbox,也不把管家 session 字段当成工作、请求或送达权威。
`restricted` 的动作提示。
7. 桌面和移动 Dashboard 显示有效 profile;配置损坏时显示回退状态。
8. 外部 audience 在没有既有 scoped grant 时继续 `read-only`,并显示真实降级状态。
9. 已核验的原生本人管家私聊沿用显式机器授权,模式改变或服务重启保留 Session 与历史;
复用健康宿主前重新核验绑定和 Provider 身份。其他 App、群聊和 `workspace_only` 资源
不继承该授权。
2 changes: 2 additions & 0 deletions loopx/capabilities/manager_runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .machine_profile import (
MANAGER_RUNTIME_PROFILE_SCHEMA,
effective_manager_runtime_profile,
load_bound_manager_runtime_profile,
load_effective_manager_runtime_profile,
manager_runtime_capability_projection,
manager_runtime_machine_configuration_namespace,
Expand All @@ -13,6 +14,7 @@
__all__ = [
"MANAGER_RUNTIME_PROFILE_SCHEMA",
"effective_manager_runtime_profile",
"load_bound_manager_runtime_profile",
"load_effective_manager_runtime_profile",
"manager_runtime_capability_projection",
"manager_runtime_machine_configuration_namespace",
Expand Down
34 changes: 30 additions & 4 deletions loopx/capabilities/manager_runtime/machine_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

from collections.abc import Mapping
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any

from ..machine_configuration.contract import (
MACHINE_CONFIGURATION_SCHEMA,
MachineConfigurationNamespace,
machine_configuration_revision,
)

if TYPE_CHECKING:
from ..native_chat.conversation_bindings import ChatConversationBindings

MANAGER_RUNTIME_PROFILE_SCHEMA = "manager_runtime_profile_v0"
MANAGER_RUNTIME_EFFECTIVE_SCHEMA = "manager_runtime_effective_profile_v0"
RESTRICTED_PROFILE = "restricted"
Expand Down Expand Up @@ -138,12 +141,14 @@ def load_effective_manager_runtime_profile(
runtime_root: Path,
*,
channel_id: str = "manager",
owner_manager_audience: bool = False,
) -> dict[str, Any]:
"""Read the selected profile for one audience-bound manager channel.

The machine choice is sufficient for the private owner conversation. An
external audience is a different trust boundary and cannot inherit broad
host-tool access until an existing audience/resource grant can be checked.
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.
Other external audiences always remain restricted.
"""

from ..machine_configuration.store import read_stored_machine_configuration
Expand All @@ -159,6 +164,7 @@ def load_effective_manager_runtime_profile(
}
if (
channel_id != "manager"
and owner_manager_audience is not True
and effective["runtime_profile"] == TRUSTED_OWNER_PROFILE
):
return {
Expand All @@ -183,6 +189,26 @@ def load_effective_manager_runtime_profile(
}


def load_bound_manager_runtime_profile(
runtime_root: Path,
*,
channel_id: str,
bindings: ChatConversationBindings | None,
steward_context: dict[str, Any] | None,
) -> dict[str, Any]:
"""Compose fresh Core audience authority with the machine-owned host grant."""

owner_manager_audience = False
if steward_context is not None:
if bindings is None:
raise ValueError("bound manager authority is unavailable")
owner_manager_audience = bindings.manager_runtime_owner(steward_context, channel_id)
return load_effective_manager_runtime_profile(
runtime_root, channel_id=channel_id,
owner_manager_audience=owner_manager_audience,
)


def manager_runtime_session_fields(profile: Mapping[str, Any]) -> dict[str, Any]:
"""Project the effective profile fields persisted with a manager session."""

Expand Down
7 changes: 7 additions & 0 deletions loopx/capabilities/native_chat/conversation_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ def session_context(self, saved: dict[str, Any]) -> dict[str, Any]:
return self.resolve(binding_id=saved["binding_id"], source_ref=saved["source_ref"],
sender_ref=saved["operator_ref"], private_human_message=True, session_context=saved)

def manager_runtime_owner(self, saved: dict[str, Any], channel_id: str) -> bool:
"""Fresh Core audience proof; saved Session text is never authority."""
selected = self.session_context(saved)
if selected["channel_id"] != channel_id:
raise ValueError("bound manager audience changed")
return selected["owner_manager_audience"] is True

def steward_scope(self, session: dict[str, Any]) -> list[str] | None:
saved = session.get("steward_context")
if not isinstance(saved, dict):
Expand Down
3 changes: 2 additions & 1 deletion loopx/chat_coordination.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ def prepare_turn_context(controller, adapter, session, turn_id, event_sink, *, s
if controller.adapters.get(session_id) is adapter:
controller.adapters.pop(session_id, None)
manager_runtime = controller.manager_runtime_profile(
str(session.get("channel_id") or "manager")
str(session.get("channel_id") or "manager"),
steward_context=session.get("steward_context"),
)
adapter = controller._start_adapter(
agent_id=str(session["agent_id"]),
Expand Down
16 changes: 9 additions & 7 deletions loopx/chat_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .capabilities.native_chat import codex_context, project_context as project_context_policy
from .control_plane.collaboration import conversation_scope
from .capabilities.manager_runtime import (
load_effective_manager_runtime_profile, manager_runtime_session_fields,
load_bound_manager_runtime_profile, manager_runtime_session_fields,
)
from .capabilities.manager_context.team_plan import (
TeamPlanProjector,
Expand Down Expand Up @@ -341,11 +341,12 @@ def __init__(
self.team_plan_projector: TeamPlanProjector | None = None

def manager_runtime_profile(
self, channel_id: str = "manager"
self, channel_id: str = "manager", *,
steward_context: dict[str, Any] | None = None,
) -> dict[str, Any]:
return load_effective_manager_runtime_profile(
self.store.root.parent,
channel_id=channel_id,
return load_bound_manager_runtime_profile(
self.store.root.parent, channel_id=channel_id,
bindings=self.project_contexts.conversation_bindings, steward_context=steward_context,
)

def steward_executor_defaults(self) -> dict[str, Any]:
Expand Down Expand Up @@ -606,7 +607,7 @@ def open_session(
if project_context is not None:
project_context_policy.validate_project_executor_scope(agent_id, project_context, self.capabilities, capability)
manager_runtime = (
self.manager_runtime_profile(selected_channel)
self.manager_runtime_profile(selected_channel, steward_context=steward_context)
if is_manager_channel(selected_channel)
else None
)
Expand Down Expand Up @@ -755,7 +756,8 @@ def _ensure_adapter_locked(
project_context_policy.validate_project_executor_scope(str(session["agent_id"]), session["project_context"], self.capabilities)
work_dir, objective = context["project"], context["objective"]
manager_runtime = (
self.manager_runtime_profile(str(session.get("channel_id") or "manager"))
self.manager_runtime_profile(str(session.get("channel_id") or "manager"),
steward_context=session.get("steward_context"))
if is_manager_channel(session.get("channel_id"))
else None
)
Expand Down
7 changes: 6 additions & 1 deletion loopx/control_plane/collaboration/conversation_binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,12 @@ export function resolveBoundConversation(params: JsonObject): JsonObject {
: JSON.stringify(saved) === JSON.stringify(context);
if (!matches) throw new EffectRuntimeRequestError("bound Session context changed");
}
return {binding: row, context, channel_id: row.context_kind === "steward"
// This is an audience proof, not a host-tool grant. Only the independently
// verified owner in this private source may inherit an explicit machine
// owner grant. Portfolio scope and project write grants do not imply it.
return {binding: row, context, owner_manager_audience: row.context_kind === "steward"
&& projects[0].filesystem_scope !== "workspace_only",
channel_id: row.context_kind === "steward"
? `manager.external.native.${id}.${source}` : `project.external.${id}.${source}`};
}

Expand Down
2 changes: 1 addition & 1 deletion loopx/semantics/project_registry_io_manifest_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@
},
{
"site": "loopx/capabilities/native_chat/conversation_bindings.py::<module>.ChatConversationBindings.agent_observation::codec_read:load_registry#1",
"line": 202,
"line": 209,
"column": 20,
"kind": "codec_read",
"api": "load_registry",
Expand Down
10 changes: 10 additions & 0 deletions tests/capabilities/test_manager_runtime_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ def test_external_audience_does_not_inherit_owner_host_tools(tmp_path: Path) ->
assert effective["source"] == "external_audience_boundary"


@pytest.mark.parametrize("profile", [None, "restricted", "trusted_owner"])
def test_owner_audience_proof_does_not_replace_the_machine_grant(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")


def test_unrelated_machine_configuration_does_not_change_manager_revision() -> None:
before = effective_manager_runtime_profile(
{
Expand Down
17 changes: 17 additions & 0 deletions tests/control_plane_ts/conversation_binding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ const current = {schema_version: "loopx_chat_conversation_bindings_v0", revision
const request = {current, expected_revision: 0, operation: "configure", binding: row, observation,
available_projects: [project]};

test("only a current verified private steward binding proves the machine owner audience", () => {
const steward = {...row, context_kind: "steward", grant: "portfolio_read", goal_ids: []};
const next = planConversationBinding({...request, binding: steward}).state;
const use = {current: next, binding_id: row.binding_id, source_ref: "e".repeat(24),
sender_ref: row.operator_ref, private_human_message: true, observation, available_projects: [project]};
assert.equal(resolveBoundConversation(use).owner_manager_audience, true);
assert.equal(resolveBoundConversation({...use,
available_projects: [{...project, filesystem_scope: "workspace_only"}]}).owner_manager_audience, false);
const projectState = planConversationBinding(request).state;
assert.equal(resolveBoundConversation({...use, current: projectState}).owner_manager_audience, false);
for (const denied of [{private_human_message: false}, {sender_ref: "f".repeat(24)},
{observation: {...observation, provider_ref: "f".repeat(24)}},
{observation: {...observation, verified: false}}, {available_projects: []}, {current}]) {
assert.throws(() => resolveBoundConversation({...use, ...denied}));
}
});

test("images use the managed conversation and never disappear into commands or an attached host", () => {
const request = {request_ref: "e".repeat(24), command: null};
assert.equal(planBoundConversationRequest({request, current_session: null, attachment_count: 1}).operation, "admit_turn");
Expand Down
Loading
Loading