Skip to content

Commit 72f53d8

Browse files
committed
test: preserve original operation profile without delegation MCP
Signed-off-by: huangruiteng <huangrt01@163.com>
1 parent cb23112 commit 72f53d8

2 files changed

Lines changed: 92 additions & 10 deletions

File tree

‎docs/reference/local-delegation.md‎

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ workspace、Codex Session 与持久 delegation operation。`delegation inspect`
110110
规划投影会读回例如 `gpt-5.6-sol@xhigh`,start/resume 也把同一配置送入原 Session。
111111
这不扩大 requester grant,也不把 profile 冒充验收或结果返回回执。
112112

113-
For a Codex binding, the delegation host also supplies one invocation-scoped
113+
By default, a Codex binding also supplies one invocation-scoped
114114
`loopx_delegation` stdio MCP server to every fresh or resumed worker Session.
115115
Its command pins the selected worker `agent_id`, workspace, Goal, registry,
116116
runtime and operator execution configuration before Codex starts. The model
117117
cannot select or rewrite those values. Codex receives the server through
118118
per-invocation configuration, so LoopX does not modify the user's global Codex
119-
MCP settings and a resumed worker keeps the same binding. The server is required
120-
for this managed worker route and its already identity-scoped tools are approved
121-
inside that route; failure to start the server rejects the Turn instead of
119+
MCP settings and a resumed worker keeps the same binding. When configured, the
120+
server and its already identity-scoped tools are approved inside that route;
121+
failure to start the server rejects the Turn instead of
122122
silently continuing without tools. The native tools are
123123
the collaboration and authorized delegation operations from that bound server;
124124
they do not add shell, Todo or external-action authority.
@@ -129,7 +129,7 @@ shell-only coordination. Both surfaces call the same `Delegations` service and
129129
preserve the same binding, operation and acceptance rules; the shell path is
130130
not a second control-plane implementation.
131131

132-
中文:Codex binding 会为每个新建或续接的 worker Session 注入一次调用范围内的
132+
中文:Codex binding 默认会为每个新建或续接的 worker Session 注入一次调用范围内的
133133
`loopx_delegation` stdio MCP server。启动前,host 已固定 worker `agent_id`、
134134
workspace、Goal、registry、runtime 与 operator execution configuration;模型不能
135135
选择或改写这些值。该配置不会修改用户的全局 Codex MCP 设置,也不会授予 shell、
@@ -726,6 +726,52 @@ home, binary, sandbox or effective MCP configuration, or retarget the Todo.
726726
Prepare the operation in this same managed session/profile. Existing delegation
727727
inspection and ordinary Turn admission still determine whether it can run.
728728

729+
If the original standalone operation Session was prepared **without** a
730+
delegation MCP server, its profile includes `mcp_server: null`. Preserve that
731+
exact profile using the existing operator-owned binding option:
732+
733+
```json
734+
{
735+
"host_args": [
736+
"--host", "codex-cli",
737+
"--codex-operation-tools",
738+
"--codex-model", "original-model",
739+
"--codex-reasoning-effort", "original-effort",
740+
"--codex-sandbox", "read-only",
741+
"--codex-mcp-server-json", "null"
742+
]
743+
}
744+
```
745+
746+
This is a fragment; replace the model/effort placeholders with their original
747+
values and retain the original executable, workspace, home and Agent/Todo
748+
grant as well. Binding `host_args` follow the default injected MCP
749+
option, so the real CLI parser uses the explicit JSON `null`. Do not infer
750+
absence by reading the first occurrence in argv. Omit this override for an
751+
original delegation-MCP profile: removing that server would also be drift.
752+
Inspect the original profile before enabling the collector; runtime
753+
qualification requires same-session/profile native acceptance, not just this
754+
configuration readback. No global Codex configuration is changed.
755+
The native `loopx_operation` tool remains bound to the original transport;
756+
`null` does not grant collaboration MCP tools, bypass ordinary receiver
757+
adoption/validation, or prove consumption, outcome or source delivery. A
758+
synthetic typed `wait` may yield native acceptance and a rejected delegation;
759+
that is not an accepted work result or an end-to-end operation loop.
760+
761+
中文:若原独立 operation Session 在没有 delegation MCP 时准备,profile 中的
762+
`mcp_server` 为 `null`,应在原 operator binding 的 `host_args` 显式设置
763+
`["--codex-mcp-server-json", "null"]`,保留 executable、workspace、home、原
764+
Agent/Todo grant 及模型/深度等其余字段。上例仅为片段,model/effort 占位符须替换
765+
为原值;binding 参数在默认注入
766+
参数之后,真实 CLI parser 采用后面的 JSON `null`,不能取 argv 第一个同名参数
767+
来判断生效配置。原 profile 使用 delegation MCP 时不要加此覆盖,删除 server
768+
同样属于漂移。启用 collector 前核对原 profile;运行资格必须由同 session/profile
769+
的原生接收回执证明,不能只凭配置读回。不改全局 Codex 配置。原生
770+
`loopx_operation` 工具保持原连接绑定,但 `null`
771+
不会授予 collaboration MCP 工具、绕过接收方采纳/验收,也不证明消费、结果或原
772+
来源送达;合成 typed `wait` 可同时产生原生接收和 delegation rejected,不代表
773+
工作完成或端到端操作闭环。
774+
729775
`loopx lark-inbox collector-plan` and `collector-status` expose
730776
`operation_callback_managed_wake_configured`, a configuration observation, not
731777
runtime qualification. An authenticated callback returns `managed_turn_wake`:

‎tests/test_local_delegation.py‎

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,12 @@ def wait(service, operation="analysis-1"):
149149
pytest.fail(str(service.read(operation)))
150150

151151

152-
def test_confirmed_wake_reaches_native_acceptance_through_real_delegation_and_turn(service):
152+
@pytest.mark.parametrize("mcp_profile", ["delegation-default", "explicit-null", "unmatched-null"])
153+
def test_confirmed_wake_reaches_native_acceptance_through_real_delegation_and_turn(service, mcp_profile):
153154
"""Real detached worker/CLI/Turn; synthetic native transport, no model/effect."""
154155
from examples import operation_action_fixtures as fixtures
155156
from loopx.chat_action_store import ChatActionStore
157+
from loopx.cli import build_parser
156158
from loopx.control_plane.collaboration.operation_wake import dispatch_confirmed_operation_wake
157159
from loopx.control_plane.turn_driver.codex_cli import load_codex_cli_session
158160
from loopx.control_plane.turn_driver.codex_operation_host import run_codex_operation_host, operation_tool_handler
@@ -161,22 +163,42 @@ def test_confirmed_wake_reaches_native_acceptance_through_real_delegation_and_tu
161163

162164
root, runner = service
163165
executable = root / "fixture-codex"
164-
executable.write_text(FAKE_SERVER)
166+
native_starts = root / "native-starts"
167+
startup_probe = (
168+
"from pathlib import Path\n"
169+
f"counter = Path({str(native_starts)!r})\n"
170+
"counter.write_text(str(int(counter.read_text()) + 1 if counter.exists() else 1))\n"
171+
)
172+
executable.write_text(FAKE_SERVER.replace('thread = "owned-app-server-thread"',
173+
startup_probe + 'thread = "owned-app-server-thread"'))
165174
executable.chmod(0o700)
166175
config = json.loads(runner.config.read_text())
167176
binding = config["bindings"][0]
168177
binding["host_args"] = ["--host", "codex-cli", "--codex-bin", str(executable),
169178
"--codex-operation-tools", "--codex-model", "test-model", "--codex-reasoning-effort", "xhigh"]
179+
if mcp_profile == "explicit-null":
180+
# Existing operator option, not a new profile or a worker-selected grant.
181+
binding["host_args"] += ["--codex-mcp-server-json", "null"]
170182
runner.config.write_text(json.dumps(config))
171183
argv = runner._execution_arguments(binding, "preparation")
172-
mcp_server = json.loads(argv[argv.index("--codex-mcp-server-json") + 1])
184+
parsed = build_parser().parse_args(["turn", "run-once", "--goal-id", runner.goal_id,
185+
"--agent-id", binding["agent_id"], "--todo-id", binding["todo_id"], *argv])
186+
if mcp_profile == "explicit-null":
187+
assert argv.count("--codex-mcp-server-json") == 2
188+
assert parsed.codex_mcp_server_json is None
189+
else:
190+
assert parsed.codex_mcp_server_json["name"] == "loopx_delegation"
191+
# Reproduce an original standalone operation Session: do NOT prepare it
192+
# with the injected delegation MCP just to make the later resume match.
193+
mcp_server = parsed.codex_mcp_server_json if mcp_profile == "delegation-default" else None
173194
lineage = {"goal_id": runner.goal_id, "agent_id": binding["agent_id"], "todo_id": binding["todo_id"]}
174195
request = _request()
175196
request["turn_envelope"].update(goal_id=runner.goal_id, agent_id=binding["agent_id"])
176197
request["turn_envelope"]["action"]["selected_todo"]["todo_id"] = binding["todo_id"]
177198
run_codex_operation_host(request, runtime_root=runner.root, registry_path=runner.registry,
178199
project=Path(binding["workspace"]), codex_bin=str(executable), model="test-model",
179200
reasoning_effort="xhigh", mcp_server=mcp_server, timeout_seconds=5)
201+
assert native_starts.read_text() == "1"
180202
session = load_codex_cli_session(runner.root, lineage=lineage)
181203
handler = operation_tool_handler(runtime_root=runner.root, registry_path=runner.registry,
182204
lineage=lineage, session_id=session["session_id"], profile_digest=session["operation_profile_digest"],
@@ -203,16 +225,30 @@ def test_confirmed_wake_reaches_native_acceptance_through_real_delegation_and_tu
203225
assert receipt["state"] == "delegation_requested", receipt
204226
result = wait(runner, receipt["operation_id"])
205227
stored = store.load(proposal["proposal_id"])
206-
assert stored["operation"]["host_start"] is not None, result
207-
assert stored["operation"]["host_start"]["route"]["thread_id"] == session["session_id"]
228+
resumed = load_codex_cli_session(runner.root, lineage=lineage)
229+
assert resumed["session_id"] == session["session_id"]
230+
assert resumed["operation_profile_digest"] == session["operation_profile_digest"]
208231
assert stored["operation"].get("agent_handoff") is None
209232
assert stored["operation"]["outcome"] is None
233+
# Omitting the original null override must still fail closed, rather than
234+
# changing/replacing the original Session to bypass the profile fence.
235+
if mcp_profile == "unmatched-null":
236+
assert result["status"] == "rejected", result
237+
assert stored["operation"].get("host_start") is None, result
238+
assert native_starts.read_text() == "1" # refused before native process start
239+
assert stored["status"] == confirmed["status"]
240+
assert stored["operation"]["confirmation"] == confirmed["operation"]["confirmation"]
241+
return
242+
assert native_starts.read_text() == "2"
243+
assert stored["operation"]["host_start"] is not None, result
244+
assert stored["operation"]["host_start"]["route"]["thread_id"] == session["session_id"]
210245
# Fixture deliberately waits: startup is not validated domain completion.
211246
assert result["status"] == "rejected"
212247
journal = json.loads(runner.path(receipt["operation_id"]).read_text())
213248
assert stored["operation"]["host_start"]["turn_key"] == journal["turn_key"]
214249
assert dispatch_confirmed_operation_wake(stored, runtime_root=runner.root,
215250
configuration=wake_config)["state"] == "existing_delegation"
251+
assert native_starts.read_text() == "2"
216252

217253

218254
def test_detached_result_reconnects_without_duplicate_execution(service):

0 commit comments

Comments
 (0)