Skip to content

feat(turn): name the operator-reachable exits in a managed executor refusal - #4477

Merged
huangruiteng merged 1 commit into
mainfrom
codex/managed-refusal-remediation
Sep 15, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/managed-refusal-remediation

Conversation

@huangruiteng

Copy link
Copy Markdown
Collaborator

What changes

A managed Turn that cannot launch already fails closed with one typed
unavailable_reason. That names the missing fact but not the way out, so an
operator on a fresh machine has to guess that DEEPSEEK_API_KEY or
--host codex-cli is the exit. This change makes the same refusal actionable
without changing anything it selects.

  • managed_executor_binding gains unavailable_remediation: typed codes naming
    the operator-reachable exits (configure_operator_credential,
    configure_dsh_runtime, correct_execution_profile, select_individual_host).
    It is [] for every launchable or non-managed executor, so the field is read
    the same way in both states.
  • The public refusal payload repeats them as remediation and adds the concrete
    remediation_host (codex-cli) and remediation_env_vars
    (DEEPSEEK_API_KEY), so no caller re-derives them from the reason string.
  • managed_executor_remediation_projection projects that entry, and contributes
    nothing when no exit was named, so a launchable payload stays byte-identical.
  • Protocol docs list the codes and state the boundary: naming an exit selects
    nothing — acting on it is still an explicit credential, profile, or --host
    change.

This is deliberately not the closed PR #4454: the shipped Turn host default
stays environment-independent and an uncredentialed machine keeps failing
closed. Only the readback becomes actionable.

Boundaries

  • Default-off parity: available, unavailable_reason, and the fail-closed
    path are unchanged; the new field is additive and empty on every happy path.
  • No new selection, endpoint, or model authority. operator_credential.py is
    untouched; the new codes quote facts the binding already reports.
  • remediation_env_vars reports names only, never values.

…efusal

Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
@huangruiteng

Copy link
Copy Markdown
Collaborator Author

Validation before merge (self-review; author-owned PR, GitHub blocks formal self-approval)

Changed surfaces (6 files, +165/−2): loopx/control_plane/turn_driver/host_binding.py
(the typed codes, the binding field, the refusal payload, the projection helper),
loopx/control_plane/turn_driver/executor.py (surfaces the remediation entry),
loopx/semantics/inventory_v0.json (regenerated), tests/test_turn_managed_executor_binding.py,
examples/loopx-turn-managed-executor-binding-smoke.py,
docs/reference/protocols/loopx-turn-v0.md.

Risk profile: additive readback on the governed Turn control path. No selection,
endpoint, model, journal, quota, or scheduler behavior changes; available and
unavailable_reason are untouched, and the projection contributes nothing when no
exit was named, so a launchable payload is byte-identical.

Checks run

check result
loopx canary premerge --from-git-diff (tier standard) gate passed, merge_gate_passed: true, self_merge_allowed: true, 4 direct + 11 selected checks, 0 failures, 0 manual holds
canary catalog-plan / risk-profile (full-public) / public-boundary runs ok, 0 failures each
pytest tests/test_loopx_turn_executor.py tests/test_turn_managed_executor_binding.py tests/test_turn_default_host_binding.py tests/test_loopx_turn_managed_step.py 123 passed
examples/loopx-turn-managed-executor-binding-smoke.py passed, including new assertions that the refusal names the credential env var and codex-cli
examples/loopx-turn-managed-default-flow-smoke.py passed
examples/docs-governance-smoke.py ok
scripts/generate_semantic_inventory.py --check up to date (4 new named string constants)
ruff check on every changed Python file passed (4 pre-existing F401s elsewhere in loopx/ are untouched)

Failures / skips / manual holds: none. Not covered: no provider call is made,
so this proves the readback and the fail-closed path, not a live endpoint; and no
batch pytest tests/ -k turn run completed in this turn (it hung on unrelated slow
integration tests and was killed) — the four Turn test modules above were run
individually instead.

Why the coverage is enough: the change is a readback field set with no control
flow in the happy path. The new tests pin all four exit codes, the empty-list parity
case, the concrete remediation_host/remediation_env_vars values, and the
credential-name-only boundary; the two public smokes re-assert the fail-closed
verdict and its zero effects. Canary's control-plane, catalog, risk-profile, and
public-boundary suites cover the touched surface.

English: additive, non-selecting readback on the managed Turn refusal path; canary
premerge gate passed with 0 failures and self_merge_allowed: true; 123 focused Turn
tests plus two public smokes and the docs governance smoke pass; semantic inventory
regenerated and up to date; no manual holds and no uncovered risky surface.

@huangruiteng
huangruiteng merged commit c361dc2 into main Sep 15, 2026
21 of 22 checks passed
@huangruiteng
huangruiteng deleted the codex/managed-refusal-remediation branch September 15, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant