Skip to content
Closed
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
32 changes: 31 additions & 1 deletion docs/integrations/deepseek-harness-connector.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ LoopX quota should-run
- A `deepseek-harness` agent type in LoopX onboarding so users can request the
exact host instead of the generic `other-agent`.
- Optional dependency `loopx[deepseek-harness]` for the validated
`deepseek-harness-sdk==0.1.2a3` Python client.
`deepseek-harness-sdk==0.1.5rc1` Python client.

## Install

Expand All @@ -39,6 +39,14 @@ Install LoopX's optional DeepSeek Harness extra:
python -m pip install 'loopx[deepseek-harness]'
```

The pin tracks the newest published dsh release channel rather than an
unreleased tag: `0.1.5rc1` for the PyPI SDK/runtime wheels and `0.1.5-rc.1` for
the npm `@deepseek-ai/dsh` `latest` tag. Upstream also publishes newer
`next`/`alpha` tags that are not the released channel. This release keeps the
Python client surface of the previously pinned `0.1.2a3` and moves the bundled
dsh runtime; LoopX selects the SDK's default `sdk` profile unless the operator
supplies an explicit cordis composition.

The DeepSeek Harness SDK spawns the bundled `dsh-jsonrpc-agent` runtime. It
uses the explicit adapter configuration plus normal provider environment
variables:
Expand All @@ -58,6 +66,28 @@ appropriate. See the
[DeepSeek Harness Python SDK reference](https://github.com/deepseek-ai/deepseek-harness/blob/master/python/sdk/README.md)
for runtime selection and configuration.

## Default Host Selection

`loopx turn plan` and `loopx turn run-once` bind their default `--host` to the
operator's own provider credential rather than to the harness alone:

| Operator configuration | Default `--host` |
| --- | --- |
| `DEEPSEEK_API_KEY` is set to a non-empty value | `dsh` |
| no such credential is configured | `codex-cli` |

A machine with an operator-supplied model credential therefore runs governed
Turns against that endpoint instead of an individual CLI subscription, and a
machine without one keeps the Codex CLI host. Surrounding whitespace does not
count as a configured credential, so an exported-but-empty variable stays on
the `codex-cli` default.

`DEEPSEEK_BASE_URL` chooses the endpoint the credential is used against; on its
own it does not change the default host. An explicit `--host` always wins, and
the resolved value is what `loopx turn plan` reports back. `loopx turn run-once`
accepts the `codex-cli`, `dsh`, and `generic-cli` hosts it ships adapters for;
`loopx turn plan` additionally accepts the planning-only `claude-code` host.

## Onboard

```bash
Expand Down
2 changes: 2 additions & 0 deletions examples/loopx-turn-path-delta-acceptance-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def _run_turn(
"json",
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
GOAL_ID,
"--agent-id",
Expand Down
15 changes: 12 additions & 3 deletions loopx/cli_commands/turn_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
import argparse
from collections.abc import Callable

from ..control_plane.turn_driver.host_binding import resolve_default_turn_host
from ..paths import default_public_scan_root

# Explicit host choices stay per-command: planning may name any host the Turn
# driver routes, while run-once only ships built-in adapters for these three.
PLANNED_TURN_HOST_CHOICES = ["codex-cli", "claude-code", "dsh", "generic-cli"]
RUN_ONCE_TURN_HOST_CHOICES = ["codex-cli", "dsh", "generic-cli"]

AddFormat = Callable[[argparse.ArgumentParser], None]

Expand Down Expand Up @@ -42,7 +47,11 @@ def register_turn_commands(
help="Build one typed read-only host decision without launching or writing.",
)
add_subcommand_format(plan)
_add_turn_decision_arguments(plan, default_host="codex-cli")
_add_turn_decision_arguments(
plan,
default_host=resolve_default_turn_host(),
host_choices=list(PLANNED_TURN_HOST_CHOICES),
)
plan.add_argument(
"--include-transaction-detail",
action="store_true",
Expand Down Expand Up @@ -84,8 +93,8 @@ def register_turn_commands(
add_subcommand_format(run_once)
_add_turn_decision_arguments(
run_once,
default_host="generic-cli",
host_choices=["codex-cli", "dsh", "generic-cli"],
default_host=resolve_default_turn_host(),
host_choices=list(RUN_ONCE_TURN_HOST_CHOICES),
execution_mode_choices=["isolated-headless"],
default_execution_mode="isolated-headless",
)
Expand Down
48 changes: 48 additions & 0 deletions loopx/control_plane/turn_driver/host_binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Credential-resolved default Turn host binding.

The default host for ``loopx turn plan`` and ``loopx turn run-once`` is decided
by what the operator configured, not by the harness alone:

- a configured operator model credential selects the DeepSeek Harness host
(``dsh``), so the bounded Turn runs on the operator-supplied endpoint instead
of any individual's CLI subscription;
- with no operator credential configured the default stays the Codex CLI host
(``codex-cli``).

Resolution is a pure function of the environment so the command defaults, the
Turn plan readback, and tests quote one rule instead of drifting apart.
"""

from __future__ import annotations

import os
from collections.abc import Mapping

HOST_WITH_OPERATOR_CREDENTIAL = "dsh"
HOST_WITHOUT_OPERATOR_CREDENTIAL = "codex-cli"

# Credentials the DSH Turn host already reads for its provider. Presence is the
# whole signal: the binding never invents a credential or falls back to a
# personal subscription when one is configured.
OPERATOR_CREDENTIAL_ENV_VARS = ("DEEPSEEK_API_KEY",)
OPERATOR_ENDPOINT_ENV_VAR = "DEEPSEEK_BASE_URL"


def configured_operator_credential(
environ: Mapping[str, str] | None = None,
) -> str | None:
"""Return the configured operator credential env var name, else ``None``."""

source = os.environ if environ is None else environ
for name in OPERATOR_CREDENTIAL_ENV_VARS:
if str(source.get(name, "") or "").strip():
return name
return None


def resolve_default_turn_host(environ: Mapping[str, str] | None = None) -> str:
"""Return the shipped default Turn host for this operator environment."""

if configured_operator_credential(environ) is not None:
return HOST_WITH_OPERATOR_CREDENTIAL
return HOST_WITHOUT_OPERATOR_CREDENTIAL
2 changes: 1 addition & 1 deletion loopx/dsh_goal_mode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ contract failure instead of collapsing into `unknown`.
## Requirements

- Optional dependency group `loopx[deepseek-harness]`, currently pinned to the
validated `deepseek-harness-sdk==0.1.2a3` API, or a compatible runner via
validated `deepseek-harness-sdk==0.1.5rc1` API, or a compatible runner via
`--dsh-runner`.
- A dsh `cordis.yml` plus any `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`
settings for the real runtime.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Changelog = "https://github.com/huangruiteng/loopx/releases"

[project.optional-dependencies]
deepseek-harness = [
"deepseek-harness-sdk==0.1.2a3",
"deepseek-harness-sdk==0.1.5rc1",
]
test = [
"jsonschema>=4.23,<5",
Expand Down
2 changes: 2 additions & 0 deletions tests/control_plane/test_cli_output_budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,8 @@ def _mode_variant_commands(
+ [
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
GOAL_ID,
"--agent-id",
Expand Down
10 changes: 10 additions & 0 deletions tests/test_loopx_turn_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,8 @@ def test_turn_run_once_cli_commits_validated_result_and_one_quota_slot(
"json",
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
"loopx-turn-fixture",
"--agent-id",
Expand Down Expand Up @@ -1782,6 +1784,8 @@ def test_turn_run_once_cli_commits_validated_result_and_one_quota_slot(
"json",
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
"loopx-turn-fixture",
"--agent-id",
Expand Down Expand Up @@ -1954,6 +1958,8 @@ def test_turn_run_once_cli_completes_selected_todo_after_validation(
"json",
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
"loopx-turn-fixture",
"--agent-id",
Expand Down Expand Up @@ -2195,6 +2201,8 @@ def _turn_run_once_completion_argv(
"json",
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
"loopx-turn-fixture",
"--agent-id",
Expand Down Expand Up @@ -2811,6 +2819,8 @@ def test_turn_run_once_cli_rejects_unproven_host_claim_before_writeback(
"json",
"turn",
"run-once",
"--host",
"generic-cli",
"--goal-id",
"loopx-turn-fixture",
"--agent-id",
Expand Down
77 changes: 77 additions & 0 deletions tests/test_turn_default_host_binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""The default Turn host follows the operator's configured credential."""

from __future__ import annotations

import pytest

from loopx.cli import build_parser
from loopx.control_plane.turn_driver.host_binding import (
HOST_WITHOUT_OPERATOR_CREDENTIAL,
HOST_WITH_OPERATOR_CREDENTIAL,
configured_operator_credential,
resolve_default_turn_host,
)


@pytest.mark.parametrize(
("environ", "expected"),
[
({}, HOST_WITHOUT_OPERATOR_CREDENTIAL),
({"DEEPSEEK_API_KEY": "sk-operator"}, HOST_WITH_OPERATOR_CREDENTIAL),
({"DEEPSEEK_API_KEY": ""}, HOST_WITHOUT_OPERATOR_CREDENTIAL),
({"DEEPSEEK_API_KEY": " "}, HOST_WITHOUT_OPERATOR_CREDENTIAL),
(
{"DEEPSEEK_BASE_URL": "https://example.invalid"},
HOST_WITHOUT_OPERATOR_CREDENTIAL,
),
],
)
def test_default_host_follows_credential_presence(environ, expected):
assert resolve_default_turn_host(environ) == expected


def test_configured_credential_names_the_env_var():
assert (
configured_operator_credential({"DEEPSEEK_API_KEY": "sk-operator"})
== "DEEPSEEK_API_KEY"
)
assert configured_operator_credential({}) is None


def _turn_argv(command: str) -> list[str]:
argv = ["turn", command, "--goal-id", "goal-x", "--agent-id", "agent-x"]
if command == "run-once":
argv.extend(["--project", "."])
return argv


@pytest.mark.parametrize("command", ["plan", "run-once"])
def test_cli_defaults_to_dsh_when_the_operator_credential_is_configured(
command, monkeypatch
):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-operator")

args = build_parser().parse_args(_turn_argv(command))

assert args.host == HOST_WITH_OPERATOR_CREDENTIAL


@pytest.mark.parametrize("command", ["plan", "run-once"])
def test_cli_defaults_to_codex_cli_without_an_operator_credential(
command, monkeypatch
):
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)

args = build_parser().parse_args(_turn_argv(command))

assert args.host == HOST_WITHOUT_OPERATOR_CREDENTIAL


def test_explicit_host_still_wins_over_the_credential_default(monkeypatch):
monkeypatch.setenv("DEEPSEEK_API_KEY", "sk-operator")

args = build_parser().parse_args(
[*_turn_argv("run-once"), "--host", "generic-cli"]
)

assert args.host == "generic-cli"