From 2fa5cc080c6015006a0ff84e058319d8ae2583ed Mon Sep 17 00:00:00 2001 From: creatixchu Date: Fri, 25 Sep 2026 13:54:04 +0800 Subject: [PATCH] test(cua): reject calls from ended agents before helper acquisition --- packages/experimental/cua/src/index.ts | 11 +++++++---- .../cua/tests/provider-lifecycle.spec.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/experimental/cua/src/index.ts b/packages/experimental/cua/src/index.ts index 757c638f6a..4c2f863f43 100644 --- a/packages/experimental/cua/src/index.ts +++ b/packages/experimental/cua/src/index.ts @@ -92,6 +92,11 @@ Await operations sequentially. After input, read fresh state to verify the effec Window objects expose observation and pointer/AX actions; keyboard input remains app-scoped and follows current focus. Scroll deltas are integer lines from -10 to 10; positive values reveal content above or left. Paste inserts plain text using native Unicode input without changing the clipboard; rich-text formats are unavailable. Browser applications can be controlled with getApp like other native apps; browser tabs and app launching are unavailable. The execution host must install the object API and own variable persistence; this plugin does not preload a cua global. In persistent PTC, initialize once with var { connectCua } = await import("@deepseek-ai/dsh-experimental-cua/bridge"); var cua = connectCua(() => tools.cua, text => console.log(text)). Retain app objects between calls. End screenshot calls with void await app.getScreenshot() or void await app.getAXStateAndScreenshot() so image bytes are not serialized as the program completion value.` +function executionAgent(execution: Readonly): Agent { + if (!execution.agent) throw new Error('dsh-cua requires an agent-owned execution') + return execution.agent +} + /** * Mount logged native operations with a separate helper per agent. * @param ctx - Host context with computer-use, subprocess and tool services. @@ -148,8 +153,7 @@ export function apply(ctx: Context, config: Config): void { async call(args, execution) { const { method, params: parsedParams } = Request.parse(args) const params = z.record(z.string(), z.json()).parse(parsedParams) - const agent = execution.agent - if (!agent) throw new Error('dsh-cua requires an agent-owned execution') + const agent = executionAgent(execution) const signal = execution.signal signal.throwIfAborted() const previous = clients.get(agent) @@ -180,8 +184,7 @@ export function apply(ctx: Context, config: Config): void { yield ctx.tools.register({ ...definition, async execute(args, execution) { - const agent = execution.agent - if (!agent) throw new Error('dsh-cua requires an agent-owned execution') + const agent = executionAgent(execution) execution.signal.throwIfAborted() lifetime.signal.throwIfAborted() const owner = ownerFor(agent) diff --git a/packages/experimental/cua/tests/provider-lifecycle.spec.ts b/packages/experimental/cua/tests/provider-lifecycle.spec.ts index 349683d9f2..3aeee1b7c6 100644 --- a/packages/experimental/cua/tests/provider-lifecycle.spec.ts +++ b/packages/experimental/cua/tests/provider-lifecycle.spec.ts @@ -225,6 +225,18 @@ it('rejects malformed and unowned operations before acquiring a helper', async ( expect(native.request).not.toHaveBeenCalled() }) +it.each([false, true])('rejects an ended Agent with prior helper acquisition %s', async (acquired) => { + const f = await setup() + if (acquired) await f.execute() + await f.handle.dispose() + const requests = native.request.mock.calls.length + const result = await f.execute() + expect(result.isError).toBe(true) + expect(JSON.stringify(result.content)).toContain('agent has been disposed') + expect(native.instances).toHaveLength(acquired ? 1 : 0) + expect(native.request).toHaveBeenCalledTimes(requests) +}) + it('supplies configured resource bounds and clears registrations on unload', async () => { const f = await setup() await f.execute('getState', { appHandle: 'app-one' })