Skip to content

feat(assistants): make deepseek-harness selectable (#639) - #653

Open
umi-appcoder[bot] wants to merge 1 commit into
kc/639-dsh-servefrom
kc/639-dsh-server
Open

feat(assistants): make deepseek-harness selectable (#639)#653
umi-appcoder[bot] wants to merge 1 commit into
kc/639-dsh-servefrom
kc/639-dsh-server

Conversation

@umi-appcoder

@umi-appcoder umi-appcoder Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Fifth of six PRs for #639. Stacked on #652 — base is kc/639-dsh-serve.

This is the one that turns the entry on. After this, deepseek-harness appears in the picker, the Hypervisor routes to the ACP adapter, the Builds tab launches the bridge, and both switchers (model, effort) are populated.

Gating — the acceptance criterion that matters most

Listed only when both shutil.which('dsh') and DEEPSEEK_API_KEY are present.

Binary presence alone is the right signal only for the OAuth CLIs (agy, codex). dsh authenticates with an API key, so listing it without one offers an entry whose every turn fails with Authentication Fails — the exact error captured from the real harness. An image predating the install simply doesn't list it, and the existing opencode-deepseek entry is untouched: one key now enables two genuinely different agents over the same API, which is the point of the issue.

Tested across all four combinations, plus an assertion that an older image leaves the other entries byte-identical.

Builds, and one honest behaviour difference

Builds run the bridge's serve mode (#652) rather than a REPL, because dsh ships none we can use.

auto_approve is deliberately a no-op, and that differs from every other assistant. For claude/ante the Build tab leaves the CLI's approval prompts on and only the Hypervisor passes a skip flag. ACP permission requests are JSON-RPC calls that must be answered inside the turn, and a tmux pane has no way to put that question to a user and get an answer back — an unanswered request stalls the turn forever. The bridge therefore always approves, on both surfaces. The pod is the sandbox.

I've put this in the code comment and given it its own section in docs/llm-setup.md rather than leaving it to be discovered.

Effort needed a third delivery shape

_EFFORT_DELIVERY knew two shapes: an env var (claude, kc-harness) and a -c key=value pair (codex). The bridge takes a plain --effort value flag, and the harness's stops (off/low/high/max) don't line up with the canonical five. So the table grows flag plus an optional vocab translation:

'deepseek-harness': {'flag': '--effort', 'vocab': _DSH_EFFORT_VOCAB},

_DSH_EFFORT_VOCAB mirrors DeepseekHarnessAdapter._EFFORT_NATIVE under the same lockstep-test discipline _EFFORT_CAP already has — I kept them as mirrored tables rather than importing across modules, because the hypervisor_session import in server.py is deliberately guarded and a None on a broken install would blow up at class-definition time.

Nothing clamps: the harness really does expose max. Canonical medium rounds up to the harness's high (its default and documented balance point) rather than down to low.

Models

The harness's own ids, read off a live session/new's advertised model option. The bridge resolves a bare id against that option, so no layer above it handles the encoded ["provider","model"] pair form. The default is the harness's own (deepseek-v4-flash), so listing the assistant changes no behaviour. The experimental vision model it also offers is left out of the default list — KC_DSH_MODELS curates.

Chart + docs

assistant.deepseekHarness.{model,models,effort}KC_DSH_*. No key of its own — it reuses assistant.deepseek.apiKey.

docs/llm-setup.md gets a DeepSeek Harness section: the two-signal gate, an explicit table of how it differs from opencode-deepseek, the Node 22 and ~/.dsh requirements, why it goes through ACP rather than --profile headless, and the approval-prompt difference. (The "three assistants" heading was already counting wrong before this change — four rows under it — and adding a row would have made it worse, so it is now just "the assistants". I left the doc's other staleness alone.)

Verification

  • 30 new Python tests: every gating combination; both DeepSeek paths coexisting; the model list and its env override; resolve_model defending the boundary against a free-form webhook caller; the cap/vocab/delivery lockstep with hypervisor_session; the full effort translation; the argv the Build tab launches, including that a hostile KC_DSH_MODEL survives shlex round-tripping as exactly one argument; and that the new flag shape leaves claude and codex delivery alone.
  • 5 new chart tests for the env wiring, including that the harness's model var does not displace KC_DEEPSEEK_MODEL.
  • helm lint clean; helm unittest 194/194.
  • Full Python suite: 2470 tests vs 2440 on the parent branch — exactly the 30 added, no losses — with the same 8 pre-existing boards_* import errors that reproduce on unmodified main in this environment.

Still not verified: anything needing a real DEEPSEEK_API_KEY. The picker/Builds/Hypervisor wiring is unit-tested, but a completed turn against live DeepSeek has not been observed from this workspace.

Last one

  1. Orchestrator (_HEADLESS_CAPABLE, assistant list, MCP tool enum), mcp_registry _PROVIDERS, and the skills provider.

Refs #639

🤖 Generated with Claude Code

@umi-appcoder

umi-appcoder Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Note on the empty checks list. CI is configured as pull_request: branches: [main], so a PR targeting another branch gets no checks at all — that is the cost of stacking, not a signal about this branch. Each PR's checks will fire once its parent merges and GitHub retargets it to main.

In the meantime everything CI runs was run locally on this exact branch: make python-tests (python3 -m unittest discover -s tests -p '*_test.py'), helm lint charts/workspace, helm unittest charts/workspace, and bash -n over the touched shell. Counts and the pre-existing boards_* import errors (which reproduce on unmodified main in that environment) are in the PR body.

Fifth slice of #639. This is the one that turns the entry on: the
dashboard picker, the Hypervisor, the Builds tab and the model/effort
switchers all become aware of it.

Gating is on BOTH `shutil.which('dsh')` AND DEEPSEEK_API_KEY. Binary
presence alone is the right signal only for the OAuth CLIs (agy, codex);
`dsh` authenticates with an API key, so listing it without one would
offer an entry whose every turn fails with "Authentication Fails". An
image predating the install simply doesn't list it, and the existing
`opencode-deepseek` entry is untouched — one key now enables two
different agents over the same API, which is the whole point.

Builds run the ACP bridge's serve mode rather than a REPL, because `dsh`
ships none we can use. `auto_approve` is deliberately a NO-OP here and
that is a real behaviour difference: ACP permission requests are
JSON-RPC calls that must be answered inside the turn, and a tmux pane
cannot put that question to a user, so the bridge always approves on
both surfaces. Called out in the code and given its own section in
docs/llm-setup.md rather than left to be discovered.

Effort needed a third delivery shape. The existing table knows an env
var and a `-c key=value` pair; the bridge takes a plain `--effort value`
flag, and the harness's stops (off/low/high/max) don't line up with the
canonical five. So _EFFORT_DELIVERY grows `flag` plus an optional
`vocab` translation, and _DSH_EFFORT_VOCAB mirrors the adapter's table
under the same lockstep-test discipline _EFFORT_CAP already has. Nothing
clamps: the harness really does expose `max`.

Models are the harness's own ids, read off a live `session/new`'s
advertised option; the bridge resolves a bare id against that option so
no layer above it handles the encoded ["provider","model"] pair form.
The default is the harness's own, so listing the assistant changes no
behaviour. The experimental vision model it also offers is left out of
the default list — KC_DSH_MODELS curates.

Chart: assistant.deepseekHarness.{model,models,effort} → KC_DSH_*. No
key of its own; it reuses assistant.deepseek.apiKey.

Docs: a DeepSeek Harness section in llm-setup.md covering the two-signal
gate, an explicit table of how it differs from opencode-deepseek, the
Node 22 and ~/.dsh requirements, why it goes through ACP rather than
--profile headless, and the approval-prompt difference. The "three
assistants" heading was already counting wrong before this change and is
now just "the assistants".

30 new tests: every gating combination (binary+key, binary only, key
only, neither) including that an older image leaves the other entries
byte-identical; both DeepSeek paths coexisting; the model list and its
env override; resolve_model defending the boundary against a free-form
webhook caller; the cap/vocab/delivery lockstep with hypervisor_session;
the effort translation; the argv the Build tab launches, including that
a hostile KC_DSH_MODEL survives shlex round-tripping as exactly one
argument; and that the new flag delivery shape leaves claude and codex
alone. 5 chart tests for the env wiring.

Refs #639
@umi-appcoder

umi-appcoder Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

One thing worth knowing about the gate, found while checking this over. Not a defect in this PR, but it affects how discoverable the new entry is.

available_assistants() reads os.environ.get('DEEPSEEK_API_KEY'). A key pasted into Settings → Provider keys lands in ProviderKeysManager, whose env_overlay() is applied at CLI spawn — nothing merges it into the dashboard server's own os.environ. So a user who pastes a DeepSeek key there will not see deepseek-harness appear in the picker until the pod is redeployed with assistant.deepseek.apiKey set.

This is pre-existing and shared by every key-gated entry — opencode-deepseek, opencode-openrouter, opencode-zen, kc-harness — and I matched the sibling's check exactly rather than making this one entry behave differently. Widening the gate to consult the overlay would change the listing semantics of four existing assistants, which is well outside #639 and deserves its own issue.

Flagging it because the failure mode is quiet and specific to a key-gated assistant: "I set my DeepSeek key and the harness never showed up."

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.

0 participants