@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.
- package/CHANGELOG.md +130 -0
- package/README.md +4 -1
- package/README.zh.md +4 -1
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/config.js +49 -20
- package/lib/config.js.map +1 -1
- package/lib/confirmations.js.map +1 -1
- package/lib/exposure.js +19 -6
- package/lib/exposure.js.map +1 -1
- package/lib/index.js +5 -1
- package/lib/index.js.map +1 -1
- package/lib/leases.js +4 -1
- package/lib/leases.js.map +1 -1
- package/lib/providers/macos.js +27 -11
- package/lib/providers/macos.js.map +1 -1
- package/lib/skill.js +13 -2
- package/lib/skill.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/config.d.ts +15 -2
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/confirmations.d.ts +1 -1
- package/lib/types/confirmations.d.ts.map +1 -1
- package/lib/types/exposure.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/leases.d.ts +2 -1
- package/lib/types/leases.d.ts.map +1 -1
- package/lib/types/providers/macos.d.ts +0 -1
- package/lib/types/providers/macos.d.ts.map +1 -1
- package/lib/types/skill.d.ts +7 -2
- package/lib/types/skill.d.ts.map +1 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/types.d.ts +3 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/web.d.ts +2 -1
- package/lib/types/web.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/lib/web.js +34 -9
- package/lib/web.js.map +1 -1
- package/native/macos/Sources/Helper/CursorOverlay.swift +3 -0
- package/native/macos/Sources/Helper/main.swift +35 -19
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +3 -3
- package/package.json +100 -41
- package/scripts/build-client.mjs +0 -0
- package/scripts/build-native.mjs +0 -0
- package/scripts/model-e2e.mjs +7 -3
- package/scripts/session-transcript.mjs +4 -3
- package/scripts/validate.mjs +0 -0
- package/src/client/index.tsx +4 -2
- package/src/config.ts +53 -21
- package/src/confirmations.ts +1 -1
- package/src/exposure.ts +20 -6
- package/src/index.ts +5 -1
- package/src/leases.ts +6 -2
- package/src/providers/macos.ts +31 -11
- package/src/skill.ts +14 -2
- package/src/tools.ts +3 -2
- package/src/types.ts +2 -1
- package/src/web.ts +37 -7
package/src/confirmations.ts
CHANGED
|
@@ -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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
}
|
package/src/providers/macos.ts
CHANGED
|
@@ -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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
/**
|
|
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
|
|
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: '
|
|
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 {
|
|
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)
|
|
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(
|
|
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
|
|
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
|
-
|
|
173
|
-
|
|
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,
|