diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md index 95cb10a56b..8ea3bca9f6 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.md @@ -347,6 +347,14 @@ validation and acceptance are reused. The profile is read back by the existing host owner; no new frontend configuration store or hidden default is added. Preflight reports an unpinned/unsupported configuration as unavailable. A valid argv is still runtime-unverified, not proof that a host or an operation ran. +The typed operation transport readback lists `supported_hosts` and +`supported_sandboxes`. An unsupported host returns +`select_supported_operation_host`; an unsupported sandbox returns +`select_supported_operation_sandbox`; missing model/effort returns +`correct_execution_profile`. Transport refusals do not suggest model-credential +environment variables. These are explicit recovery choices, not automatic host +selection or an execution grant. Ordinary host availability remains independent: +a launchable DSH model does not qualify the Codex-only operation connection. Use `workspace-write` only when the existing work grant requires it; `danger-full-access` is not accepted by this transport. diff --git a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md index a0b40f72da..e79a1ea722 100644 --- a/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md +++ b/docs/architecture/rfcs/human-confirmed-domain-operations-v0.zh-CN.md @@ -277,7 +277,13 @@ Goal instance,不改变 provider 权威,也不把旧操作移植到另一运 相同选项可用于 `turn run-once`。复用既有 delegation inspect/preflight、start、 准入、租约、session 续接、结果验证和验收。原 host owner 读回 profile; 不新增前端配置存储或隐藏默认。不固定或不受支持的配置在预检中标为 unavailable; -有效 argv 仍是 runtime-unverified,不能证明宿主或操作已运行。仅在现有工作授权 +有效 argv 仍是 runtime-unverified,不能证明宿主或操作已运行。 +typed operation transport 读回列出 `supported_hosts` 和 `supported_sandboxes`。 +不支持的宿主返回 `select_supported_operation_host`,不支持的 sandbox 返回 +`select_supported_operation_sandbox`,缺失模型/effort 返回 +`correct_execution_profile`。传输拒绝不再建议配置模型凭据环境变量。 +这些是显式恢复选项,不自动选择宿主或授予执行权限。普通宿主可用性独立保持: +DSH 模型可启动不能证明它拥有当前仅支持 Codex 的操作连接。仅在现有工作授权 确需时使用 `workspace-write`;此传输不接受 `danger-full-access`。 已准入宿主复用 `CodexChatAgentSession` app-server adapter,将不透明线程保存于 diff --git a/loopx/control_plane/turn_driver/host_binding.py b/loopx/control_plane/turn_driver/host_binding.py index 6a0be83f0a..a34f956c26 100644 --- a/loopx/control_plane/turn_driver/host_binding.py +++ b/loopx/control_plane/turn_driver/host_binding.py @@ -198,7 +198,7 @@ def _with_operation_transport( if observed["reason"]: binding["available"] = False binding["unavailable_reason"] = observed["reason"] - binding["unavailable_remediation"] = [REMEDY_CORRECT_EXECUTION_PROFILE] + binding["unavailable_remediation"] = observed["remediation"] return binding @@ -422,7 +422,13 @@ def managed_executor_unavailable_payload( if str(code) ], "remediation_host": INDIVIDUAL_TURN_HOST, - "remediation_env_vars": list(OPERATOR_CREDENTIAL_ENV_VARS), + # An operation transport refusal is not an instruction to configure + # model credentials. The existing typed transport owner supplies its + # remedies and supported hosts/sandboxes independently of launchability. + "remediation_env_vars": ( + [] if "operation_transport" in binding + else list(OPERATOR_CREDENTIAL_ENV_VARS) + ), } diff --git a/loopx/control_plane/work_items/operation_agent_handoff.ts b/loopx/control_plane/work_items/operation_agent_handoff.ts index aae367e96e..89d6c1c74a 100644 --- a/loopx/control_plane/work_items/operation_agent_handoff.ts +++ b/loopx/control_plane/work_items/operation_agent_handoff.ts @@ -67,14 +67,20 @@ export function resolveOperationSourceRoute(input: JsonObject): JsonObject { * Accept the CLI's provider-neutral effort vocabulary; actual model support * remains a host qualification, not something this preflight can establish. */ export function projectManagedOperationTransport(input: JsonObject): JsonObject { - const reason = input.host !== "codex-cli" ? "operation_transport_host_unsupported" - : !["read-only", "workspace-write"].includes(String(input.sandbox)) ? "operation_transport_sandbox_unsupported" + const supportedHosts = ["codex-cli"]; + const supportedSandboxes = ["read-only", "workspace-write"]; + const reason = typeof input.host !== "string" || !supportedHosts.includes(input.host) ? "operation_transport_host_unsupported" + : typeof input.sandbox !== "string" || !supportedSandboxes.includes(input.sandbox) ? "operation_transport_sandbox_unsupported" : typeof input.model !== "string" || !ID.test(input.model) || typeof input.reasoning_effort !== "string" || !["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"].includes(input.reasoning_effort) ? "operation_transport_profile_required" : null; - return {reason, transport: {schema_version: "loopx_operation_transport_v0", + const remediation = reason === "operation_transport_host_unsupported" ? ["select_supported_operation_host"] + : reason === "operation_transport_sandbox_unsupported" ? ["select_supported_operation_sandbox"] + : reason === "operation_transport_profile_required" ? ["correct_execution_profile"] : []; + return {reason, remediation, transport: {schema_version: "loopx_operation_transport_v0", kind: "owned_app_server", revision: "app-server-operation-tools-v0", + supported_hosts: supportedHosts, supported_sandboxes: supportedSandboxes, configuration_valid: reason === null, runtime_qualified: false, identity_source: "native_thread_turn_metadata", human_confirmation_required: true, first_consumption_required: true, source_conversation_is_executor: false}}; diff --git a/tests/control_plane/test_operation_transport_readback.py b/tests/control_plane/test_operation_transport_readback.py new file mode 100644 index 0000000000..f81685f04b --- /dev/null +++ b/tests/control_plane/test_operation_transport_readback.py @@ -0,0 +1,100 @@ +"""The original delegation CLI distinguishes transport from model readiness.""" + +import json + +import pytest + +from test_delegation_cli import cli +from test_local_delegation import service as delegation_service + +service = delegation_service + + +@pytest.mark.parametrize("host", [["codex-cli"], {"host": "codex-cli"}, None, True, 1]) +def test_supported_host_readback_never_coerces_an_invalid_host(host): + from loopx.control_plane.effect_runtime import effect_runtime_result + + result = effect_runtime_result("operation.managed_transport.project", { + "host": host, "sandbox": "read-only", "model": "test-model", + "reasoning_effort": "high", + }) + assert result["reason"] == "operation_transport_host_unsupported" + assert result["remediation"] == ["select_supported_operation_host"] + assert result["transport"]["configuration_valid"] is False + + +@pytest.mark.parametrize("sandbox", [["read-only"], ["workspace-write"], {"sandbox": "read-only"}, None, True, 1]) +def test_supported_sandbox_readback_never_coerces_an_invalid_sandbox(sandbox): + from loopx.control_plane.effect_runtime import effect_runtime_result + + result = effect_runtime_result("operation.managed_transport.project", { + "host": "codex-cli", "sandbox": sandbox, "model": "test-model", + "reasoning_effort": "high", + }) + assert result["reason"] == "operation_transport_sandbox_unsupported" + assert result["remediation"] == ["select_supported_operation_sandbox"] + assert result["transport"]["configuration_valid"] is False + + +@pytest.mark.parametrize( + ("options", "reason", "remedy"), + [ + ( + ["--host", "codex-cli", "--codex-model", "test-model", + "--codex-reasoning-effort", "high", "--codex-sandbox", "danger-full-access"], + "operation_transport_sandbox_unsupported", "select_supported_operation_sandbox", + ), + ( + ["--host", "codex-cli"], + "operation_transport_profile_required", "correct_execution_profile", + ), + ], +) +def test_original_cli_inspection_returns_typed_transport_recovery_without_effects( + service, options, reason, remedy, +): + root, runner = service + config = json.loads(runner.config.read_text()) + config["bindings"][0]["host_args"] = [*options, "--codex-operation-tools"] + runner.config.write_text(json.dumps(config)) + before = runner.config.read_bytes(), runner.registry.read_bytes() + + status, result = cli(runner, "inspect", "--binding-id", "analysis") + + assert status == 0, result + assert result["executor"]["host"] == options[1] + assert result["executor"]["available"] is False + assert result["executor"]["reason"] == reason + assert result["executor"]["unavailable_remediation"] == [remedy] + transport = result["executor"]["operation_transport"] + assert transport["supported_hosts"] == ["codex-cli"] + assert transport["supported_sandboxes"] == ["read-only", "workspace-write"] + assert transport["configuration_valid"] is False + assert transport["runtime_qualified"] is False + assert transport["human_confirmation_required"] is True + assert transport["first_consumption_required"] is True + assert not any(result["effects"].values()) + assert not (root / "host-started").exists() + assert before == (runner.config.read_bytes(), runner.registry.read_bytes()) + + +def test_cli_does_not_repurpose_codex_operation_flag_for_a_dsh_executor(service): + root, runner = service + config = json.loads(runner.config.read_text()) + config["bindings"][0]["host_args"] = [ + "--host", "dsh", "--dsh-runner", "fixture-runner.py", + "--dsh-provider", "deepseek-official", "--dsh-model", "deepseek-flash", + "--dsh-reasoning-effort", "high", "--codex-operation-tools", + ] + runner.config.write_text(json.dumps(config)) + before = runner.config.read_bytes(), runner.registry.read_bytes() + + status, result = cli(runner, "inspect", "--binding-id", "analysis") + + # The original argv owner rejects this before executor projection. Keep + # that boundary; direct binding readback does not qualify a DSH CLI route. + assert status == 1 + assert result["ok"] is False + assert "--codex-operation-tools requires the codex-cli host" in result["error"] + assert not (root / "host-started").exists() + assert before == (runner.config.read_bytes(), runner.registry.read_bytes()) diff --git a/tests/test_turn_managed_executor_binding.py b/tests/test_turn_managed_executor_binding.py index 475bb45cf8..ab9d336735 100644 --- a/tests/test_turn_managed_executor_binding.py +++ b/tests/test_turn_managed_executor_binding.py @@ -116,6 +116,7 @@ def test_owned_operation_transport_is_opt_in_and_read_back_by_the_existing_host_ blocked = managed_executor_binding_from_host_args(options, environ={}) assert blocked["available"] is False assert blocked["unavailable_reason"] == "operation_transport_profile_required" + assert blocked["unavailable_remediation"] == ["correct_execution_profile"] pinned = managed_executor_binding_from_host_args( [*options, "--codex-model", "test-model", "--codex-reasoning-effort", effort], environ={}, @@ -136,10 +137,51 @@ def test_operation_transport_never_disappears_or_falls_back_on_another_host(host ) assert observed["available"] is False assert observed["unavailable_reason"] == "operation_transport_host_unsupported" + assert observed["executor"] == host + assert observed["unavailable_remediation"] == ["select_supported_operation_host"] + assert observed["operation_transport"]["supported_hosts"] == ["codex-cli"] + assert observed["operation_transport"]["supported_sandboxes"] == ["read-only", "workspace-write"] assert observed["operation_transport"]["configuration_valid"] is False assert observed["operation_transport"]["runtime_qualified"] is False +def test_operation_transport_sandbox_refusal_is_not_a_model_or_credential_remedy(): + binding = managed_executor_binding( + "codex-cli", environ={}, codex_operation_tools=True, + codex_sandbox="danger-full-access", model="test-model", reasoning_effort="high", + ) + assert binding["available"] is False + assert binding["unavailable_reason"] == "operation_transport_sandbox_unsupported" + assert binding["unavailable_remediation"] == ["select_supported_operation_sandbox"] + payload = managed_executor_unavailable_payload( + {"managed_executor": binding}, execute=True, host_projection={"kind": "codex-cli"}, + ) + assert payload["remediation"] == binding["unavailable_remediation"] + assert payload["remediation_env_vars"] == [] + + +def test_configured_dsh_model_does_not_qualify_operation_transport_or_change_host(): + kwargs = dict( + environ={"DEEPSEEK_API_KEY": "fixture-token"}, module_probe=_RUNTIME, + provider="deepseek-official", model="deepseek-flash", reasoning_effort="high", + ) + ordinary = managed_executor_binding("dsh", **kwargs) + assert ordinary["available"] is True + assert "operation_transport" not in ordinary + blocked = managed_executor_binding("dsh", codex_operation_tools=True, **kwargs) + assert blocked["executor"] == ordinary["executor"] == "dsh" + assert blocked["execution_profile"] == ordinary["execution_profile"] + assert blocked["runtime_probe"] == ordinary["runtime_probe"] + assert blocked["unavailable_remediation"] == ["select_supported_operation_host"] + payload = managed_executor_unavailable_payload( + {"managed_executor": blocked}, execute=True, host_projection={"kind": "dsh"}, + ) + assert payload["status"] == "unavailable" + assert payload["host"] == {"kind": "dsh"} + assert payload["remediation_env_vars"] == [] + assert "fixture-token" not in json.dumps([ordinary, blocked, payload]) + + def test_managed_executor_reports_the_operator_credential_and_endpoint(): binding = managed_executor_binding( "dsh",