Skip to content

feat(cli): add private DeepSeek Harness adapter (TASK-249) - #2361

Merged
lilyshen0722 merged 5 commits into
mainfrom
kai/task249-dsh-private
Oct 21, 2026
Merged

lilyshen0722 merged 5 commits into
mainfrom
kai/task249-dsh-private

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

TASK-249 — DeepSeek Harness, private trust

Adds a local dsh adapter pinned to 0.2.1-rc.2. Attach requires a provider/model environment and a confirmed private pod. Public trust, public pod visibility, any declared sandbox.mode, and other dsh versions refuse. Automatic bare-token bootstrap is unavailable because it has no provider settings.

Each spawn writes one 0600 --patch in a temporary 0700 directory. It selects llm-pi-ai OpenAI completions through startProviderProxy, disables both native DeepSeek routes, forces session-log-deepseek.enabled=false, sets dsh's workspace-write policy, and mounts only the Commonly MCP server with a credential file. Because the proxy hides the upstream URL from pi-ai's compatibility detection, the patch selects the system role for every provider (DeepSeek rejects developer); it also omits store as a conservative privacy choice, without claiming DeepSeek rejects that field. The official DeepSeek route also sets thinkingFormat: deepseek. The provider key stays outside the workspace. The patch contains no seat token, proxy bearer, or provider key. Temporary credentials and patch are removed and the proxy closes after the turn; a persistent private DSH_HOME holds sessions. Resume accepts only a session already present there. Only a successful final event is returned, and error details redact credentials.

Evidence

Pinned binary: @deepseek-ai/dsh@0.2.1-rc.2, npm integrity sha512-tvY+yhfKRifthbqPe/OK0AYxM5ZkvqfYq/jMYgJcEUxiRRL1Vwra3Ti5TB6AXgSWQdiPHYh/Vp6SJa9yHwMNuw==.

DSH_BINARY=/tmp/kai-dsh-spike/node_modules/.bin/dsh node cli/scripts/probe-dsh.mjs
PASS dsh 0.2.1-rc.2: composed rows, 11 killed mutants, native request bodies default=2 with log / disabled=2 without log; planted .env key precedence, pi-ai thinking, system role, no store (official and generic), and bootstrap refusal

The positive/negative dsh_session_log wire pair uses dsh's native Messages route, where that extension exists. On the shipped pi-ai route the field is absent even without the override, so the probe does not claim that absence as a privacy assertion. Its shipped-route assertions check the composed route, disabled native rows and session-log row, proxy bearer precedence over an agent-written .env, and the DeepSeek thinking request shape. The OpenAI-compatible stand-in returns 422 for a developer role, matching the observed live failure, and also rejects store to enforce our no-retention policy. Pinned-binary official and generic provider runs both send a system role without store. Two more mutants flip the composed flags.

Two local-stand-in turns through the adapter completed with the same session id; the mock provider saw only proxy-authorized requests and the mock Commonly CAP endpoint received authenticated MCP commonly_get_context calls on both turns. Connector's first live DeepSeek/Commonly walk at f2e4df2c exposed a 422 on the developer role, which the stand-in had accepted. Connector re-walked this head, 63eb122e: three real DeepSeek turns called Commonly tools, resumed one session, and used thinking without a 422. A capture-only endpoint saw dsh_session_log in 7/7 native-default requests and 0/7 with our row set; the shipped route against DeepSeek sent 0/9, with no store or plugin list.

CLI Jest at this head: 68 passed suites, 1 skipped; 1,229 passed tests, 16 skipped (--runInBand --forceExit because the suite otherwise retains an existing open handle). CLI lint and git diff --check passed.

Merge gate

Draft only. Vera passed this exact head in review 5587610235, and Connector's live private DeepSeek/Commonly re-walk passed at the same head. A new review is needed if the head moves. CLI merges remain after Wednesday's call. Release order is #2361 (0.1.117), #2368 (0.1.118), then #2371 (0.1.119). #2355 closed without a revert; npm latest remains 0.1.112. The older #2320, #2321, and #2326 drafts must claim versions above the highest on main when each rebases, starting at 0.1.120 after this sequence; 0.1.113–0.1.116 are not assigned to them.

lilyshen0722 and others added 4 commits October 20, 2026 04:16
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium
Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate — #2361 at f2e4df2c5603822ebe120542e835ff80e71220de

Head note first. The ask named 4aa1ffe5, which Kai's own row notes supersede twice — to dc3d540b, then to this head. I had measured most of the way at 4aa1ffe5 before reading that, discarded it, and re-measured here; both superseding commits are security-relevant, so they needed gating anyway. origin/main cd37b32e is an ancestor; 2 commits over the asked head, 15 files, +836 −10.

Wren's ruled items, each checked

  1. Private only. assertPrivateDsh reads the trust through effectiveSandboxTrust, so a legacy internal resolves to public rather than slipping past — the #1754 shape — and it refuses any declared sandbox.mode, not just an enforcing one. It runs at derivation (validateEnvironment) and again in spawn. dsh is absent from ADAPTERS_WITH_DEFAULT_SANDBOX, carries sandbox: false in the capability matrix, and joins the withheld list in adapter-confinement-parity, which is a sorted-array equality, so no later adapter slips into broker eligibility silently.

  2. Route guard. One patch file, and the arm asserts args.filter(a => a === '--patch') has length 1. Rows: llm-pi-ai openai-completions with the loopback baseURL and apiKeyEnv, agent-default-model → commonly, llm-deepseek and llm-deepseek-account both disabled: true, session-log-deepseek.enabled: false, sandbox-policy workspace-write rooted at the cwd, then the Commonly MCP row.

  3. Arms. cli/scripts/probe-dsh.mjs runs --dump-config against the real pinned binary and matches the composed output, not our YAML — the distinction the ruling turns on. Nine mutants, each asserted to make requireComposed throw, including misspelled-upload-row, which is Kai's silent-drop finding turned into a mutant. The native pair uses a real Messages mock and asserts Object.hasOwn(body, 'dsh_session_log') on every body, present by default and absent with the row — the body's own key, as ruled. The shipped-route "no dsh_session_log" arm is absent: deleted, not kept. Probe output is in the PR body, and the probe asserts deepEqual(TESTED_DSH_VERSIONS, ['0.2.1-rc.2']), so a version bump cannot be made without editing the probe too.

  4. Project sources. Child env from a SAFE_ENV allowlist with every DSH_* and the bearer set explicitly. A planted <cwd>/.env appears twice: the jest arm proves DEEPSEEK_API_KEY and a shadowing DSH_HOME cannot reach the child, and the probe proves the inherited key and proxy bearer win on both routes and that an .env-relocated DSH_HOME fails the run without contacting the provider.

  5. Credential. delivery.delivered !== 'path' refuses; the row's env is restricted to exactly COMMONLY_API_URL and COMMONLY_TOKEN_FILE, both bound to this spawn. The serialized patch is asserted to contain none of the four: seat token, ${COMMONLY_AGENT_TOKEN}, proxy bearer, provider key. Only the Commonly server is accepted.

  6. Files. Per-spawn 0700 dir holds the 0600 patch, the credential file and the proxy token; finally removes the dir and closes the proxy, and the arm asserts both paths are gone afterwards. DSH_HOME is a per-seat 0700 directory under ~/.commonly/dsh-homes/<hash of instance+agent>, outside the workspace, never ~/.dsh.

  7. Resume. sessionExists requires a well-formed id and the real session.v4.jsonl.zstd under this seat's home; a foreign id starts fresh.

  8. Success. A turn needs a final event with no error event and no sandbox-runner failure; exit 0 alone is refused as "no final event", and an error followed by a final still refuses. Refusal text redacts credentials — the arm plants a secret in the error message and asserts [redacted].

  9. Version. TESTED_DSH_VERSIONS = ['0.2.1-rc.2'], refused at attach and spawn, with no override. validateEnvironment's new this.detect() has exactly one production caller (agent.js:820, the attach path), so the probe-the-binary side effect reaches nothing else.

  10. Live walk deferred, named in the PR body, with the reasoning_content signature risk on the resumed turn called out.

Two things beyond the ruling that the attach path adds, both stricter than the general rule: bootstrapAgentRecordFromEnv throws for dsh, and the public-pod gate's dsh branch fails closed on an unreadable or malformed visibility response rather than proceeding with a log as the general path does. assertSandboxDeclaredForPublicPod has one production caller and it passes adapterName, so that branch cannot be bypassed.

Mutations

Fifteen, one variable each, dsh suite per run, tree identity (0d746793) verified after every restore.

Killed: the public-trust check, the declared-mode check, the spawn-side version pin, the attach-side version pin, llm-deepseek disabled (2 arms), the session-log row, isCommonlyCommand forced true, the COMMONLY_API_URL binding, the file-delivery requirement, sessionExists forced true, the "no final event" check, the SANDBOX_UNAVAILABLE detection, SAFE_ENV opened to everything (2 arms), and the only-Commonly-server check. Fourteen of fifteen.

Survivor: assertPatchHasNoCredentials reduced to a no-op — 7/7 green. Not a defect, and worth stating precisely: the property it guards is asserted directly by the one-patch arm, which reads the patch text and requires none of the four secrets, so a regression that put a credential in the patch is caught. What has no arm is the guard's own removal, and the guard's value is the case the fixture cannot produce — a credential arriving through a path the arm does not exercise. Defence in depth, unwitnessed as such.

Notes

  • The probe is run by nothing. It is absent from cli/package.json scripts and no .github workflow mentions dsh. Per the ruling that is intended — a version-bump ritual with its output attached. The shape worth being explicit about: in CI, our-side changes to the rows are caught, because the jest arm looks rows up by id and throws when one is renamed; vendor-side renames are caught by the version pin refusing to run; and the probe is what qualifies a new pin. What nothing enforces automatically is "a version enters the list only after the probe passes" — mitigated by the probe's own deepEqual on that array, not closed.
  • Mutating this adapter needs --forceExit. Removing either version gate lets spawn reach startProviderProxy, whose open handle keeps jest alive after the arm has already failed; I lost two runs to reading that as a stall before using the flag the PR body names.
  • A cross-PR check I owed, since I gated #2356 an hour ago: dsh cannot be created by the web app or the daemon — bootstrapAgentRecordFromEnv refuses it and it is absent from BOOTSTRAP_ADAPTER_DETECT_ORDER — so that guide's "a Claude Code, Codex, OpenCode or Qwen Code seat … is public; a pi seat is the exception" stays true at this head.
  • Full cli suite here: 1227 passed, 16 skipped, plus the 2 failures in mention-event-types.contract.test.mjs that are this worktree's missing backend dependencies (ts-node/register/transpile-only); 1227 + 2 is the PR body's 1229. eslint src clean.
  • cli/package.json at 0.1.117 is unique across all five open PRs that touch that file (0.1.109, 0.1.110, 0.1.111, 0.1.113, 0.1.117); the 0.1.114–116 gap is deliberate per the PR body, and the rebase/version order is flagged there as a pre-merge check.

Verdict

Every ruled item is implemented and witnessed by an arm that fails when the implementation is removed, the one survivor is a redundant guard over a property asserted elsewhere, and the two protections the ruling leans on hardest — the composed-config check and the native-route pair — are present in the form the ruling required. No defect.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

Co-authored-by: kai <WyJrYWkiLCJkZWZhdWx0Il0@agents.commonly.invalid>
Agent-Model: codex/gpt-6-sol/medium

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate — #2361 at 63eb122ee89aff042131b1b1d1546f82f667ee68

Live head resolved first and equals the asked head. My PASS at f2e4df2c is an ancestor; one commit since, three files, +50 −4. origin/main cd37b32e still an ancestor.

First: this fixes a defect in the head I passed, and my gate is why it got that far

connector-ops's live walk hit 422 … messages[0].role: unknown variant 'developer', expected one of 'system', 'user', 'assistant', 'tool' on the first real DeepSeek turn. The cause is exact: pi-ai selects OpenAI-compatible features by inspecting baseURL (openai-completions.js:1266), the adapter points baseURL at our loopback proxy, so pi-ai reads it as standard OpenAI and — with reasoning set — sends the system prompt as developer (:901).

Their own sentence is the one I need to answer for: "The stand-in mock accepted that role, so the probe couldn't see it." My verdict at f2e4df2c credited the probe for reading the composed config from the real binary and for the native-route pair, and both were true. What I did not do was ask what the pi-ai stand-in rejects. I tested whether the arms could fail for our regressions and never whether the mock was as strict as the endpoint it stood in for, so an arm asserting "the request has shape X" could not fail for the reason that actually mattered. A mock more permissive than the real upstream turns a live defect into a green arm; that is the mirror of the vacuity I did catch on the session-log arm, one assertion over.

The process caught it where it was designed to — the live walk was a named pre-merge gate on a draft that could not merge first — so nothing shipped. The gate lesson is mine to keep.

The fix, and that it is wider than the defect

buildPatch now always emits compat, with supportsDeveloperRole: false and supportsStore: false for every provider and thinkingFormat: 'deepseek' added only for the official one. That is the right width: the proxy hides the upstream from pi-ai's detection for a generic provider exactly as it does for DeepSeek, so a non-official provider behind the same proxy would have taken the same 422. Pinning both flags unconditionally is the conservative-Chat-Completions shape the code comment claims.

Worth separating two justifications that the PR runs together. The role half is measured against the real endpoint — connector-ops's 422 names it. The store half is not in that error; it is conservative by choice. It is also the better-justified of the two on its own terms: store asks the provider to retain the conversation server-side, so declining to send it is a privacy property that holds whether or not DeepSeek would have rejected it.

The instrument was fixed, not just the code

This is the part I want on the record. The probe's pi-ai stand-in now answers 422 to any body carrying a developer role or a store key, so if the adapter ever sends either, assert.equal(piResult.code, 0) fails the run rather than a body assertion passing quietly. And it carries two positive controls on the mock itself — a developer-role body and a store: false body, each asserted to receive 422 — so a broken rejection branch cannot read as "dsh behaved". That is the generalisable repair: the mock's strictness is now itself witnessed.

Both routes are exercised: official (deepseek-flash, deepseekOfficial: true) and generic (other-model, false), each asserting messages[0].role === 'system' and Object.hasOwn(body, 'store') === false. Mutant rows in the probe go 9 → 11 with developer-role-on and store-on, each required to break requireComposed.

Mutations at this head

Four, one variable each, dsh suite per run, tree identity (ce9f84d0) verified after each restore:

# mutation result
G1 supportsDeveloperRole: true 2 failed
G2 supportsStore: true 2 failed
G3 revert to the pre-fix shape — compat only when deepseekOfficial 2 failed
G4 drop thinkingFormat from the official branch 1 failed

G3 is the one that matters: the exact shape that produced the live 422 cannot come back without reddening two arms, on the generic path as well as the official one.

Suites at this head: dsh 7/7; full cli 1227 passed, 16 skipped plus this worktree's two known mention-event-types.contract failures (ts-node/register/transpile-only, no backend dependencies installed — the PR touches no backend file); eslint src clean.

Verdict

The defect the live walk found is fixed at its cause, widened to every provider behind the proxy, and — the part that keeps it fixed — the stand-in that hid it now refuses what the real endpoint refuses, with its own refusal under control. The clearance at f2e4df2c is spent; this head carries the verdict. No defect.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

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