@anionex/dsh-computer-use 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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +307 -0
  4. package/README.zh.md +307 -0
  5. package/assets/computer-use-fixture.png +0 -0
  6. package/assets/cursor.png +0 -0
  7. package/cordis.patch.yml +13 -0
  8. package/docs/interaction-policy.i18n.yaml +4 -0
  9. package/docs/interaction-policy.md +140 -0
  10. package/docs/interaction-policy.zh.md +140 -0
  11. package/lib/approval-policy.js +12 -0
  12. package/lib/approval-policy.js.map +1 -0
  13. package/lib/artifacts.js +79 -0
  14. package/lib/artifacts.js.map +1 -0
  15. package/lib/backend.js +3 -0
  16. package/lib/backend.js.map +1 -0
  17. package/lib/client.js +349 -0
  18. package/lib/client.js.map +1 -0
  19. package/lib/config.js +120 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/confirmations.js +99 -0
  22. package/lib/confirmations.js.map +1 -0
  23. package/lib/diff.js +58 -0
  24. package/lib/diff.js.map +1 -0
  25. package/lib/errors.js +22 -0
  26. package/lib/errors.js.map +1 -0
  27. package/lib/exposure.js +231 -0
  28. package/lib/exposure.js.map +1 -0
  29. package/lib/index.js +56 -0
  30. package/lib/index.js.map +1 -0
  31. package/lib/leases.js +259 -0
  32. package/lib/leases.js.map +1 -0
  33. package/lib/providers/macos.js +148 -0
  34. package/lib/providers/macos.js.map +1 -0
  35. package/lib/providers/native-helper.js +356 -0
  36. package/lib/providers/native-helper.js.map +1 -0
  37. package/lib/service.js +480 -0
  38. package/lib/service.js.map +1 -0
  39. package/lib/skill.js +119 -0
  40. package/lib/skill.js.map +1 -0
  41. package/lib/target-resolver.js +180 -0
  42. package/lib/target-resolver.js.map +1 -0
  43. package/lib/tools.js +493 -0
  44. package/lib/tools.js.map +1 -0
  45. package/lib/types/approval-policy.d.ts +13 -0
  46. package/lib/types/approval-policy.d.ts.map +1 -0
  47. package/lib/types/artifacts.d.ts +10 -0
  48. package/lib/types/artifacts.d.ts.map +1 -0
  49. package/lib/types/backend.d.ts +94 -0
  50. package/lib/types/backend.d.ts.map +1 -0
  51. package/lib/types/client/index.d.ts +78 -0
  52. package/lib/types/client/index.d.ts.map +1 -0
  53. package/lib/types/config.d.ts +75 -0
  54. package/lib/types/config.d.ts.map +1 -0
  55. package/lib/types/confirmations.d.ts +23 -0
  56. package/lib/types/confirmations.d.ts.map +1 -0
  57. package/lib/types/diff.d.ts +5 -0
  58. package/lib/types/diff.d.ts.map +1 -0
  59. package/lib/types/errors.d.ts +15 -0
  60. package/lib/types/errors.d.ts.map +1 -0
  61. package/lib/types/exposure.d.ts +30 -0
  62. package/lib/types/exposure.d.ts.map +1 -0
  63. package/lib/types/index.d.ts +21 -0
  64. package/lib/types/index.d.ts.map +1 -0
  65. package/lib/types/leases.d.ts +80 -0
  66. package/lib/types/leases.d.ts.map +1 -0
  67. package/lib/types/providers/macos.d.ts +15 -0
  68. package/lib/types/providers/macos.d.ts.map +1 -0
  69. package/lib/types/providers/native-helper.d.ts +36 -0
  70. package/lib/types/providers/native-helper.d.ts.map +1 -0
  71. package/lib/types/service.d.ts +52 -0
  72. package/lib/types/service.d.ts.map +1 -0
  73. package/lib/types/skill.d.ts +9 -0
  74. package/lib/types/skill.d.ts.map +1 -0
  75. package/lib/types/target-resolver.d.ts +38 -0
  76. package/lib/types/target-resolver.d.ts.map +1 -0
  77. package/lib/types/tools.d.ts +6 -0
  78. package/lib/types/tools.d.ts.map +1 -0
  79. package/lib/types/types.d.ts +246 -0
  80. package/lib/types/types.d.ts.map +1 -0
  81. package/lib/types/web.d.ts +32 -0
  82. package/lib/types/web.d.ts.map +1 -0
  83. package/lib/types.js +9 -0
  84. package/lib/types.js.map +1 -0
  85. package/lib/web.js +163 -0
  86. package/lib/web.js.map +1 -0
  87. package/native/macos/Sources/Fixture/main.swift +366 -0
  88. package/native/macos/Sources/Helper/CursorImage.swift +11 -0
  89. package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
  90. package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
  91. package/native/macos/Sources/Helper/main.swift +1140 -0
  92. package/native/macos/Sources/Monitor/main.swift +139 -0
  93. package/native/macos/bin/dsh-computer-use-helper +0 -0
  94. package/native/macos/manifest.json +14 -0
  95. package/package.json +128 -0
  96. package/scripts/build-client.mjs +24 -0
  97. package/scripts/build-native.mjs +158 -0
  98. package/scripts/check-native.mjs +92 -0
  99. package/scripts/model-e2e.mjs +291 -0
  100. package/scripts/session-transcript.mjs +117 -0
  101. package/scripts/validate.mjs +256 -0
  102. package/src/approval-policy.ts +16 -0
  103. package/src/artifacts.ts +93 -0
  104. package/src/backend.ts +103 -0
  105. package/src/client/index.tsx +539 -0
  106. package/src/config.ts +196 -0
  107. package/src/confirmations.ts +128 -0
  108. package/src/diff.ts +52 -0
  109. package/src/errors.ts +39 -0
  110. package/src/exposure.ts +236 -0
  111. package/src/index.ts +66 -0
  112. package/src/leases.ts +337 -0
  113. package/src/providers/macos.ts +185 -0
  114. package/src/providers/native-helper.ts +393 -0
  115. package/src/service.ts +583 -0
  116. package/src/skill.ts +123 -0
  117. package/src/target-resolver.ts +237 -0
  118. package/src/tools.ts +528 -0
  119. package/src/types.ts +299 -0
  120. package/src/web.ts +180 -0
package/src/types.ts ADDED
@@ -0,0 +1,299 @@
1
+ /** Public Computer Use types shared by the Service, provider, and Tool consumer. */
2
+
3
+ import type { Agent } from '@deepseek-ai/dsh-agent'
4
+ import type { Branded } from '@deepseek-ai/dsh-brand'
5
+ import type { CallId } from '@deepseek-ai/dsh-llm'
6
+
7
+ /** Opaque identifier for one immutable observed UI state. */
8
+ export type ComputerObservationId = Branded<'ComputerObservationId'>
9
+
10
+ /** Brand a generated observation identifier. */
11
+ export const ComputerObservationId = (value: string): ComputerObservationId => value as ComputerObservationId
12
+
13
+ /** Opaque reference to one element descriptor captured inside an observation. */
14
+ export type ComputerTargetHandle = Branded<'ComputerTargetHandle'>
15
+
16
+ /** Brand a generated target handle. */
17
+ export const ComputerTargetHandle = (value: string): ComputerTargetHandle => value as ComputerTargetHandle
18
+
19
+ /** Opaque one-use grant for one confirmed sensitive action. */
20
+ export type ComputerConfirmationToken = Branded<'ComputerConfirmationToken'>
21
+
22
+ /** Brand a generated confirmation token. */
23
+ export const ComputerConfirmationToken = (value: string): ComputerConfirmationToken => value as ComputerConfirmationToken
24
+
25
+ /** Rectangle in screen-global point coordinates unless a containing type states otherwise. */
26
+ export interface ComputerRect {
27
+ x: number
28
+ y: number
29
+ width: number
30
+ height: number
31
+ }
32
+
33
+ /** macOS permission state reported without attempting to grant it. */
34
+ export type ComputerPermissionState = 'granted' | 'denied' | 'not-determined' | 'unavailable'
35
+
36
+ /** Stable identity of one running user-facing application. */
37
+ export interface ComputerAppIdentity {
38
+ bundleId: string
39
+ pid: number
40
+ name: string
41
+ }
42
+
43
+ /** Application selector accepted from a model or caller. */
44
+ export interface ComputerAppSelector {
45
+ bundleId?: string
46
+ pid?: number
47
+ name?: string
48
+ }
49
+
50
+ /** Bounded application row returned by discovery. */
51
+ export interface ComputerAppSummary extends ComputerAppIdentity {
52
+ frontmost: boolean
53
+ accessibility: ComputerPermissionState
54
+ screenRecording: ComputerPermissionState
55
+ }
56
+
57
+ /** One model-addressable element. Its index and opaque handle belong only to the enclosing observation. */
58
+ export interface ComputerElement {
59
+ index: number
60
+ targetHandle: ComputerTargetHandle
61
+ role: string
62
+ subrole?: string
63
+ title?: string
64
+ label?: string
65
+ value?: string
66
+ enabled?: boolean
67
+ focused?: boolean
68
+ selected?: boolean
69
+ frame?: ComputerRect
70
+ actions: string[]
71
+ }
72
+
73
+ /** Deterministic route used to resolve a target immediately before input. */
74
+ export type ComputerTargetResolutionMode =
75
+ | 'exact-locator'
76
+ | 'native-identifier'
77
+ | 'semantic-rebind'
78
+
79
+ /** Model-visible evidence describing how an element target was resolved. */
80
+ export interface ComputerTargetResolutionResult {
81
+ mode: ComputerTargetResolutionMode
82
+ confidence: number
83
+ candidateCount: number
84
+ targetChanged: boolean
85
+ }
86
+
87
+ /** File artifact emitted by a Computer Use observation. */
88
+ export interface ComputerArtifact {
89
+ path: string
90
+ filename: string
91
+ mimeType: 'image/png'
92
+ kind: 'image'
93
+ description: string
94
+ sourceTool: 'computer_observe' | 'computer_action'
95
+ previewIntent: 'image'
96
+ bytes: number
97
+ width: number
98
+ height: number
99
+ }
100
+
101
+ /** Complete model-visible observation. */
102
+ export interface ComputerObservation {
103
+ observationId: ComputerObservationId
104
+ app: ComputerAppIdentity
105
+ createdAt: string
106
+ expiresAt: string
107
+ frontmost: boolean
108
+ window?: {
109
+ title?: string
110
+ frame: ComputerRect
111
+ id?: number
112
+ }
113
+ tree: {
114
+ mode: 'full' | 'diff'
115
+ text: string
116
+ truncated: boolean
117
+ }
118
+ elements: ComputerElement[]
119
+ screenshot?: ComputerArtifact
120
+ permissions: {
121
+ accessibility: ComputerPermissionState
122
+ screenRecording: ComputerPermissionState
123
+ }
124
+ }
125
+
126
+ /** Whether screenshot capture is omitted, best-effort, or required. */
127
+ export type ComputerScreenshotMode = 'none' | 'optional' | 'required'
128
+
129
+ /** Request for a fresh observation. */
130
+ export interface ComputerObserveRequest {
131
+ app: ComputerAppSelector
132
+ screenshot?: ComputerScreenshotMode
133
+ full?: boolean
134
+ }
135
+
136
+ /** Closed set of supported key modifiers. */
137
+ export type ComputerKeyModifier = 'command' | 'control' | 'option' | 'shift'
138
+
139
+ /** Closed set of supported mouse buttons. */
140
+ export type ComputerMouseButton = 'left' | 'right' | 'middle'
141
+
142
+ /** Coordinate space accepted by coordinate-based pointer actions. */
143
+ export type ComputerCoordinateSpace = 'window' | 'screen'
144
+
145
+ /** Shared fields carried by every action against an existing observation. */
146
+ export interface ComputerActionBase {
147
+ observationId: ComputerObservationId
148
+ /** True only when the Skill has classified the action as requiring just-in-time confirmation. */
149
+ sensitive?: boolean
150
+ /** One-use token returned by {@link ComputerUseService.confirm}. */
151
+ confirmationToken?: ComputerConfirmationToken
152
+ }
153
+
154
+ /** Stable target selection fields shared by element-addressed actions. */
155
+ export interface ComputerElementTarget {
156
+ /** Observation-local compatibility index. It does not authorize rebinding by itself. */
157
+ elementIndex?: number
158
+ /** Opaque handle returned on the selected observation element. */
159
+ targetHandle?: ComputerTargetHandle
160
+ /** Permit deterministic native-identifier or unique semantic rebinding. */
161
+ allowRebind?: boolean
162
+ }
163
+
164
+ /** Click an observed element or an observed-window coordinate. */
165
+ export interface ComputerClickAction extends ComputerActionBase, ComputerElementTarget {
166
+ kind: 'click'
167
+ x?: number
168
+ y?: number
169
+ /** `window` (default) resolves `x`/`y` inside the observed window frame; `screen` treats them as Quartz screen-global points. */
170
+ coordinateSpace?: ComputerCoordinateSpace
171
+ button?: ComputerMouseButton
172
+ clickCount?: number
173
+ allowCoordinateFallback?: boolean
174
+ }
175
+
176
+ /** Set the Accessibility value of an observed editable element. */
177
+ export interface ComputerSetValueAction extends ComputerActionBase, ComputerElementTarget {
178
+ kind: 'set-value'
179
+ value: string
180
+ }
181
+
182
+ /** Insert Unicode into the currently focused control through Accessibility or keyboard fallback, without using the clipboard. */
183
+ export interface ComputerTypeTextAction extends ComputerActionBase {
184
+ kind: 'type-text'
185
+ text: string
186
+ }
187
+
188
+ /** Press one validated key chord. */
189
+ export interface ComputerPressKeyAction extends ComputerActionBase {
190
+ kind: 'press-key'
191
+ key: string
192
+ modifiers?: ComputerKeyModifier[]
193
+ }
194
+
195
+ /** Scroll at an observed element or coordinate. */
196
+ export interface ComputerScrollAction extends ComputerActionBase, ComputerElementTarget {
197
+ kind: 'scroll'
198
+ x?: number
199
+ y?: number
200
+ coordinateSpace?: ComputerCoordinateSpace
201
+ direction: 'up' | 'down' | 'left' | 'right'
202
+ pages?: number
203
+ }
204
+
205
+ /** Drag between two points in the observed window or screen coordinate space. */
206
+ export interface ComputerDragAction extends ComputerActionBase {
207
+ kind: 'drag'
208
+ fromX: number
209
+ fromY: number
210
+ toX: number
211
+ toY: number
212
+ coordinateSpace?: ComputerCoordinateSpace
213
+ }
214
+
215
+ /** Perform one Accessibility action advertised by an observed element. */
216
+ export interface ComputerPerformAction extends ComputerActionBase, ComputerElementTarget {
217
+ kind: 'perform-action'
218
+ action: string
219
+ }
220
+
221
+ /** Wait for a bounded UI condition without mutating state. */
222
+ export interface ComputerWaitAction extends ComputerActionBase {
223
+ kind: 'wait'
224
+ condition: {
225
+ text?: string
226
+ elementRole?: string
227
+ elementTitle?: string
228
+ }
229
+ timeoutMs?: number
230
+ }
231
+
232
+ /** Every model-accessible Computer Use action. */
233
+ export type ComputerActionRequest =
234
+ | ComputerClickAction
235
+ | ComputerSetValueAction
236
+ | ComputerTypeTextAction
237
+ | ComputerPressKeyAction
238
+ | ComputerScrollAction
239
+ | ComputerDragAction
240
+ | ComputerPerformAction
241
+ | ComputerWaitAction
242
+
243
+ /** Result of a successful action followed by a fresh observation. */
244
+ export interface ComputerActionResult {
245
+ action: ComputerActionRequest['kind']
246
+ channel: 'accessibility' | 'coordinates' | 'keyboard' | 'wait'
247
+ /** Verifiable target-app foreground state transition requested by the helper for this action. */
248
+ activation: 'not-requested' | 'already-frontmost' | 'activated'
249
+ /** Whether the helper emitted mouse, drag, or scroll-wheel input to the target process. */
250
+ pointerInput: boolean
251
+ /** Pointer-event route selected by the helper; global HID routing is not supported. */
252
+ pointerRouting: 'none' | 'target-process'
253
+ /** Present when the action addressed an observed element. */
254
+ resolution?: ComputerTargetResolutionResult
255
+ observation: ComputerObservation
256
+ }
257
+
258
+ /** Confirmation request binding an exact proposed action to human-readable impact. */
259
+ export interface ComputerConfirmRequest {
260
+ action: Omit<ComputerActionRequest, 'confirmationToken'>
261
+ reason: string
262
+ target: string
263
+ dataSummary?: string
264
+ }
265
+
266
+ /** One-use confirmation result. */
267
+ export interface ComputerConfirmation {
268
+ token: ComputerConfirmationToken
269
+ observationId: ComputerObservationId
270
+ app: ComputerAppIdentity
271
+ expiresAt: string
272
+ }
273
+
274
+ /** Caller context required for scoping, cancellation, approval audit, and artifact placement. */
275
+ export interface ComputerUseContext {
276
+ agent: Agent
277
+ workspace: string
278
+ callId?: CallId
279
+ signal: AbortSignal
280
+ }
281
+
282
+ /** Provider and permission diagnostics exposed to Settings. */
283
+ export interface ComputerUseStatus {
284
+ platform: NodeJS.Platform
285
+ provider: 'macos-ax'
286
+ generation: number
287
+ ready: boolean
288
+ helperPath: string
289
+ helperVersion?: string
290
+ helperSha256?: string
291
+ accessibility: ComputerPermissionState
292
+ screenRecording: ComputerPermissionState
293
+ lastError?: string
294
+ }
295
+
296
+ // Type-only circular reference for the link above; the emitted declaration
297
+ // retains the Service name without importing runtime code into this module.
298
+ import type { ComputerUseService } from './service.ts'
299
+ void (0 as unknown as ComputerUseService | undefined)
package/src/web.ts ADDED
@@ -0,0 +1,180 @@
1
+ /** Optional Web Settings and provider-health route. */
2
+
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+ import type { Context } from '@deepseek-ai/cordis'
5
+ import { SettingsConflictError, type SettingsDescriptor } from '@deepseek-ai/dsh-settings'
6
+ import type {} from '@deepseek-ai/dsh-host-webserver'
7
+ import {
8
+ COMPUTER_USE_SETTINGS_NAMESPACE,
9
+ type ComputerUseConfig,
10
+ } from './config.ts'
11
+ import type { ComputerUseStatus } from './types.ts'
12
+
13
+ /** Exact same-origin Settings endpoint. */
14
+ export const COMPUTER_USE_SETTINGS_ROUTE = '/_dsh/computer-use/settings'
15
+
16
+ /** Browser-safe Settings snapshot. */
17
+ export interface ComputerUseSettingsSnapshot {
18
+ schemaVersion: 1
19
+ writable: boolean
20
+ settings: {
21
+ value: ComputerUseConfig
22
+ user?: unknown
23
+ base?: unknown
24
+ revision: number
25
+ applies: 'live'
26
+ }
27
+ provider: ComputerUseStatus
28
+ }
29
+
30
+ type SettingsRequest =
31
+ | { action: 'save'; expectedRevision: number; value: ComputerUseConfig }
32
+ | { action: 'health' }
33
+ | { action: 'open-settings'; kind: 'accessibility' | 'screen-recording' }
34
+
35
+ type JsonResponse<T> = { ok: true; value: T } | { ok: false; error: { code: string; message: string } }
36
+
37
+ function isRecord(value: unknown): value is Record<string, unknown> {
38
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
39
+ }
40
+
41
+ function descriptorOf(ctx: Context): SettingsDescriptor {
42
+ const descriptor = ctx.settings.describe().find(row => row.ns === COMPUTER_USE_SETTINGS_NAMESPACE)
43
+ if (descriptor === undefined) throw new Error('computer-use Settings namespace is not registered')
44
+ return descriptor
45
+ }
46
+
47
+ function responseJson<T>(res: ServerResponse, status: number, body: JsonResponse<T>): void {
48
+ const bytes = Buffer.from(JSON.stringify(body))
49
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
50
+ res.setHeader('Content-Length', String(bytes.length))
51
+ res.setHeader('Cache-Control', 'no-store')
52
+ res.setHeader('X-Content-Type-Options', 'nosniff')
53
+ res.setHeader('Content-Security-Policy', "default-src 'none'; frame-ancestors 'none'")
54
+ res.writeHead(status)
55
+ res.end(bytes)
56
+ }
57
+
58
+ function requestError(res: ServerResponse, status: number, code: string, message: string): void {
59
+ responseJson(res, status, { ok: false, error: { code, message } })
60
+ }
61
+
62
+ function sameOriginPost(req: IncomingMessage): boolean {
63
+ const fetchSite = req.headers['sec-fetch-site']
64
+ if (fetchSite === 'cross-site') return false
65
+ const origin = req.headers.origin
66
+ if (origin === undefined) return fetchSite === 'same-origin' || fetchSite === 'same-site' || fetchSite === 'none'
67
+ const host = req.headers.host
68
+ if (host === undefined) return false
69
+ try {
70
+ const parsed = new URL(origin)
71
+ return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === host
72
+ } catch {
73
+ return false
74
+ }
75
+ }
76
+
77
+ async function readJson(req: IncomingMessage, maxBytes = 128 * 1024): Promise<unknown> {
78
+ const contentType = req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase()
79
+ if (contentType !== 'application/json') throw new TypeError('Content-Type must be application/json')
80
+ const chunks: Buffer[] = []
81
+ let bytes = 0
82
+ for await (const chunk of req) {
83
+ const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
84
+ bytes += part.length
85
+ if (bytes > maxBytes) throw new RangeError(`request body exceeds ${maxBytes} bytes`)
86
+ chunks.push(part)
87
+ }
88
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
89
+ }
90
+
91
+ function parseRequest(value: unknown): SettingsRequest {
92
+ if (!isRecord(value) || typeof value.action !== 'string') throw new TypeError('request needs an action')
93
+ if (value.action === 'health') return { action: 'health' }
94
+ if (value.action === 'open-settings') {
95
+ if (value.kind !== 'accessibility' && value.kind !== 'screen-recording') throw new TypeError('open-settings needs a valid kind')
96
+ return { action: 'open-settings', kind: value.kind }
97
+ }
98
+ if (value.action === 'save') {
99
+ if (!Number.isInteger(value.expectedRevision) || !isRecord(value.value)) throw new TypeError('save needs expectedRevision and value')
100
+ return { action: 'save', expectedRevision: value.expectedRevision as number, value: value.value as ComputerUseConfig }
101
+ }
102
+ throw new TypeError(`unknown action: ${value.action}`)
103
+ }
104
+
105
+ function publicMessage(error: unknown): string {
106
+ return error instanceof Error ? error.message.slice(0, 1000) : String(error).slice(0, 1000)
107
+ }
108
+
109
+ /** Same-origin backend used by the optional client Settings section. */
110
+ export class ComputerUseWebBackend {
111
+ constructor(private readonly ctx: Context) {}
112
+
113
+ /** Current browser-safe Settings and health state. */
114
+ snapshot(): ComputerUseSettingsSnapshot {
115
+ const descriptor = descriptorOf(this.ctx)
116
+ return {
117
+ schemaVersion: 1,
118
+ writable: this.ctx.settings.writable,
119
+ settings: {
120
+ value: descriptor.value as ComputerUseConfig,
121
+ ...(descriptor.user === undefined ? {} : { user: descriptor.user }),
122
+ ...(descriptor.base === undefined ? {} : { base: descriptor.base }),
123
+ revision: descriptor.revision,
124
+ applies: 'live',
125
+ },
126
+ provider: this.ctx.computerUse.status(),
127
+ }
128
+ }
129
+
130
+ /** Handle one Settings request. */
131
+ async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
132
+ if (req.method === 'GET') {
133
+ try { responseJson(res, 200, { ok: true, value: this.snapshot() }) }
134
+ catch (error) { requestError(res, 503, 'settings-unavailable', publicMessage(error)) }
135
+ return
136
+ }
137
+ if (req.method !== 'POST') {
138
+ res.setHeader('Allow', 'GET, POST')
139
+ requestError(res, 405, 'method-not-allowed', 'Use GET or POST')
140
+ return
141
+ }
142
+ if (!sameOriginPost(req)) {
143
+ requestError(res, 403, 'origin-rejected', 'The request must originate from this DSH Web application')
144
+ return
145
+ }
146
+ let parsed: SettingsRequest
147
+ try { parsed = parseRequest(await readJson(req)) }
148
+ catch (error) {
149
+ requestError(res, error instanceof RangeError ? 413 : 400, 'invalid-request', publicMessage(error))
150
+ return
151
+ }
152
+ try {
153
+ if (parsed.action === 'save') {
154
+ if (!this.ctx.settings.writable) throw new Error('settings provider is read-only')
155
+ await this.ctx.settings.replace(COMPUTER_USE_SETTINGS_NAMESPACE, parsed.value as object, parsed.expectedRevision)
156
+ } else if (parsed.action === 'health') {
157
+ await this.ctx.computerUse.health(AbortSignal.timeout(30000))
158
+ } else {
159
+ await this.ctx.computerUse.openPermissionSettings(parsed.kind, AbortSignal.timeout(10000))
160
+ }
161
+ responseJson(res, 200, { ok: true, value: this.snapshot() })
162
+ } catch (error) {
163
+ const conflict = error instanceof SettingsConflictError
164
+ this.ctx.logger.warn('dsh-computer-use Web action=%s failed: %s', parsed.action, publicMessage(error))
165
+ requestError(res, conflict ? 409 : 400, conflict ? 'settings-conflict' : 'action-failed', publicMessage(error))
166
+ }
167
+ }
168
+ }
169
+
170
+ /** Attach the optional route when a Web host is present. */
171
+ export function installComputerUseWeb(ctx: Context): void {
172
+ const backend = new ComputerUseWebBackend(ctx)
173
+ ctx.inject(['webServer'], (webCtx) => {
174
+ webCtx.effect(() => webCtx.webServer.register({
175
+ kind: 'exact',
176
+ path: COMPUTER_USE_SETTINGS_ROUTE,
177
+ handler: (req, res) => backend.handle(req, res),
178
+ }), 'dsh-computer-use: Web Settings route')
179
+ })
180
+ }