dsh-plugin-lcu 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kanner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,316 @@
1
+ # dsh-plugin-lcu
2
+
3
+ English | [中文](docs/README.zh.md)
4
+
5
+ Drive the desktop and Chrome from [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) by
6
+ plugging in [LCU](https://github.com/amontlabs/lcu) — *Codex computer use, decoupled from the app*.
7
+
8
+ LCU exposes the computer-use runtime that ships inside the ChatGPT desktop app as an MCP server.
9
+ This plugin makes that runtime a first-class DSH capability: an Agent in an enabled mode gets a `js`
10
+ tool that can read and operate real application windows and real Chrome tabs. **No Codex
11
+ authentication is involved** — the runtime comes from your local ChatGPT installation, and LCU never
12
+ downloads, installs, authenticates, or rewrites it.
13
+
14
+ ## Table of Contents
15
+
16
+ - [What you get](#what-you-get)
17
+ - [Requirements](#requirements)
18
+ - [Install](#install)
19
+ - [Use](#use)
20
+ - [Configuration](#configuration)
21
+ - [Approvals and the security model](#approvals-and-the-security-model)
22
+ - [Understand the implementation](#understand-the-implementation)
23
+ - [Troubleshooting](#troubleshooting)
24
+ - [Companion tools](#companion-tools)
25
+ - [Known limitations and deferred work](#known-limitations-and-deferred-work)
26
+ - [Development](#development)
27
+ - [License](#license)
28
+
29
+ ## What you get
30
+
31
+ Two model-facing tools, exactly as LCU defines them — this plugin does not invent a schema:
32
+
33
+ | Tool | What it does |
34
+ |---|---|
35
+ | `js` | Run one JavaScript program against the `cua` desktop/browser API. The first call returns the API documentation, and app or tab selection returns the initial UI state. |
36
+ | `js_reset` | Discard the persistent JavaScript session and start a fresh runtime. |
37
+
38
+ Two host-only tools stay reachable by the plugin and are **never** shown to the model:
39
+ `turn_ended` (per-turn cleanup) and `js_add_node_module_dir`.
40
+
41
+ Screenshots arrive as durable images through DSH's attachment store, so a model route that declares
42
+ image input can actually look at the screen.
43
+
44
+ ## Requirements
45
+
46
+ | | |
47
+ |---|---|
48
+ | OS | macOS on Apple Silicon. LCU supports Linux too, but this plugin is developed and verified on macOS. |
49
+ | ChatGPT desktop app | Installed and signed by OpenAI. It supplies the runtime and the instructions. |
50
+ | Python | 3.12 or newer, on `PATH` or in `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`. |
51
+ | LCU | Installed separately — see below. |
52
+ | DSH | A profile you can install a bundle into. |
53
+
54
+ The plugin targets **macOS**; the `host-guard` uses `ps`/`plutil` and the lifecycle uses LCU's macOS
55
+ path. Linux would need those two pieces revisited.
56
+
57
+ ## Install
58
+
59
+ ### 1. Install LCU
60
+
61
+ Download the release archive for your platform, verify its checksum, and run its installer. Do **not**
62
+ register another harness: this plugin is your harness.
63
+
64
+ ```sh
65
+ TAG=v0.9.6
66
+ TARGET=darwin-arm64
67
+ curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz"
68
+ curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz.sha256"
69
+ shasum -a 256 -c "lcu-${TAG#v}-$TARGET.tar.gz.sha256" # must print OK
70
+ tar -xzf "lcu-${TAG#v}-$TARGET.tar.gz" && cd "lcu-${TAG#v}-$TARGET"
71
+ ./scripts/install.sh --runtime-only --yes
72
+ ```
73
+
74
+ Then confirm the runtime loads:
75
+
76
+ ```sh
77
+ ~/.local/share/lcu/current/bin/lcu doctor --non-interactive
78
+ ```
79
+
80
+ You want `Original Mac provider loaded; app listing and app-state methods are available`. Privacy
81
+ permissions are granted on first use, not here.
82
+
83
+ ### 2. Install the plugin into a profile
84
+
85
+ Installing makes the plugin's one row — an LCU host — active in that profile. It opens nothing at load
86
+ time.
87
+
88
+ ```sh
89
+ dsh plugin --profile <profile> add dsh-plugin-lcu
90
+ ```
91
+
92
+ For the **desktop** app's managed profile, the CLI refuses; install it through the app's plugin
93
+ manager (Settings ▸ Plugins) instead, which runs the same pnpm operation.
94
+
95
+ ### 3. Generate the presets
96
+
97
+ DSH agent presets have **no inheritance**: a preset's `config.plugins` is its complete plugin list, and
98
+ a patch replaces a whole entry rather than merging into it. So a custom preset must restate its base.
99
+ Rather than hand-copying that list, generate it from the preset that is actually installed:
100
+
101
+ ```sh
102
+ node node_modules/dsh-plugin-lcu/scripts/gen-presets.mjs --profile ~/.dsh/profiles/<profile>
103
+ ```
104
+
105
+ This writes a marked block into that profile's `cordis.patch.yml` containing two presets:
106
+
107
+ | Preset | Base | Adds |
108
+ |---|---|---|
109
+ | `daily` | the shipped `ptc` preset | `subagent_codex` enabled |
110
+ | `heavy` | the same, with `tool-presentation: both` | everything above, and this plugin attaches |
111
+
112
+ Re-run it after a DSH upgrade so the copies keep up. `--with-heavy` is implied; `--dry-run` prints
113
+ without writing, and `--out FILE` writes somewhere else.
114
+
115
+ > `heavy` sets the tool presentation to `both` on purpose. In pure `ptc` presentation the model only
116
+ > sees `run_code`, so `js` would have to be nested as a JavaScript string inside another JavaScript
117
+ > program. `both` keeps `js` directly callable.
118
+
119
+ ### 4. Configure which modes get the capability
120
+
121
+ The plugin is one root row with a `presets` allowlist. Edit the installed
122
+ `cordis.patch.yml` (or the profile patch) to match the preset ids you generated:
123
+
124
+ ```yaml
125
+ - id: lcu
126
+ name: 'dsh-plugin-lcu'
127
+ config:
128
+ presets:
129
+ - heavy
130
+ ```
131
+
132
+ ### 5. Restart, then make one call
133
+
134
+ Restart the harness — plugin **code and configuration changes are not hot-reloaded**. Then start a
135
+ task in the `heavy` mode (displayed as **重活**) and ask it to do something harmless:
136
+
137
+ > Use the `js` tool to run `await cua.getState();` and tell me which apps are running.
138
+
139
+ The first time an app is touched, the runtime asks for approval. See below.
140
+
141
+ ### Optional: enable Chrome
142
+
143
+ ```yaml
144
+ config:
145
+ chrome: true
146
+ ```
147
+
148
+ Then:
149
+
150
+ ```sh
151
+ ~/.local/share/lcu/current/bin/lcu browser install
152
+ ```
153
+
154
+ Enable the official ChatGPT extension in the Chrome profile you want to drive, and restart Chrome (or
155
+ toggle the extension at `chrome://extensions`) so it reconnects through LCU's relay rather than Codex's.
156
+ `lcu browser status` reports whether the connector points at this installation. Sites stay
157
+ exact-origin approvals.
158
+
159
+ ### Optional: pre-approve sites
160
+
161
+ Every site the runtime wants to use asks once. To skip the prompt for origins you trust, list exact
162
+ origins — the message must round-trip through `new URL(...).origin` unchanged:
163
+
164
+ ```yaml
165
+ config:
166
+ allowedOrigins:
167
+ - http://localhost:3000
168
+ ```
169
+
170
+ The diagnostic log names every origin that was asked, which is the easiest way to discover them.
171
+
172
+ ## Use
173
+
174
+ Pick an enabled mode when you start a task. The tools are attached per Agent: a session in any other
175
+ mode never sees them, and never spawns the runtime.
176
+
177
+ Typical asks:
178
+
179
+ ```
180
+ Take a screenshot of the Finder window and tell me its resolution.
181
+ List my current Chrome tabs.
182
+ Open Safari, go to example.com and read the page title.
183
+ ```
184
+
185
+ ## Configuration
186
+
187
+ | Field | Default | Meaning |
188
+ |---|---|---|
189
+ | `command` | `~/.local/share/lcu/current/bin/lcu` | LCU launcher. Set it for a custom `--prefix`. |
190
+ | `chrome` | `false` | Pass `--chrome` to enable the browser surface. |
191
+ | `audio` | `false` | Pass `--audio` to enable the runtime's computer-audio API. |
192
+ | `presets` | `["heavy"]` | Agent preset ids whose sessions get the tools. |
193
+ | `allowedOrigins` | `[]` | Exact HTTP(S) origins answered without asking. Invalid entries are dropped, never widened. |
194
+ | `sectionOrder` | `0` | Prompt section order for the injected LCU instructions. |
195
+
196
+ ## Approvals and the security model
197
+
198
+ The model cannot approve anything. Every decision is a person's:
199
+
200
+ - **Per-app approval.** The runtime asks before it uses an app. The plugin renders its own choices —
201
+ *Allow once*, *Allow for this session* and *Always allow* when the runtime offers them, and
202
+ *Decline* — through DSH's question surface. An answer is mapped back to exactly what was offered; a
203
+ scope the runtime did not offer cannot be granted.
204
+ - **Site approval.** Browser access asks per exact origin. `allowedOrigins` only ever matches an exact
205
+ origin; a trailing slash, a path, or different case is a different origin and is asked, not granted.
206
+ - **The agent's own host is never approvable.** Computer use can click anything an approved app shows,
207
+ including the approval prompt itself. The guard refuses the application hosting the agent — its
208
+ process ancestry and a list of agent hosts and terminals — before any question is asked.
209
+ - **Fail closed.** No question surface, a dismissed prompt, an unrecognized request shape, or an
210
+ aborted call all end as *cancel*, which the runtime treats as a refusal.
211
+
212
+ LCU keeps no permission cache of its own; `Always allow` is remembered by the runtime, per app.
213
+
214
+ ## Understand the implementation
215
+
216
+ ```
217
+ src/connection.ts the MCP client: handshake, tool discovery, calls, elicitation, lifecycle
218
+ src/approval.ts approval-shape recognition and label→value mapping
219
+ src/host-guard.ts the anti-self-approval guard
220
+ src/tool.ts tool definitions, text projection, durable screenshots
221
+ src/index.ts the plugin: per-Agent attach, instructions, turn_ended, approvals
222
+ src/diag.ts the attach/approval diagnostic log
223
+ ```
224
+
225
+ **No MCP SDK dependency.** The harness's own MCP bridge declares `capabilities: {}` and therefore
226
+ cannot answer elicitation — which is exactly how LCU asks for approval — and pulling a second SDK into a
227
+ profile plugin would pin a version the host does not own. MCP over stdio is newline-delimited JSON-RPC,
228
+ so the wire is owned here. Together with type-only imports of the DSH packages, the plugin has **no
229
+ runtime dependencies at all**.
230
+
231
+ **Tools are registered per Agent, not at mount.** The server owns the tool schemas, so they can only be
232
+ fetched after the handshake. An Agent's connection is opened when the Agent is created or when it
233
+ commits a preset choice, and everything the plugin contributes is registered into that Agent's own
234
+ context, so it unwinds on disposal.
235
+
236
+ **Both preset timings are handled.** A new task is created with the deployment default and the picker's
237
+ choice is applied afterwards, so `agent/created` alone would see the wrong composition; the registry
238
+ re-emits `agent-preset/selected`, and the plugin reacts to that too.
239
+
240
+ **Lazy by construction.** Nothing starts at load time. No enabled session, no `lcu` process.
241
+
242
+ **Instructions are injected.** The server's `initialize.instructions` becomes a prompt section on the
243
+ Agent. It is short by design — the API manual lives in the `js` tool description and in the first tool
244
+ result.
245
+
246
+ ## Troubleshooting
247
+
248
+ Everything the plugin decides about attaching and approving is appended to:
249
+
250
+ ```
251
+ ~/.dsh/lcu-diag.log
252
+ ```
253
+
254
+ It rotates by starting over past 1 MB. `LCU_DIAG=0` disables it. This is the first place to look: the
255
+ harness has no plugin-log surface a running session can read, and a failing `agent/created` listener is
256
+ otherwise swallowed silently.
257
+
258
+ | Symptom | Cause and fix |
259
+ |---|---|
260
+ | Tools never appear in an enabled mode | Check the log for `decide … composed=`. If the composed preset is not in `presets`, fix the allowlist. If there is no `agent-preset/selected` line, the mode was never committed. |
261
+ | `no userQuestions service -> cancel (fail closed)` | The approval surface is not mounted in this profile. |
262
+ | `refusing to approve the app hosting this agent` | Working as designed; ask for a different app. |
263
+ | Calls blocked after a turn | The runtime's turn cleanup had not settled; the plugin retries it before the next call and refuses until it does. |
264
+ | `lcu doctor` reports a socket-path error | The signed helper binds under your home folder and refuses a path over 103 bytes. Use an account with a shorter home path. |
265
+ | Attach fails with a spawn error | Run `~/.local/share/lcu/current/bin/lcu doctor` directly, then check `command` in the config. |
266
+
267
+ `node scripts/probe-lcu.mjs` talks to LCU with no harness involved and prints the protocol version,
268
+ server identity, instructions length and the tool list — useful to separate a plugin problem from an
269
+ LCU problem.
270
+
271
+ ## Companion tools
272
+
273
+ `scripts/` also ships two tools for the sibling Codex subagent bundle, because the same profile
274
+ usually wants both:
275
+
276
+ - **`update-codex.mjs`** — keeps the profile's `@openai/codex` on the newest release that still passes
277
+ three gates (handshake, protocol-schema assertions, and a real turn), rolling back automatically when
278
+ one fails. The published `@deepseek-ai/dsh-subagent-codex` pins `0.153.4`, which does not serve every
279
+ current ChatGPT-account model; this bumps it through a profile-level, scoped pnpm override. See
280
+ `node scripts/update-codex.mjs --help` for `--check`, `--verify-only`, `--to` and `--rollback`.
281
+ - **`codex-baseline.json`** — the last version that passed all three gates.
282
+
283
+ ## Known limitations and deferred work
284
+
285
+ - **A delegated child cannot be asked for approval.** DSH only accepts a human answer for a live
286
+ runtime root, so a subagent's LCU approval fails closed. Subagents can perform read-only work that
287
+ needs no approval; anything that needs one must be driven from the top-level session.
288
+ - **macOS turn cleanup can outlive the host's wait.** The signed helper occasionally answers the
289
+ `turn-ended` step slowly. The runtime keeps cleaning in the background and retries; the plugin blocks
290
+ the next call until it settles rather than acting on a half-torn-down desktop.
291
+ - **One LCU connection per Agent.** LCU's JavaScript session is per connection and its approvals are
292
+ bound to a real session and turn, so sharing one connection across Agents would interleave both.
293
+ - **`chrome` needs the extension.** Enabling the flag without the official ChatGPT extension, or without
294
+ `lcu browser install`, yields no browser surface.
295
+ - **Not verified on Linux.** See [Requirements](#requirements).
296
+
297
+ ## Development
298
+
299
+ ```sh
300
+ npm install
301
+ npm run typecheck # strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes
302
+ npm run build # emits lib/
303
+ npm test # node --test, no build step
304
+ ```
305
+
306
+ The connection suite talks to the **real** installed `lcu` and skips itself when none is present, so
307
+ `npm test` is meaningful locally and still passes on CI. The approval, projection and guard suites are
308
+ pure and always run.
309
+
310
+ Plugin code and configuration are **not hot-reloaded** by the harness: a running process keeps the
311
+ module it loaded. Rebuild and restart to see a change.
312
+
313
+ ## License
314
+
315
+ MIT. LCU is MIT (Amont Labs); the ChatGPT application and its instructions remain under their own
316
+ terms and are used from your local installation.
@@ -0,0 +1,23 @@
1
+ # The dsh-plugin-lcu bundle patch.
2
+ #
3
+ # One row. The plugin owns every LCU connection and registers the model-facing
4
+ # computer-use tools per Agent; it starts nothing at load time, so a session pays
5
+ # for the CUA runtime only when its Agent preset is listed here.
6
+ #
7
+ # `presets` is an explicit allowlist rather than a capability the preset
8
+ # declares, because the server owns the tool schemas: they can only be fetched
9
+ # after the MCP handshake, which happens once a matching Agent is created.
10
+ #
11
+ # `chrome: true` enables LCU's browser surface (`lcu --chrome`). It also needs
12
+ # the official ChatGPT extension enabled in the Chrome profile and
13
+ # `lcu browser install`; sites stay exact-origin approvals.
14
+
15
+ - insert:
16
+ - id: lcu
17
+ name: 'dsh-plugin-lcu'
18
+ config:
19
+ presets:
20
+ - heavy
21
+ chrome: true
22
+ # allowedOrigins:
23
+ # - http://localhost:3000
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Development-only declarations for the harness packages this plugin consumes.
3
+ *
4
+ * Transcription of the surfaces actually used, from the running harness's own
5
+ * inspection output. Deliberately not published: the host supplies the real
6
+ * packages at runtime and every import of them here is type-only, so nothing in
7
+ * this file reaches the built plugin.
8
+ */
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // @deepseek-ai/dsh-agent
12
+ // ---------------------------------------------------------------------------
13
+
14
+ declare module '@deepseek-ai/dsh-agent' {
15
+ import type { Context } from '@deepseek-ai/cordis'
16
+
17
+ export type SessionId = string & { readonly __brand?: 'SessionId' }
18
+
19
+ /** The live session an agent drives; its log is the durable source of truth. */
20
+ export interface AgentSession {
21
+ readonly header: {
22
+ readonly cwd?: string
23
+ /** Id of the agent preset this session's agent was composed from. */
24
+ readonly agentPreset?: string
25
+ }
26
+ requestHeader(): { readonly config?: { readonly provider?: string; readonly model?: string } } | undefined
27
+ }
28
+
29
+ export interface Agent {
30
+ readonly id: SessionId
31
+ readonly options: { readonly provider?: string; readonly model?: string }
32
+ readonly session: AgentSession
33
+ /** Agent-scoped context: contributions unwind on disposal. */
34
+ readonly ctx: Context
35
+ }
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // @deepseek-ai/dsh-tools
40
+ // ---------------------------------------------------------------------------
41
+
42
+ declare module '@deepseek-ai/dsh-tools' {
43
+ import type { Agent } from '@deepseek-ai/dsh-agent'
44
+
45
+ /** A model-facing tool schema, as assembly projects it. */
46
+ export interface ToolSchema {
47
+ readonly name: string
48
+ readonly description: string
49
+ readonly parameters: Record<string, unknown>
50
+ }
51
+
52
+ /** One model-facing content block. */
53
+ export type ContentBlock =
54
+ | { readonly type: 'text'; readonly text: string }
55
+ | { readonly type: 'image'; readonly attachment: ImageAttachmentRef }
56
+ | { readonly type: string; readonly [key: string]: unknown }
57
+
58
+ /** A durable image reference minted by the attachment store. */
59
+ export interface ImageAttachmentRef {
60
+ readonly attachmentId: string
61
+ readonly mediaType: string
62
+ readonly bytes: number
63
+ readonly width: number
64
+ readonly height: number
65
+ readonly name?: string
66
+ }
67
+
68
+ /** Immutable identity plus cooperation surface for one call. */
69
+ export interface ToolRunContext {
70
+ readonly callId: string
71
+ readonly name: string
72
+ readonly arguments: unknown
73
+ readonly agent?: Agent
74
+ readonly signal: AbortSignal
75
+ }
76
+
77
+ /** Normalized outcome handed to post-execute policy and projection. */
78
+ export interface ToolExecutionResult {
79
+ readonly value: unknown
80
+ readonly content: readonly ContentBlock[]
81
+ readonly isError?: boolean
82
+ }
83
+
84
+ /** The execution identity a projection callback receives. */
85
+ export interface ToolExecution extends ToolRunContext {}
86
+
87
+ /** Declares the tool's canonical JSON value and its text fallback. */
88
+ export interface ToolOutputDefinition {
89
+ readonly schema: Record<string, unknown>
90
+ render(args: unknown, value: never): ContentBlock[]
91
+ }
92
+
93
+ /** A complete tool contribution. */
94
+ export interface ToolDefinition extends ToolSchema {
95
+ readonly output: ToolOutputDefinition
96
+ execute(args: never, exec: ToolRunContext): Promise<unknown>
97
+ projectContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
98
+ finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
99
+ readonly timeoutMs?: number
100
+ }
101
+
102
+ /** The tool registry a context exposes. */
103
+ export interface ToolRegistry {
104
+ register(definition: ToolDefinition): () => void
105
+ schemas(agent?: Agent): readonly ToolSchema[]
106
+ }
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // @deepseek-ai/dsh-system-prompt
111
+ // ---------------------------------------------------------------------------
112
+
113
+ declare module '@deepseek-ai/dsh-system-prompt' {
114
+ /** One ordered prompt section. */
115
+ export interface PromptSection {
116
+ readonly name: string
117
+ readonly order: number
118
+ readonly text: string | ((context: unknown) => string)
119
+ readonly interpolate?: boolean
120
+ readonly complete?: boolean
121
+ }
122
+
123
+ export interface SystemPromptRegistry {
124
+ section(section: PromptSection): () => void
125
+ context(context: { readonly name: string; readonly order: number; readonly text: string | ((context: unknown) => string) }): () => void
126
+ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // @deepseek-ai/dsh-attachment
131
+ // ---------------------------------------------------------------------------
132
+
133
+ declare module '@deepseek-ai/dsh-attachment' {
134
+ import type { ImageAttachmentRef } from '@deepseek-ai/dsh-tools'
135
+
136
+ /** One decoded image ready for durable storage. */
137
+ export interface SaveImageAttachment {
138
+ readonly data: Buffer
139
+ readonly mediaType: string
140
+ }
141
+
142
+ export interface AttachmentStore {
143
+ saveImages(images: readonly SaveImageAttachment[]): Promise<readonly ImageAttachmentRef[]>
144
+ }
145
+ }
146
+
147
+ // ---------------------------------------------------------------------------
148
+ // @deepseek-ai/dsh-llm
149
+ // ---------------------------------------------------------------------------
150
+
151
+ declare module '@deepseek-ai/dsh-llm' {
152
+ export interface ModelInfo {
153
+ readonly inputModalities?: readonly string[]
154
+ }
155
+
156
+ export interface LlmService {
157
+ resolveModelInfo(provider: string, model: string, signal: AbortSignal): Promise<ModelInfo>
158
+ }
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // @deepseek-ai/dsh-user-questions
163
+ // ---------------------------------------------------------------------------
164
+
165
+ declare module '@deepseek-ai/dsh-user-questions' {
166
+ import type { Agent } from '@deepseek-ai/dsh-agent'
167
+
168
+ export interface AskUserQuestionOption {
169
+ readonly label: string
170
+ readonly description?: string
171
+ }
172
+
173
+ export interface AskUserQuestionItem {
174
+ readonly id: string
175
+ readonly question: string
176
+ readonly detail?: string
177
+ readonly header?: string
178
+ readonly options?: readonly AskUserQuestionOption[]
179
+ readonly multiSelect?: boolean
180
+ }
181
+
182
+ export interface AskUserQuestionAnswer {
183
+ readonly answers: readonly {
184
+ readonly id: string
185
+ readonly selected: readonly string[]
186
+ readonly custom?: string
187
+ }[]
188
+ }
189
+
190
+ export interface UserQuestionsService {
191
+ ask(request: {
192
+ readonly questions: readonly AskUserQuestionItem[]
193
+ readonly agent?: Agent
194
+ readonly signal?: AbortSignal
195
+ }): Promise<AskUserQuestionAnswer>
196
+ }
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // @deepseek-ai/dsh-computer-use
201
+ // ---------------------------------------------------------------------------
202
+
203
+ declare module '@deepseek-ai/dsh-computer-use' {
204
+ export type ComputerUseProviderName = string & { readonly __brand?: 'ComputerUseProviderName' }
205
+
206
+ export interface ComputerUseRegistry {
207
+ /** Reserves the sole provider slot; a second registration fails. */
208
+ register(name: ComputerUseProviderName): () => Promise<void>
209
+ readonly providerName?: ComputerUseProviderName
210
+ }
211
+
212
+ export function ComputerUseProviderName(name: string): ComputerUseProviderName
213
+ }
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // @deepseek-ai/dsh-agent-preset-registry
217
+ // ---------------------------------------------------------------------------
218
+
219
+ declare module '@deepseek-ai/dsh-agent-preset-registry' {
220
+ import type { Context } from '@deepseek-ai/cordis'
221
+
222
+ export interface AgentPresetsService {
223
+ /**
224
+ * Read the preset a live Agent actually uses.
225
+ *
226
+ * This is the authoritative answer: the session header records the
227
+ * deployment default at creation, which the new-task picker may replace
228
+ * afterwards.
229
+ */
230
+ composedPreset(agentCtx: Context): string | undefined
231
+ }
232
+ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // @deepseek-ai/cordis
236
+ // ---------------------------------------------------------------------------
237
+
238
+ declare module '@deepseek-ai/cordis' {
239
+ import type { Agent } from '@deepseek-ai/dsh-agent'
240
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment'
241
+ import type { ComputerUseRegistry } from '@deepseek-ai/dsh-computer-use'
242
+ import type { LlmService } from '@deepseek-ai/dsh-llm'
243
+ import type { SystemPromptRegistry } from '@deepseek-ai/dsh-system-prompt'
244
+ import type { ToolRegistry } from '@deepseek-ai/dsh-tools'
245
+ import type { UserQuestionsService } from '@deepseek-ai/dsh-user-questions'
246
+ import type { AgentPresetsService } from '@deepseek-ai/dsh-agent-preset-registry'
247
+
248
+ /** The live-Agent registry, keyed by session id. */
249
+ export interface AgentRegistry {
250
+ get(id: string): Agent | undefined
251
+ }
252
+
253
+ export interface Logger {
254
+ debug(message: string): void
255
+ info(message: string): void
256
+ warn(message: string): void
257
+ error(message: string): void
258
+ }
259
+
260
+ /**
261
+ * The plugin context, narrowed to the services this plugin consumes.
262
+ *
263
+ * Optional services are read with `get()` so a composition without them still
264
+ * activates everything else.
265
+ */
266
+ export interface Context {
267
+ readonly tools: ToolRegistry
268
+ readonly systemPrompt: SystemPromptRegistry
269
+ readonly logger: Logger
270
+ get(name: 'attachments'): AttachmentStore | undefined
271
+ get(name: 'llm'): LlmService | undefined
272
+ get(name: 'userQuestions'): UserQuestionsService | undefined
273
+ get(name: 'computerUse'): ComputerUseRegistry | undefined
274
+ get(name: 'agentPresets'): AgentPresetsService | undefined
275
+ get(name: 'agents'): AgentRegistry | undefined
276
+ get(name: string): unknown
277
+ effect(callback: () => (() => void | Promise<void>)): () => void
278
+ on(event: 'agent/created', listener: (payload: { agent: Agent; signal?: AbortSignal }) => void | Promise<void>): () => void
279
+ on(
280
+ event: 'agent/turn-stopping',
281
+ listener: (payload: { agent: Agent; turn: number; signal: AbortSignal }) => void | Promise<void>,
282
+ ): () => void
283
+ /**
284
+ * The registry re-emits a session's committed preset choice. This fires
285
+ * AFTER `agent/created`: a new task is created with the deployment default
286
+ * and the picker's choice is applied on this event, so it is the only
287
+ * reliable moment to react to the preset a session will actually use.
288
+ */
289
+ on(event: 'agent-preset/selected', listener: (sessionId: string, agentPreset: string) => void | Promise<void>): () => void
290
+ on(event: string, listener: (...args: never[]) => unknown): () => void
291
+ }
292
+ }