@anionex/dsh-computer-use 0.3.1 → 0.3.3

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +4 -1
  3. package/README.zh.md +4 -1
  4. package/lib/client.js +1 -1
  5. package/lib/client.js.map +1 -1
  6. package/lib/config.js +49 -20
  7. package/lib/config.js.map +1 -1
  8. package/lib/confirmations.js.map +1 -1
  9. package/lib/exposure.js +19 -6
  10. package/lib/exposure.js.map +1 -1
  11. package/lib/index.js +5 -1
  12. package/lib/index.js.map +1 -1
  13. package/lib/leases.js +4 -1
  14. package/lib/leases.js.map +1 -1
  15. package/lib/providers/macos.js +27 -11
  16. package/lib/providers/macos.js.map +1 -1
  17. package/lib/skill.js +13 -2
  18. package/lib/skill.js.map +1 -1
  19. package/lib/tools.js +1 -1
  20. package/lib/tools.js.map +1 -1
  21. package/lib/types/client/index.d.ts +1 -1
  22. package/lib/types/client/index.d.ts.map +1 -1
  23. package/lib/types/config.d.ts +15 -2
  24. package/lib/types/config.d.ts.map +1 -1
  25. package/lib/types/confirmations.d.ts +1 -1
  26. package/lib/types/confirmations.d.ts.map +1 -1
  27. package/lib/types/exposure.d.ts.map +1 -1
  28. package/lib/types/index.d.ts.map +1 -1
  29. package/lib/types/leases.d.ts +2 -1
  30. package/lib/types/leases.d.ts.map +1 -1
  31. package/lib/types/providers/macos.d.ts +0 -1
  32. package/lib/types/providers/macos.d.ts.map +1 -1
  33. package/lib/types/skill.d.ts +7 -2
  34. package/lib/types/skill.d.ts.map +1 -1
  35. package/lib/types/tools.d.ts.map +1 -1
  36. package/lib/types/types.d.ts +3 -1
  37. package/lib/types/types.d.ts.map +1 -1
  38. package/lib/types/web.d.ts +2 -1
  39. package/lib/types/web.d.ts.map +1 -1
  40. package/lib/types.js.map +1 -1
  41. package/lib/web.js +34 -9
  42. package/lib/web.js.map +1 -1
  43. package/native/macos/Sources/Helper/CursorOverlay.swift +3 -0
  44. package/native/macos/Sources/Helper/main.swift +35 -19
  45. package/native/macos/bin/dsh-computer-use-helper +0 -0
  46. package/native/macos/manifest.json +3 -3
  47. package/package.json +100 -41
  48. package/scripts/build-client.mjs +0 -0
  49. package/scripts/build-native.mjs +0 -0
  50. package/scripts/model-e2e.mjs +7 -3
  51. package/scripts/session-transcript.mjs +4 -3
  52. package/scripts/validate.mjs +0 -0
  53. package/src/client/index.tsx +4 -2
  54. package/src/config.ts +53 -21
  55. package/src/confirmations.ts +1 -1
  56. package/src/exposure.ts +20 -6
  57. package/src/index.ts +5 -1
  58. package/src/leases.ts +6 -2
  59. package/src/providers/macos.ts +31 -11
  60. package/src/skill.ts +14 -2
  61. package/src/tools.ts +3 -2
  62. package/src/types.ts +2 -1
  63. package/src/web.ts +37 -7
@@ -48,7 +48,7 @@ export class ComputerConfirmationManager {
48
48
  agent: Agent,
49
49
  app: ComputerAppIdentity,
50
50
  request: ComputerConfirmRequest,
51
- callId: import('@deepseek-ai/dsh-llm').CallId | undefined,
51
+ callId: import('@deepseek-ai/dsh-tools').ToolRunContext['callId'] | undefined,
52
52
  signal: AbortSignal,
53
53
  ): Promise<ComputerConfirmation> {
54
54
  if (approvalPolicy(this.ctx, agent) === 'never') {
package/src/exposure.ts CHANGED
@@ -5,7 +5,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
5
5
  import type { Session } from '@deepseek-ai/dsh-session'
6
6
  import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
7
7
  import type { Context } from '@deepseek-ai/cordis'
8
- import { COMPUTER_USE_SKILL_CONTENT, COMPUTER_USE_SKILL_NAME } from './skill.ts'
8
+ import { COMPUTER_USE_SKILL_CONTENT, COMPUTER_USE_SKILL_NAME, COMPUTER_USE_SKILL_NAMES } from './skill.ts'
9
9
 
10
10
  /** One global bootstrap retained until the current Agent loads the Skill. */
11
11
  export const COMPUTER_USE_ACTIVATE = 'computer_use_activate'
@@ -60,7 +60,7 @@ function adHocOcrCommand(value: unknown): boolean {
60
60
  }
61
61
 
62
62
  function isSkillArguments(value: unknown): boolean {
63
- return isRecord(value) && value.name === COMPUTER_USE_SKILL_NAME
63
+ return isRecord(value) && COMPUTER_USE_SKILL_NAMES.has(value.name as string)
64
64
  }
65
65
 
66
66
  function nativeSkillCall(raw: string): boolean {
@@ -76,18 +76,32 @@ function containsSkillContent(blocks: readonly unknown[]): boolean {
76
76
 
77
77
  function isSkillResult(value: unknown): boolean {
78
78
  return isRecord(value)
79
- && value.name === COMPUTER_USE_SKILL_NAME
79
+ && COMPUTER_USE_SKILL_NAMES.has(value.name as string)
80
80
  && value.content === COMPUTER_USE_SKILL_CONTENT
81
81
  }
82
82
 
83
+ /**
84
+ * Session event types carrying one PTC (`run_code`) sub-dispatch.
85
+ *
86
+ * DSH 0.1.5 renamed the append site from `tool/code-dispatch` to
87
+ * `tool/ptc-dispatch` (`dsh-tools`) with an identical payload, and its session
88
+ * format migration renames replayed legacy events to the new name, so a log
89
+ * can legitimately carry either. Both are accepted: a pre-0.1.5 host only ever
90
+ * appends the old name, a 0.1.5 host only the new one.
91
+ */
92
+ const PTC_DISPATCH_EVENT_TYPES: ReadonlySet<string> = new Set(['tool/ptc-dispatch', 'tool/code-dispatch'])
93
+
83
94
  /** Whether durable Session history proves that the bundled Skill was loaded. */
84
95
  export function hasLoadedComputerUseSkill(session: Session): boolean {
85
96
  const nativeCalls = new Set<string>()
86
- for (const event of session.events) {
97
+ const events = typeof (session as any).snapshotEvents === 'function'
98
+ ? (session as any).snapshotEvents()
99
+ : (session as unknown as { events?: readonly import('@deepseek-ai/dsh-session').SessionEvent[] }).events ?? []
100
+ for (const event of events) {
87
101
  if (event.type === 'user/message') {
88
102
  const source = event.data.source
89
103
  if (source.kind === 'skill-invocation'
90
- && source.name === COMPUTER_USE_SKILL_NAME
104
+ && COMPUTER_USE_SKILL_NAMES.has(source.name)
91
105
  && containsSkillContent(event.data.content)) return true
92
106
  continue
93
107
  }
@@ -103,7 +117,7 @@ export function hasLoadedComputerUseSkill(session: Session): boolean {
103
117
  && containsSkillContent(block.content)) return true
104
118
  continue
105
119
  }
106
- if (event.type === 'tool/code-dispatch'
120
+ if (PTC_DISPATCH_EVENT_TYPES.has(event.type)
107
121
  && event.data.name === 'skill'
108
122
  && event.data.isError === false
109
123
  && isSkillArguments(event.data.arguments)
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ import type {} from '@deepseek-ai/dsh-tools'
8
8
  import { Config, type ComputerUseConfig } from './config.ts'
9
9
  import { ComputerUseExposure } from './exposure.ts'
10
10
  import { MacOSComputerUseProvider } from './providers/macos.ts'
11
- import { COMPUTER_USE_SKILL } from './skill.ts'
11
+ import { COMPUTER_USE_SKILL, LEGACY_COMPUTER_USE_SKILL } from './skill.ts'
12
12
  import { createComputerUseTools } from './tools.ts'
13
13
  import { installComputerUseWeb } from './web.ts'
14
14
 
@@ -22,14 +22,17 @@ export function installComputerUseConsumer(ctx: Context): () => void {
22
22
  const exposure = new ComputerUseExposure(ctx, () => createComputerUseTools(ctx.computerUse))
23
23
  let activation: (() => void) | undefined
24
24
  let skill: (() => void) | undefined
25
+ let legacySkill: (() => void) | undefined
25
26
  let exposureDispose: (() => void) | undefined
26
27
  try {
27
28
  activation = ctx.tools.register(exposure.activationTool)
28
29
  skill = ctx.skills.register(COMPUTER_USE_SKILL)
30
+ legacySkill = ctx.skills.register(LEGACY_COMPUTER_USE_SKILL)
29
31
  exposureDispose = exposure.install()
30
32
  installComputerUseWeb(ctx)
31
33
  } catch (error) {
32
34
  exposureDispose?.()
35
+ legacySkill?.()
33
36
  skill?.()
34
37
  activation?.()
35
38
  throw error
@@ -37,6 +40,7 @@ export function installComputerUseConsumer(ctx: Context): () => void {
37
40
  return () => {
38
41
  exposureDispose?.()
39
42
  activation?.()
43
+ legacySkill?.()
40
44
  skill?.()
41
45
  }
42
46
  }
package/src/leases.ts CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { z } from 'zod'
4
4
  import type { Agent } from '@deepseek-ai/dsh-agent'
5
- import type { CallId } from '@deepseek-ai/dsh-llm'
5
+ import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
6
+ type CallId = ToolRunContext['callId']
6
7
  import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
7
8
  import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain'
8
9
  import type { KvTable } from '@deepseek-ai/dsh-storage-domain'
@@ -185,7 +186,10 @@ export class ComputerLeaseManager {
185
186
  signal: AbortSignal,
186
187
  ): Promise<ComputerLeaseSource> {
187
188
  await this.prepareStorage()
188
- const turn = currentTurn(agent.session.events)
189
+ const sessionEvents = typeof (agent.session as any).snapshotEvents === 'function'
190
+ ? (agent.session as any).snapshotEvents()
191
+ : (agent.session as unknown as { events?: readonly SessionEvent[] }).events ?? []
192
+ const turn = currentTurn(sessionEvents)
189
193
  if (turn === undefined) {
190
194
  throw new ComputerUseError('COMPUTER_PERMISSION_REQUIRED', `${scope} access for ${app.name} must be requested inside an open Agent turn`)
191
195
  }
@@ -21,6 +21,7 @@ import type {
21
21
  import {
22
22
  Config,
23
23
  COMPUTER_USE_SETTINGS_NAMESPACE,
24
+ readComputerUseConfig,
24
25
  resolveConfig,
25
26
  type ComputerUseConfig,
26
27
  type ResolvedComputerUseConfig,
@@ -253,21 +254,26 @@ export class MacOSComputerUseProvider extends ComputerUseService {
253
254
  static inject = ['subprocess', 'approval', 'settings', 'sessions', 'agents']
254
255
  static Config = Config
255
256
 
256
- private readonly settings
257
-
258
257
  constructor(ctx: Context, config: ComputerUseConfig = {}) {
259
- const settings = ctx.settings.register(COMPUTER_USE_SETTINGS_NAMESPACE, Config, {
260
- base: config,
261
- applies: 'live',
262
- validate: (value) => { resolveConfig(value) },
263
- })
264
- const resolved = resolveConfig(settings.get())
258
+ const legacySettings = ctx.settings as typeof ctx.settings & {
259
+ register?: (ns: typeof COMPUTER_USE_SETTINGS_NAMESPACE, schema: typeof Config, options: object) => {
260
+ get(): ComputerUseConfig
261
+ watch(listener: (value: ComputerUseConfig) => Promise<void>): () => void
262
+ }
263
+ }
264
+ const settings = typeof legacySettings.register === 'function'
265
+ ? legacySettings.register(COMPUTER_USE_SETTINGS_NAMESPACE, Config, {
266
+ base: config,
267
+ applies: 'live',
268
+ validate: (value: ComputerUseConfig) => { resolveConfig(value) },
269
+ })
270
+ : undefined
271
+ const resolved = resolveConfig(settings?.get() ?? readComputerUseConfig(config))
265
272
  super(ctx, createBackend(ctx, resolved), resolved)
266
- this.settings = settings
267
273
  if (process.platform !== 'darwin') {
268
274
  ctx.logger.warn('dsh-computer-use: supports macOS only; Computer Use Tools are disabled on %s', process.platform)
269
275
  }
270
- ctx.effect(() => this.settings.watch(async (next) => {
276
+ const reconfigure = async (next: ComputerUseConfig): Promise<void> => {
271
277
  const candidate = resolveConfig(next)
272
278
  const backend = createBackend(ctx, candidate)
273
279
  try {
@@ -276,7 +282,21 @@ export class MacOSComputerUseProvider extends ComputerUseService {
276
282
  await backend.dispose()
277
283
  throw error
278
284
  }
279
- }), 'dsh-computer-use: Settings watch')
285
+ }
286
+ if (settings) {
287
+ ctx.effect(() => settings.watch(reconfigure), 'dsh-computer-use: Settings watch')
288
+ } else {
289
+ // DSH 0.1.7 projects Settings from Config and commits volatile fields in
290
+ // place. The Loader emits this event after its references have changed.
291
+ ctx.effect(() => {
292
+ const onVolatileUpdate = ctx.on as unknown as (name: string, listener: () => void) => () => void
293
+ return onVolatileUpdate.call(ctx, 'loader/volatile-update', () => {
294
+ void reconfigure(readComputerUseConfig(config)).catch(error => {
295
+ ctx.logger.warn('dsh-computer-use: live config update failed: %s', error)
296
+ })
297
+ })
298
+ }, 'dsh-computer-use: Loader volatile update')
299
+ }
280
300
  ctx.effect(() => ctx.on('agent/disposed', ({ agent }) => { this.releaseAgent(agent) }), 'dsh-computer-use: Agent cleanup')
281
301
  }
282
302
 
package/src/skill.ts CHANGED
@@ -2,8 +2,14 @@
2
2
 
3
3
  import type { SkillRegistration } from '@deepseek-ai/dsh-skill'
4
4
 
5
- /** Stable Skill name used by progressive exposure and durable restore. */
6
- export const COMPUTER_USE_SKILL_NAME = 'computer-use'
5
+ /** Namespaced entry avoids collisions with filesystem Skills in Agent presets. */
6
+ export const COMPUTER_USE_SKILL_NAME = 'dsh-computer-use'
7
+ /** Existing sessions and slash-command workflows continue to recognize this entry. */
8
+ export const LEGACY_COMPUTER_USE_SKILL_NAME = 'computer-use'
9
+ export const COMPUTER_USE_SKILL_NAMES: ReadonlySet<string> = new Set([
10
+ COMPUTER_USE_SKILL_NAME,
11
+ LEGACY_COMPUTER_USE_SKILL_NAME,
12
+ ])
7
13
 
8
14
  /** Complete model-visible operating and confirmation workflow. */
9
15
  export const COMPUTER_USE_SKILL_CONTENT = `# DSH Computer Use
@@ -127,3 +133,9 @@ export const COMPUTER_USE_SKILL: SkillRegistration = {
127
133
  source: 'runtime',
128
134
  content: COMPUTER_USE_SKILL_CONTENT,
129
135
  }
136
+
137
+ /** Compatibility alias for existing Sessions; preset Skills may shadow it. */
138
+ export const LEGACY_COMPUTER_USE_SKILL: SkillRegistration = {
139
+ ...COMPUTER_USE_SKILL,
140
+ name: LEGACY_COMPUTER_USE_SKILL_NAME,
141
+ }
package/src/tools.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /** Focused model-facing Computer Use Tool definitions. */
2
2
 
3
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
3
4
  import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
4
- import { defineTool, type JsonValue, type ToolDefinition, type ToolRunContext, type ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
5
+ import { defineTool, type ToolDefinition, type ToolRunContext, type ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
5
6
  import {
6
7
  ComputerConfirmationToken,
7
8
  ComputerObservationId,
@@ -38,7 +39,7 @@ function deferVisionHandoff(exec: ToolRunContext, artifact: ComputerArtifact | u
38
39
  'Do not inspect OCR executables or use bash, tesseract, screencapture, or an ad hoc Swift/Python OCR implementation.',
39
40
  ].join(' '),
40
41
  }],
41
- source: { kind: 'plugin', plugin: 'dsh-computer-use' },
42
+ source: { kind: 'tool', callId: exec.callId },
42
43
  }))
43
44
  }
44
45
 
package/src/types.ts CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  import type { Agent } from '@deepseek-ai/dsh-agent'
4
4
  import type { Branded } from '@deepseek-ai/dsh-brand'
5
- import type { CallId } from '@deepseek-ai/dsh-llm'
5
+ import type { ToolRunContext } from '@deepseek-ai/dsh-tools'
6
+ type CallId = ToolRunContext['callId']
6
7
 
7
8
  /** Opaque identifier for one immutable observed UI state. */
8
9
  export type ComputerObservationId = Branded<'ComputerObservationId'>
package/src/web.ts CHANGED
@@ -6,6 +6,7 @@ import { SettingsConflictError, type SettingsDescriptor } from '@deepseek-ai/dsh
6
6
  import type {} from '@deepseek-ai/dsh-host-webserver'
7
7
  import {
8
8
  COMPUTER_USE_SETTINGS_NAMESPACE,
9
+ resolveConfig,
9
10
  type ComputerUseConfig,
10
11
  } from './config.ts'
11
12
  import type { ComputerUseStatus } from './types.ts'
@@ -59,11 +60,17 @@ function requestError(res: ServerResponse, status: number, code: string, message
59
60
  responseJson(res, status, { ok: false, error: { code, message } })
60
61
  }
61
62
 
62
- function sameOriginPost(req: IncomingMessage): boolean {
63
+ function sameOriginPost(req: IncomingMessage, authorizeUnmarkedPost?: (req: IncomingMessage) => boolean): boolean {
63
64
  const fetchSite = req.headers['sec-fetch-site']
64
65
  if (fetchSite === 'cross-site') return false
65
66
  const origin = req.headers.origin
66
- if (origin === undefined) return fetchSite === 'same-origin' || fetchSite === 'same-site' || fetchSite === 'none'
67
+ if (origin === undefined) {
68
+ if (fetchSite === 'same-origin' || fetchSite === 'same-site' || fetchSite === 'none') return true
69
+ // Desktop strips browser origin metadata before forwarding, but injects a
70
+ // Host-issued signed cookie. Verify it through Connection's own fence;
71
+ // mere Cookie header presence is not authentication.
72
+ return fetchSite === undefined && authorizeUnmarkedPost?.(req) === true
73
+ }
67
74
  const host = req.headers.host
68
75
  if (host === undefined) return false
69
76
  try {
@@ -106,9 +113,27 @@ function publicMessage(error: unknown): string {
106
113
  return error instanceof Error ? error.message.slice(0, 1000) : String(error).slice(0, 1000)
107
114
  }
108
115
 
116
+ /**
117
+ * Whether one thrown value is the Settings provider's optimistic-concurrency
118
+ * conflict. The provider — not this module — throws it, so a second copy of
119
+ * `SettingsConflictError` (plugin-local versus host-local install) makes
120
+ * `instanceof` alone unreliable. Both the 0.1.5 and the older class carry the
121
+ * stable machine code `"SETTINGS_CONFLICT"`, which is checked first-class;
122
+ * `instanceof` stays as an equivalent additional branch.
123
+ */
124
+ function isSettingsConflict(error: unknown): boolean {
125
+ if (error instanceof SettingsConflictError) return true
126
+ return typeof error === 'object'
127
+ && error !== null
128
+ && (error as { code?: unknown }).code === 'SETTINGS_CONFLICT'
129
+ }
130
+
109
131
  /** Same-origin backend used by the optional client Settings section. */
110
132
  export class ComputerUseWebBackend {
111
- constructor(private readonly ctx: Context) {}
133
+ constructor(
134
+ private readonly ctx: Context,
135
+ private readonly authorizeUnmarkedPost?: (req: IncomingMessage) => boolean,
136
+ ) {}
112
137
 
113
138
  /** Current browser-safe Settings and health state. */
114
139
  snapshot(): ComputerUseSettingsSnapshot {
@@ -139,7 +164,7 @@ export class ComputerUseWebBackend {
139
164
  requestError(res, 405, 'method-not-allowed', 'Use GET or POST')
140
165
  return
141
166
  }
142
- if (!sameOriginPost(req)) {
167
+ if (!sameOriginPost(req, this.authorizeUnmarkedPost)) {
143
168
  requestError(res, 403, 'origin-rejected', 'The request must originate from this DSH Web application')
144
169
  return
145
170
  }
@@ -152,6 +177,7 @@ export class ComputerUseWebBackend {
152
177
  try {
153
178
  if (parsed.action === 'save') {
154
179
  if (!this.ctx.settings.writable) throw new Error('settings provider is read-only')
180
+ resolveConfig(parsed.value)
155
181
  await this.ctx.settings.replace(COMPUTER_USE_SETTINGS_NAMESPACE, parsed.value as object, parsed.expectedRevision)
156
182
  } else if (parsed.action === 'health') {
157
183
  await this.ctx.computerUse.health(AbortSignal.timeout(30000))
@@ -160,7 +186,7 @@ export class ComputerUseWebBackend {
160
186
  }
161
187
  responseJson(res, 200, { ok: true, value: this.snapshot() })
162
188
  } catch (error) {
163
- const conflict = error instanceof SettingsConflictError
189
+ const conflict = isSettingsConflict(error)
164
190
  this.ctx.logger.warn('dsh-computer-use Web action=%s failed: %s', parsed.action, publicMessage(error))
165
191
  requestError(res, conflict ? 409 : 400, conflict ? 'settings-conflict' : 'action-failed', publicMessage(error))
166
192
  }
@@ -169,8 +195,12 @@ export class ComputerUseWebBackend {
169
195
 
170
196
  /** Attach the optional route when a Web host is present. */
171
197
  export function installComputerUseWeb(ctx: Context): void {
172
- const backend = new ComputerUseWebBackend(ctx)
173
- ctx.inject(['webServer'], (webCtx) => {
198
+ ctx.inject(['webServer', 'connection'], (webCtx) => {
199
+ const connection = (webCtx as Context & {
200
+ connection?: { requestRejection?: (req: IncomingMessage) => 401 | 403 | undefined }
201
+ }).connection
202
+ const backend = new ComputerUseWebBackend(webCtx, (req) =>
203
+ typeof connection?.requestRejection === 'function' && connection.requestRejection(req) === undefined)
174
204
  webCtx.effect(() => webCtx.webServer.register({
175
205
  kind: 'exact',
176
206
  path: COMPUTER_USE_SETTINGS_ROUTE,