dsh Preview
Integrate Intutic governance with DeepSeek's "dsh" — a developer preview (@deepseek-ai/dsh, first published 2026-08-13) plugin-first coding-agent harness built on DeepSeek's own "Cordis" extensibility framework.
PREVIEW — breaking changes possible
dsh is a developer preview with its own stated breaking-changes policy. This integration was last verified — including a live session — against @deepseek-ai/dsh 0.2.0-rc.2, and declares @intutic/gate ^3.0.0 in a profile's package.json when the profile has no declaration of its own. A preview product can still change its plugin API, its tools/pre-execute payload shape, or its configuration layout between releases (0.2 moved live configuration out of settings.yaml and into each profile's cordis.patch.yml). Known gaps lists what remains open.
How it works
Unlike every other native harness Intutic supports, dsh has no hooks.json/shell-script gate surface at all — it is plugin-first ("Cordis," DeepSeek's own extensibility system). The blocking gate for dsh ships as a real, checked-in TypeScript module, @intutic/gate/dsh (packages/gate-js/src/dsh.ts in this repo), not a per-workspace generated script the way Grok Build's or Muse Code's gates are.
That module is a genuine Cordis Plugin subscribed to dsh's tools/pre-execute event — a waterfall (Cordis's cooperative, composable event-dispatch mode) that runs before every tool call. It calls into @intutic/gate's own Gate.guard() evaluator and returns {kind: 'deny', reason} to veto a call, or calls the waterfall's next() to let it (and any other listener, including dsh's own built-in approval flow) proceed.
Intutic does not generate this plugin file — it already exists, published on npm. What Intutic writes is the registration: a row in every existing dsh profile's cordis.patch.yml naming the plugin, plus the @intutic/gate dependency declaration in that profile's package.json, plus — in that same cordis.patch.yml — with --route, a baseURL override on the llm-deepseek entry, dsh's default LLM route, for LLM egress. It also (re)generates $DSH_HOME/INSTALL.md each time it writes — see step 5 and "What gets written" below.
Setup
1. dsh detection
dsh is detected by any of:
$DSH_HOME(defaults to~/.dsh/) containingsettings.yaml(left by dsh releases before 0.2),.credentials.yaml, or aprofiles/directory, or- the
dshbinary being found in yourPATH.
2. Initialize Intutic
intutic init ✔ dsh → /path/to/project/AGENTS.mdintutic init only detects the harness and records it in ~/.intutic/config.json; it writes no harness files. The files described on this page are written by intutic gates install (open core, no control plane), from the rule sets in .intutic/sops/ — see What writes harness files:
intutic gates install3. Start the proxy and route dsh
intutic start
intutic gates install --routeintutic gates install writes the dsh registration described below (dsh must be in ~/.intutic/config.json's harnesses, which intutic init records when it detects dsh). --route also points dsh's default LLM route at the proxy; the proxy must be running.
4. Run dsh with a profile at least once
dsh's own CLI requires --profile <name> on every invocation — there is no bare "default" profile. Intutic's writer only registers into profiles that already exist:
dsh --profile myprojectIf you have never run dsh before, Intutic logs dsh_skip and registers nothing yet — there is no profile to register into. Run intutic gates install again after your first dsh --profile <name> run.
5. Install the plugin's dependency
Intutic declares @intutic/gate in your profile's package.json, but — like every other Intutic writer — cannot run a package manager on your behalf. Install it once per profile:
dsh plugin --profile myproject add @intutic/gatedsh 0.2 installs it as a plain profile dependency and prints @intutic/gate declares no dsh.bundle — installed as a plain dependency, not a profile layer. That warning is expected: the cordis.patch.yml row Intutic wrote is what loads the plugin. (cd $DSH_HOME/profiles/myproject && pnpm add @intutic/gate is equivalent.)
Until this step runs, dsh is NOT governed
Without the package, dsh cannot import the row's module. It prints two lines on stderr at every start and then runs the session with no gate:
dsh: warning: 1 entry did not activate
intutic-governance (@intutic/gate/dsh): failed to importdsh offers no way for a profile row to make startup fail instead, so this step is permanent and manual. intutic status shows which profiles are registered but not yet activated.
Each write also (re)writes $DSH_HOME/INSTALL.md, listing the exact dsh plugin --profile <name> add @intutic/gate command for every profile currently registered — a standing, always-current reference alongside the onboarding text (the same pattern the n8n integration's own auto-generated INSTALL.md follows).
Coverage visibility
These surface the "silent no-profile window" (dsh installed but never run, so there is no profile to register into) and the pending activation step above, so neither goes unnoticed between syncs:
intutic statusprints a dedicateddsh (DeepSeek harness):block: a warning when dsh is detected but has zero profiles, or — once profiles exist — a per-profile breakdown of which ones are registered but not yet activated (pointing atINSTALL.md), versus fully registered and activated.
What gets written
- Plugin registration: a
{ insert: [{ id: 'intutic-governance', name: '@intutic/gate/dsh', config: {...} }] }row merged into every existing$DSH_HOME/profiles/*/cordis.patch.yml— a structural YAML edit (via theyamlpackage'sparseDocument/setIn) that preserves every other row and any comments/formatting around it. - Dependency:
@intutic/gate^3.0.0added to that profile'spackage.jsondependenciesonly when the profile declares none — a rangedsh plugin addalready wrote is left alone (see step 5 above for why the declaration alone is not enough). A profile an earlier Intutic version set up keeps its^2.0.0declaration. Upgrade it to evaluate decide rules (cd $DSH_HOME/profiles/<name> && pnpm add @intutic/gate@^3); until then its 2.x gate refuses every call to an enforcing decide rule's tools, through the rule's fallback line. - LLM egress — default route (with
--route): a{ id: llm-deepseek, config: { baseURL: <proxy> } }override in the samecordis.patch.yml. In dsh 0.2 thellm-deepseekentry is@deepseek-ai/dsh-llm-deepseek-api-key, which serves thedeepseek-officialprovider thatdsh-base'sagent-default-modelrow selects (deepseek-flash). A Cordis override replaces the entry's wholeconfig, so when the profile already overridesllm-deepseek(for example after a Models-page save), onlybaseURLis set on that row and its other fields (apiKeyEnv,reasoningEffort,models, ...) are kept. Requests then go to<proxy>/v1/messagesin the Anthropic Messages format. - How the proxy forwards them: the proxy sends DeepSeek's own API model ids (
deepseek-chat,deepseek-reasoner, anddeepseek-flash, which dsh uses) to DeepSeek. These Messages requests go unchanged to DeepSeek's Anthropic-compatible API at$DEEPSEEK_UPSTREAM_URL/anthropic/v1/messages(defaulthttps://api.deepseek.com). OpenAI-format requests (/v1/chat/completions) for the same models go to$DEEPSEEK_UPSTREAM_URL/v1/chat/completions, and any other route naming one of them is refused with 400unsupported_route. DeepSeek requests get the same governance as any other provider: DLP, SOP rules, the response gate's tool-call refusals, and metering. Open-weight names such asdeepseek-r1,deepseek-coder-v2-instructordeepseek-r1:7bare not DeepSeek's API and keep going toOPENAI_UPSTREAM_URL. The upstream key comes from one of two places:- dsh's own
DEEPSEEK_API_KEY: dsh sends it inx-api-key, and the proxy passes it through. - An Intutic virtual key: if you set
DEEPSEEK_API_KEYto a virtual key (vk_...), as a gateway that accepts only virtual keys requires, the proxy sends the workspace's DeepSeek key in its place. Provision that key under Settings › Security › Provider Keys, or setDEEPSEEK_API_KEYon a self-hosted proxy. Without one, the request is refused with 402 (no_upstream_credential, orbyok_requiredwhere the gateway requires your own key). The virtual key is never sent to DeepSeek.
- dsh's own
- Not redirected: the signed-in DeepSeek account route (
llm-deepseek-account), whose token dsh only releases to DeepSeek's own origin, andllm-pi-airoutes you configure yourself. Earlier versions of this integration also wrote anllm-pi-airoute into$DSH_HOME/settings.yaml; dsh 0.2 no longer reads that file (it imports it once into the first profile that boots and renames itsettings.yaml.imported), so Intutic no longer writes it. $DSH_HOME/INSTALL.md: regenerated on each write (write-if-changed) — lists the manualdsh plugin --profile <name> add @intutic/gatecommand for every currently-registered profile. See step 5 above.- Protected paths: agent tool calls that touch
.dsh/profiles,.dsh/cordis.patch.yml(the home-level patch layer, which outranks every profile's) or.dsh/settings.yamlare blocked by the generated gates. - Rules: the section between
<!-- INTUTIC:RULES:START -->and<!-- INTUTIC:RULES:END -->in the workspace'sAGENTS.md, which the default profile's agent-instructions plugin reads. It is written when at least one SOP targets dsh or anotherAGENTS.mdreader in the workspace. Your own text in the file is kept, and the section is shared with every other harness that readsAGENTS.md(see Where rule sets go).
To undo what intutic gates install writes here, run intutic disconnect --harness dsh: each file goes back to what it held before Intutic first wrote it, or is deleted if Intutic created it, and edits you made since are kept. In AGENTS.md only the marked section is taken out, and it stays while another harness that writes it is still connected. See intutic disconnect.
Pre-tool hooks (blocking)
dsh's veto contract is confirmed, not assumed — read from @deepseek-ai/dsh-tools's shipped code (re-read for 0.2.0-rc.2) and observed in a live 0.2.0-rc.2 session:
- The event is
tools/pre-execute, declared by@deepseek-ai/dsh-tools— dsh's own tool-execution pipeline's "reorderable allow/deny/ask gate." It fires for every tool call unconditionally, not behind an opt-in matcher. - It is a genuine Cordis
waterfall:(exec, next) => Promise<PreToolDecision>.PreToolDecisionis{kind:'allow'}|{kind:'deny', reason}|{kind:'ask', reason?}|{kind:'cancel'}(cancelis new in 0.2). - A deny reaches the model as an error tool result (
Error: <reason>) and the tool never runs. Anaskgoes to dsh's approval service, and becomes a deny when no approval service is mounted. Guards that run after the waterfall can only deny, so nothing later turns Intutic's deny back into an allow. - Intutic's plugin calls
next()on allow (so later listeners — including dsh's own built-in approval flow — still run) and returns{kind:'deny', reason}without callingnext()to veto, per Cordis's own waterfall semantics ("a listener that does not callnext()vetoes the rest of the chain"). - A crash inside the gate (anything other than its own structured refusal) is treated as "cannot evaluate" and denied — fail-closed, the same posture every other harness's gate in this product takes.
Every decision is appended to .intutic/events/hook-events.jsonl, same as every other harness.
Enforcement tiers under dsh vs. under a shell/JS hook
@intutic/gate's Tier A1 (the local policy snapshot) reads only ~/.intutic/hooks/policy-snapshot.rules — unlike the shell/JS gates this product generates for other harnesses, it does not separately compile in the "static floor" (bypass/secret-content/skill-surface pattern tables). A dsh workspace therefore gets Tier A0 (SSO group policy from the snapshot), Tier A1 (snapshot rules), the MCP server registry and allowlist checks, Tier A2 (image integrity), Tier A3 (SOP rules), and Tier B (the control-plane /hook-gate call) — a strict subset of the compiled-in floor a generated shell/JS hook enforces, by design (this is a pre-existing, documented gap in the @intutic/gate package itself, not something dsh's integration introduced). See @intutic/gate's own README for the full accounting.
Known gaps
What remains open:
- The signed-in DeepSeek account route is not redirected (see "What gets written"). dsh releases that route's token only to DeepSeek's own origin, so this limit is permanent.
- A machine where dsh has never been run has no profile to register into until the user's first
dsh --profile <name>run. - The plugin needs a manual install per profile (step 5 above). Until then dsh warns and runs ungoverned — this is a permanent, accepted step.
The proxy's DeepSeek route is tested against a mock DeepSeek upstream (packages/proxy/tests/deepseek_routing_test.rs). It has not been tested against the real DeepSeek API or with a live dsh session through the proxy.
(The three access-restricted dsh packages earlier versions of this page listed are no longer dsh dependencies as of 0.2; their roles moved to public packages that were read directly.)
Config details
| Property | Value |
|---|---|
| Harness type | dsh |
| Rules file | AGENTS.md (a marked section) |
| Registration files | $DSH_HOME/profiles/*/cordis.patch.yml (plugin row + llm-deepseek egress row), $DSH_HOME/profiles/*/package.json |
| Gate module | @intutic/gate/dsh — a real, checked-in TypeScript Cordis plugin, not a generated script |
| Detection | $DSH_HOME/~/.dsh/ (settings.yaml, .credentials.yaml, or profiles/), or dsh in PATH |
| Format | YAML |
| Write strategy | Structural YAML edit (write-if-changed), atomic rename |
| Block contract | Cordis waterfall — returns {kind:'deny', reason} without calling next() — confirmed against @deepseek-ai/dsh-tools' shipped code and in a live session |
| Last verified | @deepseek-ai/dsh 0.2.0-rc.2, 2026-10-03 |
Live-verified against dsh 0.2.0-rc.2
On 2026-10-03 a headless dsh 0.2.0-rc.2 session ran against a local scripted model, with @intutic/gate 2.0.0 installed through dsh plugin add and a policy snapshot that blocks one command. The model's request for that command was refused: the command never ran and the model received the block reason as an error tool result, while a command the snapshot does not match ran normally. Every model request went to the baseURL Intutic wrote. Not covered: a real DeepSeek endpoint, the Intutic proxy in the path (its DeepSeek route was added afterwards and is tested against a mock upstream), and the web/ACP templates' approval UIs.