@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.6

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 (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +38 -2
  4. package/cordis.patch.yml +6 -0
  5. package/dist/client.js +6 -0
  6. package/package.json +83 -4
  7. package/personal-assistant.plugin.json +31 -0
  8. package/personal-assistant.plugin.schema.json +43 -0
  9. package/runtime/assistant-runtime.mjs +317 -0
  10. package/runtime/check-manifest.mjs +21 -0
  11. package/runtime/cloud-browser-cdp.mjs +313 -0
  12. package/runtime/cloud-browser.mjs +972 -0
  13. package/runtime/cloud-execution.mjs +320 -0
  14. package/runtime/cloud-session-channel.mjs +45 -0
  15. package/runtime/computer-agentd.mjs +488 -0
  16. package/runtime/desktop-clipboard-limit.mjs +2 -0
  17. package/runtime/desktop-launcher.mjs +106 -0
  18. package/runtime/desktop-permit.mjs +42 -0
  19. package/runtime/desktop-session.mjs +840 -0
  20. package/runtime/desktop-ws.mjs +371 -0
  21. package/runtime/prepare-dsh-profile.mjs +17 -0
  22. package/runtime/welcome-server.mjs +144 -0
  23. package/scripts/assistant-runtime-launcher.sh +3 -0
  24. package/scripts/build-artifacts.mjs +81 -0
  25. package/scripts/build-client.mjs +82 -0
  26. package/scripts/computer-agentd-launcher.sh +3 -0
  27. package/scripts/deterministic_tar.py +21 -0
  28. package/scripts/install-local.mjs +262 -0
  29. package/scripts/scan-secrets.mjs +41 -0
  30. package/scripts/sync-public.mjs +52 -0
  31. package/scripts/verification/gateway-desktop-client.mjs +142 -0
  32. package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
  33. package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
  34. package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
  35. package/scripts/verify-cloud-turn.mjs +18 -0
  36. package/scripts/verify-computer-broker.mjs +10 -0
  37. package/src/automation-tools.mjs +18 -0
  38. package/src/bot-private-tools.mjs +23 -0
  39. package/src/bot-registry.mjs +157 -0
  40. package/src/bot-tool-policy.mjs +48 -0
  41. package/src/bot-tool-scope.mjs +40 -0
  42. package/src/client/bot-directory.mjs +23 -0
  43. package/src/client/components/desktop-clipboard.mjs +194 -0
  44. package/src/client/conversation.mjs +558 -0
  45. package/src/client/desktop-control.mjs +70 -0
  46. package/src/client/desktop-view.mjs +68 -0
  47. package/src/client/entry.mjs +8 -0
  48. package/src/client/index.mjs +693 -0
  49. package/src/client/markdown.mjs +130 -0
  50. package/src/client/native-workbench.mjs +62 -0
  51. package/src/cloud-computer-tools.mjs +245 -0
  52. package/src/cloud-files.mjs +57 -0
  53. package/src/cloud-model.mjs +10 -0
  54. package/src/cloud-session-transport.mjs +76 -0
  55. package/src/computer-broker-protocol.mjs +116 -0
  56. package/src/computer-broker.mjs +220 -0
  57. package/src/desktop-tools.mjs +64 -0
  58. package/src/dsh-session-bridge.mjs +207 -0
  59. package/src/dsh-web-bridge.mjs +127 -0
  60. package/src/host-gateway.mjs +228 -0
  61. package/src/host-session-channel.mjs +410 -0
  62. package/src/implementation-capabilities.mjs +10 -0
  63. package/src/index.mjs +148 -0
  64. package/src/persistent-state.mjs +201 -0
  65. package/src/platform-manifest.mjs +24 -0
  66. package/src/plugin-manifest.mjs +82 -0
  67. package/src/schedule-manager.mjs +88 -0
  68. package/src/system-fetch.mjs +72 -0
  69. package/src/workspace-tools.mjs +22 -0
  70. package/src/workspace.mjs +22 -0
@@ -0,0 +1,488 @@
1
+ import { createServer } from 'node:http';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { join } from 'node:path';
6
+ import { verifyDesktopPermit } from './desktop-permit.mjs';
7
+ import { createCloudBrowser } from './cloud-browser.mjs';
8
+ import { createCloudExecutor } from './cloud-execution.mjs';
9
+ import { createDesktopController, DESKTOP_CLIPBOARD_MAX_BYTES } from './desktop-session.mjs';
10
+
11
+ export function createComputerAgentServer({ env = process.env, desktopFactory = createDesktopController, browserFactory = createCloudBrowser, executorFactory = createCloudExecutor, logger = console } = {}) {
12
+ const host = env.COMPUTER_HOST || '127.0.0.1';
13
+ const port = Number(env.COMPUTER_PORT || 8787);
14
+ const workspace = env.AGENTKIT_WORKSPACE_DIR || '/workspace';
15
+ const identity = Object.freeze({ userId: env.USER_ID || '', assistantId: env.ASSISTANT_ID || '', computerId: env.COMPUTER_ID || '' });
16
+ const leases = new Map();
17
+ const agentDesktopSessions = new Map();
18
+ const agentExecutions = new Map();
19
+ const controlGrants = new Map(), usedControlPermits = new Map();
20
+ const actionContext = new AsyncLocalStorage();
21
+ let starting = env.AGENTKIT_DESKTOP_MODE === '1', startupFailure, warmup, warmupMs = 0, closing = false;
22
+ let resolveReady, rejectReady, closePromise;
23
+ const whenReady = new Promise((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
24
+ // 库调用方可以只查询 health;主入口仍消费拒绝并明确退出,避免未处理拒绝。
25
+ void whenReady.catch(() => {});
26
+ let browser;
27
+ let desktopBrowserOpening;
28
+ let desktopBrowserClosing;
29
+ let executor;
30
+ let desktop;
31
+ // 桌面首屏由 desktop-session 的本地桥提供;浏览器创建时把地址透传过去。
32
+ const browserEnv = () => (env.AGENTKIT_DESKTOP_MODE === '1' && typeof desktop?.welcomeUrl === 'string' && desktop.welcomeUrl
33
+ ? { ...env, AGENTKIT_WELCOME_URL: desktop.welcomeUrl } : env);
34
+ const browserOptions = () => ({ workspace, env: browserEnv(), beforeCommand: () => { const owner = actionContext.getStore(); if (owner) assertAgentLease(owner); },
35
+ ...(env.AGENTKIT_DESKTOP_MODE === '1' ? { userDataDir: join(env.AGENTKIT_COMPUTER_STATE_DIR || '/var/lib/agentkit/computer', 'chromium-profile'), preserveUserDataDir: true } : {}),
36
+ });
37
+ const closeDesktopBrowser = () => {
38
+ if (desktopBrowserClosing) return desktopBrowserClosing;
39
+ if (!browser) return Promise.resolve();
40
+ const current = browser;
41
+ desktopBrowserOpening = undefined;
42
+ desktopBrowserClosing = (async () => {
43
+ await current.close?.().catch?.(() => {});
44
+ await current.dispose?.();
45
+ if (browser === current) browser = undefined;
46
+ })();
47
+ // 停止失败保留拒绝状态,不能随后创建第二个进程抢同一持久 profile。
48
+ void desktopBrowserClosing.then(() => { desktopBrowserClosing = undefined; }, () => {});
49
+ return desktopBrowserClosing;
50
+ };
51
+ // 云端 DSH 网页地址:runtime 侧把 127.0.0.1:8081 的带 token 地址推过来,只存内存、不落盘。
52
+ let dshWebUrl = null;
53
+ const acceptsDshWebUrl = value => {
54
+ if (typeof value !== 'string' || !value || Buffer.byteLength(value) > 512) return false;
55
+ try {
56
+ const parsed = new URL(value);
57
+ if (!['http:', 'https:'].includes(parsed.protocol)) return false;
58
+ return ['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname);
59
+ } catch { return false; }
60
+ };
61
+ const ensureDesktopBrowser = async (options = {}) => {
62
+ if (closing) throw Object.assign(new Error('云电脑正在关闭'), { code: 'DESKTOP_LAUNCH_CLOSED' });
63
+ if (desktopBrowserClosing) await desktopBrowserClosing;
64
+ if (closing) throw Object.assign(new Error('云电脑正在关闭'), { code: 'DESKTOP_LAUNCH_CLOSED' });
65
+ if (desktopBrowserOpening) return desktopBrowserOpening;
66
+ browser ||= browserFactory(browserOptions());
67
+ if (!browser.available) throw Object.assign(new Error(browser.resolution?.reason || 'desktop browser unavailable'), { code: 'ASSISTANT_BROWSER_UNAVAILABLE', status: 503 });
68
+ // 桌面窗口和助理工具始终复用同一个可见 Chromium 实例。
69
+ // 首帧只等待本地窗口,外网页面加载由明确的浏览器操作触发。
70
+ desktopBrowserOpening = browser.openDesktopWindow(options);
71
+ try {
72
+ const result = await desktopBrowserOpening;
73
+ if (closing) throw Object.assign(new Error('云电脑正在关闭'), { code: 'DESKTOP_LAUNCH_CLOSED' });
74
+ return result;
75
+ } finally { desktopBrowserOpening = undefined; }
76
+ };
77
+ const openDshWeb = async ({ signal } = {}) => {
78
+ if (closing || signal?.aborted) throw Object.assign(new Error('桌面入口已停止'), { code: 'DESKTOP_LAUNCH_CLOSED' });
79
+ if (!dshWebUrl) throw Object.assign(new Error('云端助理网页还没准备好'), { code: 'DSH_WEB_UNAVAILABLE' });
80
+ const target = dshWebUrl;
81
+ const view = await ensureDesktopBrowser({ browsing: true });
82
+ if (closing || signal?.aborted) throw Object.assign(new Error('桌面入口已停止'), { code: 'DESKTOP_LAUNCH_CLOSED' });
83
+ browser ||= browserFactory(browserOptions());
84
+ const page = await browser.navigate(target);
85
+ // 普通浏览器窗口第一次拉起时 Chromium 可能自带一个空白“新标签页”,导航完再清一次。
86
+ await browser.closeBlankTabs?.();
87
+ return { ...view, ...page };
88
+ };
89
+ desktop = desktopFactory({ applications: env.AGENTKIT_DESKTOP_MODE === '1', workspace,
90
+ dshWebUrl: () => dshWebUrl, onLaunchDshWeb: openDshWeb,
91
+ onLaunchBrowser: async ({ signal } = {}) => {
92
+ if (closing || signal?.aborted) throw Object.assign(new Error('桌面入口已停止'), { code: 'DESKTOP_LAUNCH_CLOSED' });
93
+ // 用户主动点“浏览器”:欢迎页应用窗口切换成带地址栏的普通浏览器窗口。
94
+ const result = await ensureDesktopBrowser({ browsing: true });
95
+ if (closing || signal?.aborted) throw Object.assign(new Error('桌面入口已停止'), { code: 'DESKTOP_LAUNCH_CLOSED' });
96
+ return result;
97
+ },
98
+ onLaunchError: event => logger.error(JSON.stringify({ event: 'desktop-app-launch-failed', ...event })),
99
+ onStop: closeDesktopBrowser, onIdle: async owner => {
100
+ if (!owner.releaseLeaseOnIdle || !owner.leaseId) return;
101
+ const lease = leases.get(identity.computerId);
102
+ if (leaseIdentityMatches(lease, { ...identity, botId: owner.botId, taskId: owner.taskId, leaseId: owner.leaseId })) leases.delete(identity.computerId);
103
+ } });
104
+ const send = (response, status, body) => {
105
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
106
+ response.end(JSON.stringify(body));
107
+ };
108
+ const sendFrame = (response, status, frame) => {
109
+ response.writeHead(status, { 'content-type': 'image/jpeg', 'cache-control': 'no-store', 'x-desktop-seq': String(frame.seq || 0), 'x-desktop-width': String(frame.width || 1280), 'x-desktop-height': String(frame.height || 800), 'x-desktop-bytes': String(frame.bytes || 0), 'x-desktop-next-frame-delay-ms': String(frame.nextFrameDelayMs || 80), ...(frame.unchanged ? { 'x-desktop-unchanged': '1' } : {}) });
110
+ if (frame.data && !frame.unchanged) response.end(frame.data); else response.end();
111
+ };
112
+ const readBody = request => new Promise((resolve, reject) => {
113
+ const chunks = [];
114
+ request.on('data', chunk => chunks.push(chunk));
115
+ request.on('end', () => {
116
+ try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')); } catch (error) { reject(error); }
117
+ });
118
+ request.on('error', reject);
119
+ });
120
+ function identityMatches(body) {
121
+ return body.userId === identity.userId && body.assistantId === identity.assistantId && body.computerId === identity.computerId;
122
+ }
123
+ function closeAgentSessions(leaseId) {
124
+ for (const controller of agentExecutions.get(leaseId) || []) controller.abort(Object.assign(new Error('电脑租约已撤销;命令可能已部分执行,请核对结果,不要重放'), { code: 'COMPUTER_LEASE_REVOKED', status: 409 }));
125
+ agentExecutions.delete(leaseId);
126
+ agentDesktopSessions.delete(leaseId); desktop.closeSessionsForLease(leaseId);
127
+ }
128
+ function expireLeases() {
129
+ const now = Date.now();
130
+ for (const [computerId, lease] of leases) if (!Number.isFinite(lease.expiresAt) || lease.expiresAt <= now) {
131
+ if (lease.kind === 'human') {
132
+ if (!lease.expiring) {
133
+ lease.expiring = true;
134
+ void desktop.setSessionControl({ sessionId: lease.sessionId, controllable: false }).finally(() => {
135
+ if (leases.get(computerId) === lease) leases.delete(computerId);
136
+ }).catch(() => {});
137
+ }
138
+ continue;
139
+ }
140
+ leases.delete(computerId);
141
+ closeAgentSessions(lease.leaseId);
142
+ }
143
+ }
144
+ function leaseIdentityMatches(lease, body) {
145
+ return identityMatches(body) && lease && lease.botId === body.botId && lease.taskId === body.taskId && lease.leaseId === body.leaseId && lease.expiresAt > Date.now();
146
+ }
147
+ function assertAgentLease(body) {
148
+ actionContext.getStore()?.signal?.throwIfAborted();
149
+ const lease = leaseFor(body);
150
+ if (lease.kind === 'human') throw Object.assign(new Error('用户正在操作云电脑,请等待交还后继续'), { code: 'COMPUTER_USER_CONTROLLED', status: 409 });
151
+ return lease;
152
+ }
153
+ function leaseFor(body) {
154
+ expireLeases();
155
+ const lease = leases.get(body.computerId);
156
+ if (!leaseIdentityMatches(lease, body)) throw Object.assign(new Error(lease?.kind === 'human' ? '用户正在操作云电脑,请等待交还后继续' : 'COMPUTER_LEASE_REQUIRED'), { code: lease?.kind === 'human' ? 'COMPUTER_USER_CONTROLLED' : 'COMPUTER_LEASE_REQUIRED', status: 409 });
157
+ return lease;
158
+ }
159
+ async function action(body, { signal } = {}) {
160
+ signal?.throwIfAborted();
161
+ const lease = assertAgentLease(body);
162
+ return actionContext.run({ ...body, leaseId: lease.leaseId, signal }, async () => {
163
+ const args = body.args || {};
164
+ if (body.action.startsWith('desktop.')) {
165
+ let session = desktop.getSession(agentDesktopSessions.get(lease.leaseId));
166
+ if (!session) {
167
+ const opened = await desktop.openSession({ identity, botId: body.botId, taskId: body.taskId, leaseId: lease.leaseId, controllable: true });
168
+ // 预热/打开期间可能已被人接管,旧授权不得跨过异步边界。
169
+ try { assertAgentLease(body); } catch (error) { desktop.closeSession(opened.sessionId); throw error; }
170
+ agentDesktopSessions.set(lease.leaseId, opened.sessionId);
171
+ session = desktop.getSession(opened.sessionId);
172
+ }
173
+ assertAgentLease(body);
174
+ if (body.action === 'desktop.capture') {
175
+ const frame = await desktop.frame({ sessionId: session.sessionId });
176
+ assertAgentLease(body);
177
+ return { image: { data: frame.data, mediaType: 'image/jpeg', width: frame.width, height: frame.height, bytes: frame.bytes } };
178
+ }
179
+ if (body.action === 'desktop.input') {
180
+ const events = args.events ?? [args.event];
181
+ if (!Array.isArray(events) || events.length < 1 || events.length > 32) throw Object.assign(new Error('invalid desktop input batch'), { status: 400 });
182
+ try {
183
+ for (const event of events) {
184
+ assertAgentLease(body);
185
+ await desktop.input({ sessionId: session.sessionId, controlEpoch: session.controlEpoch, event });
186
+ }
187
+ return { accepted: true, events: events.length };
188
+ } catch (error) {
189
+ // 同一手势始终绑定最初租约。接管/错误时关闭旧会话,释放按键,不跨新租约续发剩余事件。
190
+ desktop.closeSession(session.sessionId); agentDesktopSessions.delete(lease.leaseId);
191
+ throw error;
192
+ }
193
+ }
194
+ if (body.action === 'desktop.windows') return { windows: await desktop.windows() };
195
+ if (body.action === 'desktop.focus') return desktop.focusWindow(args.windowId, () => assertAgentLease(body));
196
+ if (body.action === 'desktop.launch') {
197
+ if (!['browser', 'terminal', 'files'].includes(args.application)) throw Object.assign(new Error('unsupported desktop application'), { status: 400 });
198
+ await desktop.launchApplication(args.application, { beforeCommand: () => assertAgentLease(body) });
199
+ assertAgentLease(body);
200
+ return { application: args.application, opened: true };
201
+ }
202
+ }
203
+ if (body.action === 'clipboard.read' || body.action === 'clipboard.write') {
204
+ // 剪贴板是 X11 桌面资源;未启用桌面模式时不能假装同步成功。
205
+ if (env.AGENTKIT_DESKTOP_MODE !== '1') throw Object.assign(new Error('云电脑桌面未启用'), { code: 'DESKTOP_UNAVAILABLE', status: 503 });
206
+ if (body.action === 'clipboard.read') return { text: await desktop.readClipboard() };
207
+ if (typeof args.text !== 'string') throw Object.assign(new Error('clipboard text must be a string'), { code: 'CLIPBOARD_TEXT_INVALID', status: 400 });
208
+ if (Buffer.byteLength(args.text, 'utf8') > DESKTOP_CLIPBOARD_MAX_BYTES) throw Object.assign(new Error(`clipboard text must not exceed ${DESKTOP_CLIPBOARD_MAX_BYTES} bytes`), { code: 'CLIPBOARD_TEXT_TOO_LARGE', status: 413 });
209
+ const written = await desktop.writeClipboard(args.text);
210
+ return { written: written?.written === true };
211
+ }
212
+ if (body.action === 'open') {
213
+ if (env.AGENTKIT_DESKTOP_MODE === '1') {
214
+ await desktop.ensureDisplay();
215
+ assertAgentLease(body);
216
+ await ensureDesktopBrowser();
217
+ }
218
+ assertAgentLease(body);
219
+ return (browser ||= browserFactory(browserOptions())).open(args.url);
220
+ }
221
+ if (body.action === 'snapshot') return browser?.snapshot();
222
+ if (body.action === 'click') return browser?.click(args);
223
+ if (body.action === 'type') return browser?.type(args);
224
+ if (body.action === 'press') return browser?.press(args);
225
+ if (body.action === 'screenshot') return browser?.screenshot();
226
+ if (body.action === 'download') return browser?.download(args);
227
+ if (body.action === 'stop') { desktopBrowserOpening = undefined; return browser?.close(); }
228
+ if (body.action === 'terminal.exec') {
229
+ executor ||= executorFactory({ workspaceRoot: workspace });
230
+ const controller = new AbortController();
231
+ const abort = () => controller.abort(signal.reason);
232
+ if (signal?.aborted) abort(); else signal?.addEventListener('abort', abort, { once: true });
233
+ const active = agentExecutions.get(lease.leaseId) || new Set();
234
+ agentExecutions.set(lease.leaseId, active); active.add(controller);
235
+ try {
236
+ assertAgentLease(body);
237
+ return await (args.language === 'shell' ? executor.runBash(args, { signal: controller.signal }) : executor.runCode(args, { signal: controller.signal }));
238
+ } catch (error) {
239
+ const reason = controller.signal.aborted ? controller.signal.reason || error : error;
240
+ const failure = reason instanceof Error ? reason : new Error('云端命令已中断');
241
+ if (failure.code !== 'ASSISTANT_CLOUD_EXEC_REFUSED') failure.outcomeUnknown = true;
242
+ throw failure;
243
+ } finally {
244
+ signal?.removeEventListener('abort', abort); active.delete(controller);
245
+ if (!active.size && agentExecutions.get(lease.leaseId) === active) agentExecutions.delete(lease.leaseId);
246
+ }
247
+ }
248
+ throw new Error(`unsupported computer action: ${body.action}`);
249
+ });
250
+ }
251
+ function desktopIdentity(body) {
252
+ if (!identityMatches(body)) throw Object.assign(new Error('COMPUTER_IDENTITY_MISMATCH'), { code: 'COMPUTER_IDENTITY_MISMATCH', status: 403 });
253
+ return { userId: identity.userId, assistantId: identity.assistantId, computerId: identity.computerId };
254
+ }
255
+ function desktopLease(body) {
256
+ expireLeases();
257
+ const lease = leases.get(body.computerId);
258
+ if (!leaseIdentityMatches(lease, body)) {
259
+ throw Object.assign(new Error('COMPUTER_LEASE_REQUIRED'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
260
+ }
261
+ return lease;
262
+ }
263
+ const viewControl = session => ({ sessionId: session.sessionId, readOnly: !session.controllable, controlAvailable: controlGrants.has(session.sessionId), controlEpoch: session.controlEpoch, handoffProtocol: 'desktop-handoff/v1' });
264
+ function controlSession(body, checkEpoch = true) {
265
+ desktopIdentity(body);
266
+ const session = desktop.getSession(body.sessionId);
267
+ if (!session) throw Object.assign(new Error('DESKTOP_SESSION_NOT_FOUND'), { code: 'DESKTOP_SESSION_NOT_FOUND', status: 404 });
268
+ const grant = controlGrants.get(session.sessionId);
269
+ if (!grant || grant.botId !== body.botId || grant.taskId !== body.taskId || session.botId !== body.botId || session.taskId !== body.taskId) throw Object.assign(new Error('桌面只读授权不能取得控制权'), { code: 'DESKTOP_CONTROL_GRANT_REQUIRED', status: 403 });
270
+ if (checkEpoch && (!Number.isSafeInteger(body.controlEpoch) || body.controlEpoch !== session.controlEpoch)) throw Object.assign(new Error('桌面控制代次已改变'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
271
+ return session;
272
+ }
273
+ async function releaseControl(body) {
274
+ const session = controlSession(body), lease = leases.get(identity.computerId);
275
+ if (!session.controllable) return viewControl(session);
276
+ if (!leaseIdentityMatches(lease, body) || lease.kind !== 'human' || lease.sessionId !== session.sessionId) throw Object.assign(new Error('COMPUTER_LEASE_REQUIRED'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
277
+ // 清掉鼠标/修饰键后才开放租约,避免助理接到卡住的按键状态。
278
+ const changed = await desktop.setSessionControl({ sessionId: session.sessionId, controllable: false, controlEpoch: body.controlEpoch });
279
+ if (leases.get(identity.computerId) === lease) leases.delete(identity.computerId);
280
+ return { ...changed, controlAvailable: true, handoffProtocol: 'desktop-handoff/v1' };
281
+ }
282
+ const server = createServer(async (request, response) => {
283
+ if (closing) return send(response, 503, { error: 'COMPUTER_CLOSING' });
284
+ if (request.method === 'GET' && ['/health', '/v1/status'].includes(request.url) && (starting || startupFailure)) return send(response, 503, { status: starting ? 'starting' : 'error', error: startupFailure?.code || (startupFailure ? 'DESKTOP_STARTUP_FAILED' : undefined), desktopReady: false, ...identity });
285
+ if (request.method === 'GET' && request.url === '/health') return send(response, 200, { status: 'ok', desktopReady: !starting, warmupMs, ...identity });
286
+ if (request.method === 'GET' && request.url === '/v1/status') return send(response, 200, { status: 'ready', desktopReady: !starting, warmupMs, ...identity, capabilities: ['computer-broker/v1'] });
287
+ if (request.method !== 'POST' || !request.url?.startsWith('/broker/')) return send(response, 404, { error: 'not_found' });
288
+ try {
289
+ const body = await readBody(request);
290
+ if (request.url === '/broker/status') {
291
+ if (!identityMatches(body)) return send(response, 403, { error: 'COMPUTER_IDENTITY_MISMATCH' });
292
+ return send(response, 200, { protocol: body.protocol, computerId: identity.computerId, connected: true });
293
+ }
294
+ if (request.url === '/broker/lease/acquire') {
295
+ expireLeases();
296
+ if (!identityMatches(body)) return send(response, 403, { error: 'COMPUTER_IDENTITY_MISMATCH' });
297
+ if (typeof body.botId !== 'string' || !body.botId.trim() || body.botId.length > 128 || typeof body.taskId !== 'string' || !body.taskId.trim() || body.taskId.length > 128) return send(response, 400, { error: 'COMPUTER_LEASE_IDENTITY_INVALID' });
298
+ const existing = leases.get(body.computerId);
299
+ if (existing && existing.botId === body.botId && existing.taskId === body.taskId && body.idempotencyKey && existing.idempotencyKey === body.idempotencyKey) return send(response, 200, { protocol: body.protocol, lease: existing });
300
+ if (existing) return send(response, 409, { error: existing.kind === 'human' ? 'COMPUTER_USER_CONTROLLED' : 'COMPUTER_LEASE_CONFLICT' });
301
+ const ttlMs = Number(body.ttlMs || 60000);
302
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 300000) return send(response, 400, { error: 'COMPUTER_LEASE_TTL_INVALID' });
303
+ const lease = { ...identity, kind: 'agent', botId: body.botId, taskId: body.taskId, idempotencyKey: body.idempotencyKey, ttlMs, leaseId: randomUUID(), expiresAt: Date.now() + ttlMs };
304
+ leases.set(body.computerId, lease);
305
+ return send(response, 200, { protocol: body.protocol, lease });
306
+ }
307
+ if (request.url === '/broker/lease/renew') {
308
+ const lease = leaseFor(body);
309
+ const ttlMs = Number(body.ttlMs || lease.ttlMs);
310
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1000 || ttlMs > 300000) return send(response, 400, { error: 'COMPUTER_LEASE_TTL_INVALID' });
311
+ lease.expiresAt = Date.now() + ttlMs;
312
+ return send(response, 200, { protocol: body.protocol, lease });
313
+ }
314
+ if (request.url === '/broker/lease/release') {
315
+ const lease = leases.get(body.computerId);
316
+ const released = Boolean(leaseIdentityMatches(lease, body));
317
+ if (released && lease.kind === 'human') await releaseControl({ ...body, sessionId: lease.sessionId, controlEpoch: desktop.getSession(lease.sessionId)?.controlEpoch });
318
+ else if (released) { leases.delete(body.computerId); closeAgentSessions(body.leaseId); }
319
+ return send(response, 200, { protocol: body.protocol, released });
320
+ }
321
+ if (request.url === '/broker/action') {
322
+ const controller = new AbortController();
323
+ const disconnected = () => { if (!response.writableEnded) controller.abort(Object.assign(new Error('请求连接已取消;动作可能已部分执行,请核对结果'), { code: 'COMPUTER_ACTION_ABORTED', status: 499 })); };
324
+ response.on('close', disconnected); if (response.destroyed) disconnected();
325
+ try { return send(response, 200, { protocol: body.protocol, ...(await action(body, { signal: controller.signal })) }); }
326
+ finally { response.off('close', disconnected); }
327
+ }
328
+ // 云端 DSH 网页地址:宿主就绪后由运行时写入。这里不涉及桌面控制权(不是桌面动作),
329
+ // 所以不要求 agent lease;只接受本机回环地址,且不落盘、不写日志。
330
+ if (request.url === '/broker/dsh-web') {
331
+ // 注意:请求体在上面已经读过一次,这里不能再 readBody(流已消费会一直挂住)。
332
+ if (!acceptsDshWebUrl(body?.url)) return send(response, 400, { error: 'DSH_WEB_URL_INVALID' });
333
+ dshWebUrl = body.url;
334
+ logger.info(JSON.stringify({ event: 'desktop.dsh-web.registered', host: new URL(body.url).hostname, port: new URL(body.url).port }));
335
+ return send(response, 200, { registered: true });
336
+ }
337
+ if (request.url === '/broker/desktop/session') {
338
+ const desktopIdentityValue = desktopIdentity(body);
339
+ expireLeases();
340
+ let controlGrant;
341
+ if (body.mode === 'observe') {
342
+ const claims = await verifyDesktopPermit(body.controlPermit, { assistantId: identity.assistantId, env });
343
+ if (claims.mode !== 'control' || claims.botId === '__directory__' || claims.botId !== body.botId || !body.taskId || body.leaseId) throw Object.assign(new Error('桌面接管需要具名助理控制授权'), { code: 'DESKTOP_CONTROL_GRANT_REQUIRED', status: 403 });
344
+ for (const [nonce, expiry] of usedControlPermits) if (expiry <= Date.now()) usedControlPermits.delete(nonce);
345
+ if (usedControlPermits.has(claims.jti)) throw Object.assign(new Error('desktop permit already used'), { code: 'DESKTOP_UNAUTHORIZED', status: 401 });
346
+ usedControlPermits.set(claims.jti, claims.exp * 1000);
347
+ controlGrant = { botId: body.botId, taskId: body.taskId };
348
+ }
349
+ const lease = body.leaseId ? desktopLease({ ...body, ...desktopIdentityValue }) : null;
350
+ const opened = await desktop.openSession({ identity: desktopIdentityValue, botId: body.botId || null, controllable: Boolean(lease), leaseId: lease?.leaseId || null, taskId: body.taskId || null, releaseLeaseOnIdle: body.releaseLeaseOnIdle === true });
351
+ try { await ensureDesktopBrowser(); }
352
+ catch (error) { desktop.closeSession(opened.sessionId); throw error; }
353
+ if (controlGrant) controlGrants.set(opened.sessionId, controlGrant);
354
+ return send(response, 200, { ...opened, controlAvailable: Boolean(controlGrant), handoffProtocol: 'desktop-handoff/v1' });
355
+ }
356
+ if (request.url === '/broker/desktop/control/acquire') {
357
+ const session = controlSession(body);
358
+ expireLeases();
359
+ const previous = leases.get(identity.computerId);
360
+ if (previous?.kind === 'human') {
361
+ if (previous.sessionId !== session.sessionId || previous.expiring) return send(response, 409, { error: 'COMPUTER_LEASE_CONFLICT' });
362
+ return send(response, 200, { ...viewControl(session), lease: previous });
363
+ }
364
+ const lease = { ...identity, kind: 'human', sessionId: session.sessionId, botId: body.botId, taskId: body.taskId, leaseId: randomUUID(), ttlMs: 120000, expiresAt: Date.now() + 120000 };
365
+ // 先封住旧助理租约;CDP 每次发命令前会再次检查,旧异步回调不能穿透。
366
+ leases.set(identity.computerId, lease);
367
+ if (previous) closeAgentSessions(previous.leaseId);
368
+ try {
369
+ await browser?.releaseInput?.();
370
+ await ensureDesktopBrowser();
371
+ const changed = await desktop.setSessionControl({ sessionId: session.sessionId, controllable: true, leaseId: lease.leaseId, taskId: body.taskId, controlEpoch: body.controlEpoch });
372
+ return send(response, 200, { ...changed, controlAvailable: true, handoffProtocol: 'desktop-handoff/v1', lease });
373
+ } catch (error) { if (leases.get(identity.computerId) === lease) leases.delete(identity.computerId); throw error; }
374
+ }
375
+ if (request.url === '/broker/desktop/control/state') {
376
+ const session = controlSession(body, false);
377
+ expireLeases();
378
+ const lease = leases.get(identity.computerId);
379
+ return send(response, 200, { ...viewControl(session), lease: lease?.kind === 'human' && lease.sessionId === session.sessionId && !lease.expiring ? lease : null });
380
+ }
381
+ if (request.url === '/broker/desktop/control/release') return send(response, 200, await releaseControl(body));
382
+ if (['/broker/desktop/idle', '/broker/desktop/resume'].includes(request.url)) {
383
+ desktopIdentity(body);
384
+ const session = desktop.getSession(body.sessionId);
385
+ if (!session) throw Object.assign(new Error('DESKTOP_SESSION_NOT_FOUND'), { code: 'DESKTOP_SESSION_NOT_FOUND', status: 404 });
386
+ if (!session.botId || !session.taskId || session.botId !== body.botId || session.taskId !== body.taskId) throw Object.assign(new Error('DESKTOP_SESSION_OWNER_MISMATCH'), { code: 'DESKTOP_SESSION_OWNER_MISMATCH', status: 403 });
387
+ if (request.url.endsWith('/idle')) {
388
+ if (!session.idle && session.leaseId && (body.leaseId !== session.leaseId || !leaseIdentityMatches(leases.get(identity.computerId), body))) throw Object.assign(new Error('COMPUTER_LEASE_REQUIRED'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
389
+ return send(response, 200, await desktop.suspendSession(body.sessionId));
390
+ }
391
+ const lease = body.leaseId ? desktopLease(body) : null;
392
+ return send(response, 200, await desktop.resumeSession({ sessionId: body.sessionId, controllable: Boolean(lease), leaseId: lease?.leaseId || null, releaseLeaseOnIdle: body.releaseLeaseOnIdle === true }));
393
+ }
394
+ if (request.url === '/broker/desktop/frame') {
395
+ desktopIdentity(body);
396
+ return send(response, 200, await desktop.frame({ sessionId: body.sessionId, afterSeq: body.afterSeq }));
397
+ }
398
+ if (request.url === '/broker/desktop/frame-binary') {
399
+ desktopIdentity(body);
400
+ return sendFrame(response, 200, await desktop.frameBinary({ sessionId: body.sessionId, afterSeq: body.afterSeq }));
401
+ }
402
+ if (request.url === '/broker/desktop/input') {
403
+ desktopIdentity(body);
404
+ const session = desktop.getSession(body.sessionId);
405
+ if (!session || session.identity.computerId !== identity.computerId) throw Object.assign(new Error('DESKTOP_SESSION_NOT_FOUND'), { code: 'DESKTOP_SESSION_NOT_FOUND', status: 404 });
406
+ if (session.idle) throw Object.assign(new Error('DESKTOP_SESSION_IDLE'), { code: 'DESKTOP_SESSION_IDLE', status: 409 });
407
+ if (controlGrants.has(session.sessionId) && body.controlEpoch !== session.controlEpoch) throw Object.assign(new Error('桌面控制代次已改变'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
408
+ if (session.leaseId !== body.leaseId || session.taskId !== body.taskId) throw Object.assign(new Error('COMPUTER_LEASE_REQUIRED'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
409
+ desktopLease({ ...body, computerId: identity.computerId });
410
+ return send(response, 202, await desktop.input({ sessionId: body.sessionId, event: body.event, controlEpoch: body.controlEpoch }));
411
+ }
412
+ if (request.url === '/broker/desktop/close') {
413
+ desktopIdentity(body);
414
+ const session = desktop.getSession(body.sessionId), lease = leases.get(identity.computerId);
415
+ if (controlGrants.has(body.sessionId) && lease?.kind === 'human' && lease.sessionId === body.sessionId) {
416
+ await releaseControl({ ...body, botId: session.botId, taskId: session.taskId, leaseId: lease.leaseId, controlEpoch: session.controlEpoch });
417
+ }
418
+ const closed = desktop.closeSession(body.sessionId);
419
+ controlGrants.delete(body.sessionId);
420
+ // 关闭观察面板只停止采集,不关闭桌面窗口。
421
+ return send(response, 200, { closed });
422
+ }
423
+ return send(response, 404, { error: 'not_found' });
424
+ } catch (error) {
425
+ if (response.destroyed || response.writableEnded) return;
426
+ return send(response, Number(error?.status) || 400, { error: error?.code || error?.message || 'COMPUTER_ACTION_FAILED', ...(error?.outcomeUnknown ? { outcomeUnknown: true, message: '动作已中断,可能已部分执行;请先核对结果,不要盲目重放' } : {}) });
427
+ }
428
+ });
429
+ const leaseReaper = setInterval(expireLeases, 1000);
430
+ leaseReaper.unref();
431
+ const close = () => {
432
+ if (closePromise) return closePromise;
433
+ closing = true; clearInterval(leaseReaper);
434
+ for (const leaseId of agentExecutions.keys()) closeAgentSessions(leaseId);
435
+ closePromise = (async () => {
436
+ const failures = [];
437
+ try { await desktop.stopLauncher?.(); } catch (error) { failures.push(error); }
438
+ await warmup;
439
+ try { await desktop.stopLauncher?.(); } catch (error) { failures.push(error); }
440
+ for (const cleanup of [() => browser?.dispose?.(), () => executor?.dispose?.(), () => desktop.dispose()]) {
441
+ try { await cleanup(); } catch (error) { failures.push(error); }
442
+ }
443
+ await new Promise(resolve => server.close(resolve));
444
+ if (failures.length) throw new AggregateError(failures, 'computer shutdown cleanup failed');
445
+ })();
446
+ return closePromise;
447
+ };
448
+ return { server, whenReady, listen: () => server.listen(port, host, () => {
449
+ if (!starting) { resolveReady({ desktopReady: true, warmupMs: 0 }); return; }
450
+ const began = Date.now();
451
+ warmup = (async () => {
452
+ try {
453
+ await desktop.ensureDisplay();
454
+ if (!closing) await ensureDesktopBrowser();
455
+ warmupMs = Date.now() - began;
456
+ if (!closing) logger.info(JSON.stringify({ event: 'desktop-ready', warmupMs, computerId: identity.computerId }));
457
+ resolveReady({ desktopReady: !closing, warmupMs });
458
+ } catch (error) {
459
+ startupFailure = error;
460
+ logger.error(JSON.stringify({ event: 'desktop-startup-failed', code: error.code || 'DESKTOP_STARTUP_FAILED', message: String(error.message).slice(0, 500), diagnostics: error.desktopDiagnostics }));
461
+ rejectReady(error);
462
+ } finally { starting = false; }
463
+ })();
464
+ }), close };
465
+ }
466
+
467
+ // 生产入口在一次预热失败后清理并非零退出;恢复由 Kubernetes 显式重启承担。
468
+ // exit/onSignal 可注入,单测无需退出测试进程,也不在库内悄悄重试。
469
+ export function runComputerAgentDaemon({ service = createComputerAgentServer(), exit = code => process.exit(code), onSignal = handler => { process.once('SIGTERM', handler); process.once('SIGINT', handler); } } = {}) {
470
+ let shutdown, resolveStopped;
471
+ const stopped = new Promise(resolve => { resolveStopped = resolve; });
472
+ const stop = (code = 0) => {
473
+ if (shutdown) return shutdown;
474
+ shutdown = (async () => {
475
+ try { await service.close(); }
476
+ catch (error) { code = 1; console.error(JSON.stringify({ event: 'computer-shutdown-failed', message: String(error.message).slice(0, 500) })); }
477
+ try { exit(code); } finally { resolveStopped(); }
478
+ })();
479
+ return shutdown;
480
+ };
481
+ void service.whenReady.catch(() => stop(1));
482
+ onSignal(() => { void stop(0); });
483
+ service.listen();
484
+ return { service, stop, stopped };
485
+ }
486
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
487
+ runComputerAgentDaemon();
488
+ }
@@ -0,0 +1,2 @@
1
+ /** 与 computer 容器的 desktop-session.mjs 保持同一剪贴板上限,供 runtime 侧的桌面 WebSocket 复用。 */
2
+ export const DESKTOP_CLIPBOARD_MAX_BYTES = 64 * 1024;
@@ -0,0 +1,106 @@
1
+ import { createServer, createConnection } from 'node:net';
2
+ import { chmod, mkdtemp, rm } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ const actions = new Set(['browser', 'terminal', 'files', 'dsh']);
7
+ const MAX_REQUEST_BYTES = 256;
8
+ const failure = (code, message) => Object.assign(new Error(message), { code });
9
+ const assertRequest = value => {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value)
11
+ || Object.keys(value).sort().join(',') !== 'action,v' || value.v !== 1 || !actions.has(value.action)) {
12
+ throw failure('DESKTOP_LAUNCH_INVALID', '桌面入口仅接受浏览器、终端或文件管理器');
13
+ }
14
+ return value.action;
15
+ };
16
+
17
+ // 此 socket 只承接本机桌面的固定应用入口,不提供 URL、路径、命令或远程输入接口。
18
+ export async function createDesktopLauncherServer({ launch, timeoutMs = 8000, onError = () => {} } = {}) {
19
+ const directory = await mkdtemp('/tmp/agentkit-desktop-launcher-');
20
+ await chmod(directory, 0o700);
21
+ const socketPath = join(directory, 'launcher.sock');
22
+ const clients = new Map();
23
+ let closing = false, closePromise;
24
+ const server = createServer(socket => {
25
+ if (closing) { socket.destroy(); return; }
26
+ const controller = new AbortController();
27
+ clients.set(socket, controller);
28
+ let body = Buffer.alloc(0), accepted = false, finished = false, action;
29
+ const finish = value => {
30
+ if (finished) return;
31
+ finished = true; clearTimeout(timer);
32
+ if (!socket.destroyed) socket.end(JSON.stringify(value) + '\n');
33
+ };
34
+ const timer = setTimeout(() => {
35
+ controller.abort();
36
+ if (action) onError({ action, code: 'DESKTOP_LAUNCH_TIMEOUT' });
37
+ finish({ ok: false, code: 'DESKTOP_LAUNCH_TIMEOUT' });
38
+ }, timeoutMs);
39
+ socket.on('error', () => {});
40
+ socket.once('close', () => { clearTimeout(timer); controller.abort(); clients.delete(socket); });
41
+ socket.on('data', chunk => {
42
+ if (accepted || finished) return;
43
+ body = Buffer.concat([body, chunk]);
44
+ if (body.length > MAX_REQUEST_BYTES) { controller.abort(); finish({ ok: false, code: 'DESKTOP_LAUNCH_INVALID' }); return; }
45
+ if (!body.includes(10)) return;
46
+ accepted = true;
47
+ try { action = assertRequest(JSON.parse(body.toString('utf8'))); }
48
+ catch { finish({ ok: false, code: 'DESKTOP_LAUNCH_INVALID' }); return; }
49
+ void (async () => {
50
+ try {
51
+ if (closing || controller.signal.aborted) throw failure('DESKTOP_LAUNCH_CLOSED', '桌面入口已停止');
52
+ await launch(action, { signal: controller.signal });
53
+ if (closing || controller.signal.aborted) throw failure('DESKTOP_LAUNCH_CLOSED', '桌面入口已停止');
54
+ finish({ ok: true });
55
+ } catch (error) {
56
+ if (!closing && !controller.signal.aborted) onError({ action, code: String(error.code || 'DESKTOP_LAUNCH_FAILED').slice(0, 64) });
57
+ finish({ ok: false, code: String(error.code || 'DESKTOP_LAUNCH_FAILED').slice(0, 64) });
58
+ }
59
+ })();
60
+ });
61
+ });
62
+ server.maxConnections = 8;
63
+ try {
64
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(socketPath, resolve); });
65
+ await chmod(socketPath, 0o600);
66
+ } catch (error) { server.close(); await rm(directory, { recursive: true, force: true }); throw error; }
67
+ const close = () => {
68
+ if (closePromise) return closePromise;
69
+ closing = true;
70
+ for (const [socket, controller] of clients) { controller.abort(); socket.destroy(); }
71
+ closePromise = new Promise(resolve => server.close(resolve)).then(() => rm(directory, { recursive: true, force: true }));
72
+ return closePromise;
73
+ };
74
+ return Object.freeze({ socketPath, close });
75
+ }
76
+
77
+ export function requestDesktopLaunch(socketPath, action, { timeoutMs = 9000 } = {}) {
78
+ assertRequest({ v: 1, action });
79
+ return new Promise((resolve, reject) => {
80
+ const socket = createConnection(socketPath);
81
+ let body = '', settled = false;
82
+ const finish = (error, value) => { if (settled) return; settled = true; clearTimeout(timer); socket.destroy(); error ? reject(error) : resolve(value); };
83
+ const timer = setTimeout(() => finish(failure('DESKTOP_LAUNCH_TIMEOUT', '桌面入口响应超时')), timeoutMs);
84
+ socket.once('connect', () => socket.write(JSON.stringify({ v: 1, action }) + '\n'));
85
+ socket.on('error', () => finish(failure('DESKTOP_LAUNCH_UNAVAILABLE', '桌面入口暂时不可用')));
86
+ socket.on('data', chunk => {
87
+ body += chunk.toString('utf8');
88
+ if (Buffer.byteLength(body) > 1024) { finish(failure('DESKTOP_LAUNCH_INVALID', '桌面入口响应无效')); return; }
89
+ if (!body.includes('\n')) return;
90
+ try {
91
+ const value = JSON.parse(body);
92
+ if (value.ok !== true) throw failure(value.code || 'DESKTOP_LAUNCH_FAILED', '无法打开桌面应用');
93
+ finish(null, value);
94
+ } catch (error) { finish(error); }
95
+ });
96
+ socket.once('close', () => { if (!settled) finish(failure('DESKTOP_LAUNCH_CLOSED', '桌面入口已停止')); });
97
+ });
98
+ }
99
+
100
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
101
+ const args = process.argv.slice(2);
102
+ try {
103
+ if (args.length !== 4 || args[0] !== '--socket' || args[2] !== '--action') throw failure('DESKTOP_LAUNCH_INVALID', '桌面入口参数无效');
104
+ await requestDesktopLaunch(args[1], args[3]);
105
+ } catch (error) { console.error(JSON.stringify({ event: 'desktop-launch-failed', code: error.code || 'DESKTOP_LAUNCH_FAILED' })); process.exitCode = 1; }
106
+ }
@@ -0,0 +1,42 @@
1
+ /** Runtime 与 Computer 共用的网关签名授权校验。只包含公钥,不持有签名私钥。 */
2
+ import { createPublicKey, verify } from 'node:crypto';
3
+ export const desktopUnauthorized = message => Object.assign(new Error(message), { status: 401, code: 'DESKTOP_UNAUTHORIZED' });
4
+ const unauthorized = desktopUnauthorized;
5
+ const permitKeys = new Map();
6
+
7
+ async function desktopJwks(env) {
8
+ if (env.AGENTKIT_DESKTOP_JWKS_JSON) return JSON.parse(env.AGENTKIT_DESKTOP_JWKS_JSON);
9
+ const source = String(env.AGENTKIT_DESKTOP_JWKS_URL || '');
10
+ if (!source) throw unauthorized('desktop verification keys are not configured');
11
+ const url = new URL(source);
12
+ if (url.username || url.password || url.hash || (url.protocol !== 'https:' && !(url.protocol === 'http:' && env.AGENTKIT_DESKTOP_ALLOW_HTTP_JWKS === '1'))) throw unauthorized('desktop JWKS URL is not trusted');
13
+ const cached = permitKeys.get(source);
14
+ if (cached && cached.expires > Date.now()) return cached.value;
15
+ const response = await fetch(url, { signal: AbortSignal.timeout(3000), redirect: 'error' });
16
+ if (!response.ok) throw unauthorized('desktop verification keys unavailable');
17
+ const raw = await response.text();
18
+ if (raw.length > 64 * 1024) throw unauthorized('desktop verification keys exceed limit');
19
+ const value = JSON.parse(raw);
20
+ permitKeys.set(source, { value, expires: Date.now() + 60_000 });
21
+ return value;
22
+ }
23
+
24
+ /** 验证既有 Server workload 密钥签发的 JWS;不信任 caller 自带身份头。 */
25
+ export async function verifyDesktopPermit(token, { assistantId, env = process.env, now = () => Date.now() } = {}) {
26
+ try {
27
+ if (typeof token !== 'string' || token.length > 16 * 1024) throw unauthorized('desktop permit is missing');
28
+ const segments = token.split('.');
29
+ if (segments.length !== 3 || segments.some(value => !/^[A-Za-z0-9_-]+$/u.test(value))) throw unauthorized('desktop permit is malformed');
30
+ const header = JSON.parse(Buffer.from(segments[0], 'base64url').toString('utf8'));
31
+ const claims = JSON.parse(Buffer.from(segments[1], 'base64url').toString('utf8'));
32
+ if (header.alg !== 'EdDSA' || header.typ !== 'agentkit-desktop+jwt' || !header.kid) throw unauthorized('desktop permit algorithm is invalid');
33
+ const jwks = await desktopJwks(env);
34
+ const key = jwks?.keys?.find(value => value.kid === header.kid && value.kty === 'OKP' && value.crv === 'Ed25519');
35
+ if (!key || !verify(null, Buffer.from(`${segments[0]}.${segments[1]}`, 'ascii'), createPublicKey({ key, format: 'jwk' }), Buffer.from(segments[2], 'base64url'))) throw unauthorized('desktop permit signature is invalid');
36
+ const seconds = Math.floor(now() / 1000);
37
+ if (claims.iss !== 'agentengine-server' || claims.aud !== 'agentkit-personal-assistant-desktop' || claims.scope !== 'desktop' || claims.agentId !== assistantId || !claims.botId || !claims.jti || !['read', 'control'].includes(claims.mode)) throw unauthorized('desktop permit binding is invalid');
38
+ if (!Number.isSafeInteger(claims.iat) || !Number.isSafeInteger(claims.exp) || claims.iat > seconds || claims.exp <= seconds || claims.exp - claims.iat > 60) throw unauthorized('desktop permit expired');
39
+ return Object.freeze(claims);
40
+ } catch (error) { throw error?.status === 401 ? error : unauthorized('desktop permit could not be verified'); }
41
+ }
42
+