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

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 +733 -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 +281 -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,371 @@
1
+ /** Minimal authenticated WebSocket bridge for the Studio desktop view. */
2
+ import { createHash, timingSafeEqual } from 'node:crypto';
3
+ import { verifyDesktopPermit, desktopUnauthorized as unauthorized } from './desktop-permit.mjs';
4
+ // 云端桌面的剪贴板上限与 computer 容器保持一致;这里不导入 desktop-session.mjs,
5
+ // 因为该模块只打进 computer 产物,runtime 镜像里不存在(导入会让插件启动失败)。
6
+ import { DESKTOP_CLIPBOARD_MAX_BYTES } from './desktop-clipboard-limit.mjs';
7
+ import { DIRECTORY_BOT_ID, publicBotDirectory } from '../src/client/bot-directory.mjs';
8
+
9
+ const PROTOCOL = 'agentkit.personal-assistant.desktop.v1';
10
+ const maxFrame = 300 * 1024;
11
+ const frame = (opcode, payload = Buffer.alloc(0)) => {
12
+ const data = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
13
+ const length = data.length;
14
+ if (length < 126) return Buffer.concat([Buffer.from([0x80 | opcode, length]), data]);
15
+ if (length < 65536) { const h = Buffer.alloc(4); h[0] = 0x80 | opcode; h[1] = 126; h.writeUInt16BE(length, 2); return Buffer.concat([h, data]); }
16
+ const h = Buffer.alloc(10); h[0] = 0x80 | opcode; h[1] = 127; h.writeBigUInt64BE(BigInt(length), 2); return Buffer.concat([h, data]);
17
+ };
18
+ const accept = key => createHash('sha1').update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest('base64');
19
+ const jsonFrame = value => frame(1, JSON.stringify(value));
20
+ export const DESKTOP_MAX_PENDING_BYTES = 2 * (maxFrame + 1024);
21
+ export const CLOUD_SESSION_MAX_PENDING_BYTES = 1024 * 1024;
22
+
23
+ function createBoundedSocketWriter(socket, maxPendingBytes) {
24
+ let blocked = false;
25
+ const drain = () => { blocked = false; };
26
+ socket.on('drain', drain);
27
+ return {
28
+ get ready() { return !socket.destroyed && !blocked && socket.writableLength < maxPendingBytes; },
29
+ write(packet) {
30
+ if (socket.destroyed) return false;
31
+ if (socket.writableLength + packet.length > maxPendingBytes) { socket.destroy(); return false; }
32
+ if (!socket.write(packet)) blocked = true;
33
+ return true;
34
+ },
35
+ dispose() { socket.off('drain', drain); },
36
+ };
37
+ }
38
+ /** 慢桌面客户端最多保留两帧;等待 drain 时跳帧,不建立帧队列。 */
39
+ export const createDesktopSocketWriter = socket => createBoundedSocketWriter(socket, DESKTOP_MAX_PENDING_BYTES);
40
+ /** 独立会话连接容纳一个 512 KiB 文件的 base64 响应;仍有固定内存上限。 */
41
+ export const createCloudSessionSocketWriter = socket => createBoundedSocketWriter(socket, CLOUD_SESSION_MAX_PENDING_BYTES);
42
+ export { verifyDesktopPermit } from './desktop-permit.mjs';
43
+ const loopback = address => ['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(address);
44
+ const equalToken = (left, right) => {
45
+ const first = Buffer.from(left), second = Buffer.from(right);
46
+ return first.length === second.length && timingSafeEqual(first, second);
47
+ };
48
+
49
+ function parseFrames(state, chunk, onFrame) {
50
+ state.buffer = Buffer.concat([state.buffer, chunk]);
51
+ while (state.buffer.length >= 2) {
52
+ const first = state.buffer[0]; const second = state.buffer[1]; const masked = Boolean(second & 0x80); let length = second & 0x7f; let offset = 2;
53
+ if (length === 126) { if (state.buffer.length < 4) return; length = state.buffer.readUInt16BE(2); offset = 4; }
54
+ else if (length === 127) { if (state.buffer.length < 10) return; const value = state.buffer.readBigUInt64BE(2); if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('frame too large'); length = Number(value); offset = 10; }
55
+ if (!masked || length > 1024 * 1024) throw new Error('invalid client frame');
56
+ if (state.buffer.length < offset + 4 + length) return;
57
+ const key = state.buffer.subarray(offset, offset + 4); offset += 4;
58
+ const payload = Buffer.from(state.buffer.subarray(offset, offset + length)); state.buffer = state.buffer.subarray(offset + length);
59
+ for (let i = 0; i < payload.length; i += 1) payload[i] ^= key[i % 4];
60
+ onFrame(first & 0x0f, payload);
61
+ }
62
+ }
63
+
64
+ export function attachDesktopWebSocket({ server, runtimeToken, assistantId, brokerDesktopRequest, brokerDesktopFrameBinary, botIdOf, taskIdOf, listBots, sessionRequest, env = process.env }) {
65
+ const usedPermits = new Map();
66
+ server.on('upgrade', async (request, socket, head) => {
67
+ const url = new URL(request.url || '/', 'http://desktop.local');
68
+ const pathname = url.pathname;
69
+ if (!['/v1/desktop/ws', '/v1/computer/desktop/ws'].includes(pathname)) { socket.destroy(); return; }
70
+ if (url.search) { socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'); socket.destroy(); return; }
71
+ const authorization = String(request.headers.authorization || '');
72
+ const direct = loopback(request.socket.remoteAddress) && runtimeToken && equalToken(authorization, `Bearer ${runtimeToken}`);
73
+ let claims = null;
74
+ try {
75
+ if (!direct) {
76
+ claims = await verifyDesktopPermit(String(request.headers['x-agentengine-desktop-permit'] || ''), { assistantId, env });
77
+ for (const [nonce, expires] of usedPermits) if (expires <= Date.now()) usedPermits.delete(nonce);
78
+ if (usedPermits.has(claims.jti)) throw unauthorized('desktop permit already used');
79
+ usedPermits.set(claims.jti, claims.exp * 1000);
80
+ }
81
+ } catch { socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n'); socket.destroy(); return; }
82
+ const requested = String(request.headers['sec-websocket-protocol'] || '').split(',').map(value => value.trim()).filter(Boolean);
83
+ if (!requested.includes(PROTOCOL)) { socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n'); socket.destroy(); return; }
84
+ const protocol = PROTOCOL;
85
+ const key = request.headers['sec-websocket-key'];
86
+ if (!key) { socket.destroy(); return; }
87
+ socket.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept(key)}\r\nSec-WebSocket-Protocol: ${protocol}\r\n\r\n`);
88
+ if (head?.length) socket.unshift(head);
89
+ const state = { buffer: Buffer.alloc(0), sessionId: null, botId: null, taskId: null, leaseId: null, autoLease: false, closed: false, paused: false, idle: false, idleTimer: null, idleTransition: null, suspendSupported: false, epoch: 0, lastInputAt: Date.now(), lastFrameSeq: -1, timer: null, renewTimer: null, frameBusy: false, pending: Promise.resolve(), pendingMessages: 0 };
90
+ const writer = createDesktopSocketWriter(socket);
91
+ let sessionWriter;
92
+ const send = value => { if (!state.closed) (value.type === 'session.response' ? sessionWriter || writer : writer).write(jsonFrame(value)); };
93
+ const releaseResources = async () => {
94
+ const sessionId = state.sessionId, leaseId = state.autoLease ? state.leaseId : null;
95
+ state.sessionId = null; state.autoLease = false; state.leaseId = null;
96
+ // 独立发起清理,避免一个失联的 close 请求挡住租约释放。
97
+ await Promise.allSettled([
98
+ ...(sessionId ? [Promise.resolve().then(() => brokerDesktopRequest('/broker/desktop/close', { sessionId, botId: state.botId, taskId: state.taskId }))] : []),
99
+ ...(leaseId && !state.handoff ? [Promise.resolve().then(() => brokerDesktopRequest('/broker/lease/release', { botId: state.botId, taskId: state.taskId, leaseId }))] : []),
100
+ ]);
101
+ };
102
+ const close = async ({ event, closeCode = 1000 } = {}) => {
103
+ if (state.closed) return releaseResources();
104
+ if (event) send({ type: 'closed', retryable: false, ...event });
105
+ state.closed = true; state.epoch++; clearTimeout(state.timer); clearInterval(state.renewTimer); clearTimeout(state.idleTimer);
106
+ state.timer = null; state.renewTimer = null; writer.dispose(); sessionWriter?.dispose();
107
+ const payload = Buffer.alloc(2); payload.writeUInt16BE(closeCode);
108
+ try { socket.end(frame(8, Buffer.concat([payload, Buffer.from(event?.reason || 'desktop-closed')]))); } catch {}
109
+ await releaseResources();
110
+ };
111
+ const fail = async (error, fallback) => {
112
+ if (state.closed) return;
113
+ const status = error?.status || 400, code = error?.code || fallback;
114
+ const message = String(error?.message || 'desktop request failed');
115
+ send({ type: 'error', status, code, message });
116
+ if ([404, 409].includes(Number(status))) await close({ event: { status, code, message, reason: Number(status) === 409 ? 'lease-lost' : 'session-ended' } });
117
+ };
118
+ const armIdle = () => {
119
+ clearTimeout(state.idleTimer);
120
+ if (!state.suspendSupported || state.closed || state.idle || state.requestedMode === 'read' || state.handoff) return;
121
+ state.idleTimer = setTimeout(() => {
122
+ state.pending = state.pending.then(() => suspend(false)).catch(error => fail(error, 'DESKTOP_IDLE_FAILED'));
123
+ }, Math.max(0, state.idleTimeoutMs - (Date.now() - state.lastInputAt)));
124
+ state.idleTimer.unref?.();
125
+ };
126
+ const startRenewal = () => {
127
+ clearInterval(state.renewTimer);
128
+ if (!state.autoLease || state.idle || state.closed) return;
129
+ const leaseId = state.leaseId, epoch = state.epoch;
130
+ state.renewTimer = setInterval(() => {
131
+ if (state.closed || state.idle || state.epoch !== epoch) return;
132
+ if (state.handoff && Date.now() - state.lastInputAt >= state.idleTimeoutMs) {
133
+ state.pending = state.pending.then(async () => {
134
+ if (!state.leaseId || state.closed) return;
135
+ const changed = await brokerDesktopRequest('/broker/desktop/control/release', { sessionId: state.sessionId, botId: state.botId, taskId: state.taskId, leaseId: state.leaseId, controlEpoch: state.controlEpoch });
136
+ applyControl(changed);
137
+ }).catch(error => fail(error, 'DESKTOP_CONTROL_RELEASE_FAILED'));
138
+ return;
139
+ }
140
+ void brokerDesktopRequest('/broker/lease/renew', { botId: state.botId, taskId: state.taskId, leaseId, ttlMs: 120000 }).catch(async error => {
141
+ if (state.closed || state.idle || state.epoch !== epoch) return;
142
+ if (state.handoff && Number(error?.status) === 409) {
143
+ try { await syncControl(); } catch (syncError) { await fail(syncError, 'DESKTOP_CONTROL_SYNC_FAILED'); }
144
+ return;
145
+ }
146
+ await fail(error, 'COMPUTER_LEASE_RENEW_FAILED');
147
+ if (!state.closed) await close({ event: { reason: 'broker-unavailable', code: error?.code || 'COMPUTER_LEASE_RENEW_FAILED', status: error?.status || 503, retryable: true }, closeCode: 1011 });
148
+ });
149
+ }, 30_000);
150
+ state.renewTimer.unref?.();
151
+ };
152
+ const applyControl = changed => {
153
+ state.controlEpoch = changed.controlEpoch; state.leaseId = changed.readOnly ? null : changed.lease?.leaseId || state.leaseId;
154
+ state.autoLease = !changed.readOnly; state.epoch++; state.lastInputAt = Date.now(); startRenewal();
155
+ send({ type: 'control', ...changed, lease: undefined, protocol: PROTOCOL });
156
+ };
157
+ const syncControl = async () => {
158
+ const changed = await brokerDesktopRequest('/broker/desktop/control/state', { sessionId: state.sessionId, botId: state.botId, taskId: state.taskId });
159
+ if (!state.closed) applyControl(changed);
160
+ };
161
+ const suspend = async fromBroker => {
162
+ if (state.closed || state.idle || !state.sessionId || !state.suspendSupported) return;
163
+ if (!fromBroker && Date.now() - state.lastInputAt < state.idleTimeoutMs) { armIdle(); return; }
164
+ state.idle = true; state.epoch++;
165
+ clearTimeout(state.timer); clearTimeout(state.idleTimer); clearInterval(state.renewTimer);
166
+ state.timer = null; state.renewTimer = null;
167
+ state.idleTransition = (async () => {
168
+ await brokerDesktopRequest('/broker/desktop/idle', { sessionId: state.sessionId, botId: state.botId, taskId: state.taskId, leaseId: state.leaseId });
169
+ if (state.autoLease && state.leaseId) await brokerDesktopRequest('/broker/lease/release', { botId: state.botId, taskId: state.taskId, leaseId: state.leaseId });
170
+ state.autoLease = false; state.leaseId = state.externalLeaseId || null;
171
+ if (!state.closed) send({ type: 'idle', sessionId: state.sessionId, reason: 'idle-timeout', leaseReleased: !state.externalLeaseId, protocol: PROTOCOL });
172
+ })();
173
+ try { await state.idleTransition; } finally { state.idleTransition = null; }
174
+ };
175
+ const activate = opened => {
176
+ state.idle = false; state.epoch++; state.lastInputAt = Date.now(); state.lastFrameSeq = -1;
177
+ state.controlEpoch = opened.controlEpoch ?? 0;
178
+ state.suspendSupported = opened.idleMode === 'suspend'; state.idleTimeoutMs = Math.max(1000, Number(opened.idleTimeoutMs) || 600000);
179
+ startRenewal(); armIdle();
180
+ send({ type: 'ready', ...opened, protocol: PROTOCOL }); void poll();
181
+ };
182
+ const poll = async () => {
183
+ if (state.closed || state.idle || state.paused || !state.sessionId || state.frameBusy) return;
184
+ if (!writer.ready) { state.timer = setTimeout(poll, 80); return; }
185
+ state.frameBusy = true;
186
+ const epoch = state.epoch;
187
+ const startedAt = Date.now();
188
+ let nextFrameDelayMs = Date.now() - state.lastInputAt < 10_000 ? 80 : 1000;
189
+ try {
190
+ if (typeof brokerDesktopFrameBinary !== 'function') throw Object.assign(new Error('binary desktop broker is unavailable'), { code: 'DESKTOP_BINARY_BROKER_REQUIRED' });
191
+ const result = await brokerDesktopFrameBinary({ sessionId: state.sessionId, afterSeq: state.lastFrameSeq });
192
+ if (state.closed || state.idle || state.epoch !== epoch) return;
193
+ if (Number.isFinite(result.nextFrameDelayMs)) nextFrameDelayMs = Math.max(50, Math.min(1000, result.nextFrameDelayMs));
194
+ if (!result.unchanged && result.data) {
195
+ const metadata = { type: 'frame', seq: result.seq, format: result.format, width: result.width, height: result.height, bytes: result.bytes };
196
+ if (!Buffer.isBuffer(result.data)) throw Object.assign(new Error('desktop broker did not return binary data'), { code: 'DESKTOP_BINARY_FRAME_REQUIRED' });
197
+ const bytes = result.data;
198
+ if (bytes.length > maxFrame) throw Object.assign(new Error('desktop frame too large'), { code: 'DESKTOP_FRAME_TOO_LARGE' });
199
+ // 元数据和 JPEG 一起写,回压期间不会只发送半对消息。
200
+ if (!state.closed && writer.ready && writer.write(Buffer.concat([jsonFrame(metadata), frame(2, bytes)]))) state.lastFrameSeq = result.seq;
201
+ } else send({ type: 'frame', seq: result.seq, unchanged: true, width: result.width, height: result.height });
202
+ } catch (error) {
203
+ if (!state.closed && state.epoch === epoch) {
204
+ if (state.suspendSupported && error?.code === 'DESKTOP_SESSION_IDLE') {
205
+ try { await suspend(true); } catch (idleError) { await fail(idleError, 'DESKTOP_IDLE_FAILED'); }
206
+ }
207
+ else await fail(error, 'DESKTOP_FRAME_FAILED');
208
+ }
209
+ }
210
+ finally {
211
+ state.frameBusy = false;
212
+ if (!state.closed && !state.idle && !state.paused) state.timer = setTimeout(poll, Math.max(0, nextFrameDelayMs - (Date.now() - startedAt)));
213
+ }
214
+ };
215
+ const onMessage = async (opcode, payload) => {
216
+ if (state.closed) return;
217
+ if (opcode === 8) return close();
218
+ if (opcode === 9) { writer.write(frame(10, payload)); return; }
219
+ if (opcode !== 1) return;
220
+ let body; try { body = JSON.parse(payload.toString('utf8')); } catch { send({ type: 'error', code: 'INVALID_JSON' }); return; }
221
+ let brokerStarted = false;
222
+ try {
223
+ if (body.type === 'session.request') {
224
+ // control 票据授权交互执行;目录/只读桌面票据不能读取会话或提交回合。
225
+ // 不打开桌面、不申请电脑租约;真实云端工具执行时独立获取租约。
226
+ if (claims && (claims.mode !== 'control' || claims.botId === DIRECTORY_BOT_ID)) throw Object.assign(new Error('会话执行需要具名助理控制授权'), { code: 'CLOUD_SESSION_CONTROL_REQUIRED', status: 403 });
227
+ if (!/^[A-Za-z0-9_-]{1,80}$/u.test(String(body.requestId || ''))) throw Object.assign(new Error('会话请求标识无效'), { code: 'CLOUD_SESSION_REQUEST_INVALID', status: 400 });
228
+ if (body.agentId !== assistantId || (claims && body.botId !== claims.botId)) throw Object.assign(new Error('会话身份与票据不一致'), { code: 'CLOUD_SESSION_IDENTITY_MISMATCH', status: 403 });
229
+ if (state.sessionId) throw Object.assign(new Error('会话与桌面需要独立连接'), { code: 'CLOUD_SESSION_CONNECTION_CONFLICT', status: 409 });
230
+ if (typeof sessionRequest !== 'function') throw Object.assign(new Error('请升级云端会话服务'), { code: 'CLOUD_SESSION_UNAVAILABLE', status: 503 });
231
+ sessionWriter ??= createCloudSessionSocketWriter(socket);
232
+ try {
233
+ const value = await sessionRequest({ agentId: assistantId, botId: botIdOf(body.botId), operation: body.operation, payload: body.payload });
234
+ send({ type: 'session.response', requestId: body.requestId, agentId: assistantId, botId: body.botId, ok: true, value });
235
+ } catch (error) {
236
+ send({ type: 'session.response', requestId: body.requestId, agentId: assistantId, botId: body.botId, ok: false, error: { code: error.code || 'CLOUD_SESSION_FAILED', message: error.status ? error.message : '云端会话请求失败', status: error.status || 503 } });
237
+ }
238
+ return;
239
+ }
240
+ if (body.type === 'bots.list') {
241
+ if (claims && (claims.mode !== 'read' || claims.botId !== DIRECTORY_BOT_ID)) throw Object.assign(new Error('助理目录需要独立只读授权'), { code: 'DESKTOP_DIRECTORY_SCOPE_REQUIRED', status: 403 });
242
+ if (state.sessionId || typeof listBots !== 'function') throw Object.assign(new Error('助理目录暂不可用'), { code: 'DESKTOP_DIRECTORY_UNAVAILABLE', status: 503 });
243
+ send({ type: 'bots', bots: publicBotDirectory(await listBots()), features: ['desktop-handoff/v1'] });
244
+ return close();
245
+ }
246
+ if (body.type === 'open') {
247
+ if (sessionWriter) throw Object.assign(new Error('会话与桌面需要独立连接'), { code: 'CLOUD_SESSION_CONNECTION_CONFLICT', status: 409 });
248
+ if (claims?.botId === DIRECTORY_BOT_ID) throw Object.assign(new Error('目录授权不能打开桌面'), { code: 'DESKTOP_DIRECTORY_READ_ONLY', status: 403 });
249
+ if (state.sessionId) throw Object.assign(new Error('desktop session already open'), { code: 'DESKTOP_SESSION_ALREADY_OPEN', status: 409 });
250
+ if (claims && body.botId !== claims.botId) throw Object.assign(new Error('desktop Bot does not match permit'), { code: 'DESKTOP_BOT_MISMATCH', status: 403 });
251
+ if (claims?.mode === 'control' && body.mode !== 'observe') throw Object.assign(new Error('请更新客户端,通过旁观会话接管云电脑'), { code: 'DESKTOP_HANDOFF_REQUIRED', status: 403 });
252
+ if (claims?.mode === 'read' && (['control', 'observe'].includes(body.mode) || body.leaseId)) throw Object.assign(new Error('desktop permit is read-only'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
253
+ if (body.mode === 'observe' && (!claims || claims.mode !== 'control')) throw Object.assign(new Error('无签名控制授权不能启用桌面接管'), { code: 'DESKTOP_CONTROL_GRANT_REQUIRED', status: 403 });
254
+ if (claims && body.leaseId && body.leaseId !== claims.leaseId) throw Object.assign(new Error('desktop lease does not match permit'), { code: 'DESKTOP_LEASE_MISMATCH', status: 403 });
255
+ state.botId = botIdOf(body.botId); state.taskId = taskIdOf(body.taskId); state.leaseId = body.leaseId || null;
256
+ state.handoff = body.mode === 'observe';
257
+ state.externalLeaseId = state.leaseId; state.requestedMode = state.handoff ? 'observe' : (claims ? claims.mode : body.mode) === 'control' ? 'control' : 'read';
258
+ brokerStarted = true;
259
+ if (!state.handoff && (claims ? claims.mode : body.mode) === 'control' && !state.leaseId) {
260
+ const acquired = await brokerDesktopRequest('/broker/lease/acquire', { botId: state.botId, taskId: state.taskId, idempotencyKey: `desktop-ws:${state.botId}:${state.taskId}:${Date.now()}`, ttlMs: 120000 });
261
+ state.leaseId = acquired.lease?.leaseId || null; state.autoLease = true;
262
+ if (state.closed) return releaseResources();
263
+ }
264
+ const opened = await brokerDesktopRequest('/broker/desktop/session', { botId: state.botId, taskId: state.taskId, leaseId: state.leaseId || undefined, releaseLeaseOnIdle: state.autoLease,
265
+ ...(state.handoff ? { mode: 'observe', controlPermit: String(request.headers['x-agentengine-desktop-permit'] || '') } : {}) });
266
+ state.sessionId = opened.sessionId;
267
+ if (state.closed) return releaseResources();
268
+ activate(opened); return;
269
+ }
270
+ if (body.type === 'control.acquire' || body.type === 'control.release') {
271
+ if (!state.sessionId || !state.handoff || claims?.mode !== 'control' || claims.botId === DIRECTORY_BOT_ID) throw Object.assign(new Error('当前授权只允许旁观'), { code: 'DESKTOP_CONTROL_GRANT_REQUIRED', status: 403 });
272
+ if (!Number.isSafeInteger(body.controlEpoch) || body.controlEpoch !== state.controlEpoch) throw Object.assign(new Error('控制权状态已更新,请重新操作'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
273
+ const sessionId = state.sessionId;
274
+ const changed = await brokerDesktopRequest(`/broker/desktop/control/${body.type === 'control.acquire' ? 'acquire' : 'release'}`, { sessionId, botId: state.botId, taskId: state.taskId, leaseId: state.leaseId, controlEpoch: body.controlEpoch });
275
+ if (state.closed) { await brokerDesktopRequest('/broker/desktop/close', { sessionId, botId: state.botId, taskId: state.taskId }); return; }
276
+ applyControl(changed);
277
+ return;
278
+ }
279
+ if (body.type === 'input') {
280
+ if (claims?.mode === 'read') throw Object.assign(new Error('只读授权不能发送输入'), { code: 'DESKTOP_READ_ONLY', status: 403 });
281
+ if (!state.sessionId) throw Object.assign(new Error('desktop session is not open'), { code: 'DESKTOP_SESSION_NOT_FOUND', status: 404 });
282
+ if (state.idle) { send({ type: 'error', status: 409, code: 'DESKTOP_SESSION_IDLE', recoverable: true, message: '桌面已空闲,请先恢复连接' }); return; }
283
+ if (state.handoff && (!state.leaseId || body.controlEpoch !== state.controlEpoch)) throw Object.assign(new Error('输入未持有当前控制权'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
284
+ brokerStarted = true;
285
+ const accepted = await brokerDesktopRequest('/broker/desktop/input', { sessionId: state.sessionId, botId: state.botId, taskId: state.taskId, leaseId: state.leaseId, controlEpoch: state.controlEpoch, event: body.event });
286
+ if (state.closed) return;
287
+ state.lastInputAt = Date.now(); armIdle();
288
+ if (state.timer) clearTimeout(state.timer);
289
+ if (!state.frameBusy) state.timer = setTimeout(poll, 0);
290
+ send({ type: 'input-accepted', ...accepted }); return;
291
+ }
292
+ if (body.type === 'clipboard.read' || body.type === 'clipboard.write') {
293
+ // 面板与云电脑之间只有这条 WS 是活通道;剪贴板复用 agentd 的
294
+ // /broker/action,不在这里重新实现 X11 读写。
295
+ const requestId = String(body.requestId || '');
296
+ // 非法标识无法安全回显,直接回通用错误帧;合法标识的错误统一走
297
+ // clipboard.response,客户端只按 requestId 关联。
298
+ if (!/^[A-Za-z0-9_-]{1,80}$/u.test(requestId)) { send({ type: 'error', status: 400, code: 'CLIPBOARD_REQUEST_INVALID', message: '剪贴板请求标识无效' }); return; }
299
+ try {
300
+ if (claims?.mode === 'read') throw Object.assign(new Error('只读授权不能读写剪贴板'), { code: 'DESKTOP_READ_ONLY', status: 403 });
301
+ if (!state.sessionId) throw Object.assign(new Error('desktop session is not open'), { code: 'DESKTOP_SESSION_NOT_FOUND', status: 404 });
302
+ if (state.idle) throw Object.assign(new Error('桌面已空闲,请先恢复连接'), { code: 'DESKTOP_SESSION_IDLE', status: 409 });
303
+ if (state.handoff && (!state.leaseId || body.controlEpoch !== state.controlEpoch)) throw Object.assign(new Error('剪贴板未持有当前控制权'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
304
+ if (body.type === 'clipboard.write' && typeof body.text !== 'string') throw Object.assign(new Error('剪贴板文本必须是字符串'), { code: 'CLIPBOARD_TEXT_INVALID', status: 400 });
305
+ if (body.type === 'clipboard.write' && Buffer.byteLength(body.text, 'utf8') > DESKTOP_CLIPBOARD_MAX_BYTES) throw Object.assign(new Error('剪贴板文本不能超过 64 KiB'), { code: 'CLIPBOARD_TEXT_TOO_LARGE', status: 413 });
306
+ brokerStarted = true;
307
+ const value = await brokerDesktopRequest('/broker/action', { botId: state.botId, taskId: state.taskId, leaseId: state.leaseId, action: body.type, args: body.type === 'clipboard.write' ? { text: body.text } : {} });
308
+ if (state.closed) return;
309
+ send({ type: 'clipboard.response', requestId, ok: true, value });
310
+ return;
311
+ } catch (error) {
312
+ if (state.closed) return;
313
+ // 控制权在操作期间变化时同步一次 epoch,客户端下一帧即可重试。
314
+ if (state.handoff && Number(error?.status) === 409) { try { await syncControl(); } catch {} }
315
+ send({ type: 'clipboard.response', requestId, ok: false, error: { code: error?.code || 'CLIPBOARD_FAILED', message: error?.status ? String(error?.message || '剪贴板请求失败') : '云端剪贴板请求失败', status: error?.status || 400 } });
316
+ return;
317
+ }
318
+ }
319
+ if (body.type === 'close') return close();
320
+ if (body.type === 'pause') { state.paused = true; if (state.timer) clearTimeout(state.timer); return; }
321
+ if (body.type === 'resume') {
322
+ if (claims?.mode === 'read' && body.mode === 'control') throw Object.assign(new Error('desktop permit is read-only'), { code: 'DESKTOP_READ_ONLY', status: 403 });
323
+ state.paused = false;
324
+ if (!state.idle) { void poll(); return; }
325
+ if (body.activity !== true) return;
326
+ await state.idleTransition;
327
+ if (state.closed) return;
328
+ let mode = body.mode || state.requestedMode;
329
+ if (!['read', 'control'].includes(mode) || (claims?.mode === 'read' && mode === 'control')) throw Object.assign(new Error('desktop permit is read-only'), { code: 'DESKTOP_READ_ONLY', status: 403 });
330
+ brokerStarted = true;
331
+ if (mode === 'control' && !state.leaseId) {
332
+ try {
333
+ const acquired = await brokerDesktopRequest('/broker/lease/acquire', { botId: state.botId, taskId: state.taskId, idempotencyKey: `desktop-resume:${state.botId}:${state.taskId}:${Date.now()}`, ttlMs: 120000 });
334
+ state.leaseId = acquired.lease?.leaseId || null; state.autoLease = true;
335
+ if (state.closed) return releaseResources();
336
+ } catch (error) {
337
+ if (Number(error?.status) !== 409) throw error;
338
+ mode = 'read'; send({ type: 'error', status: 409, code: error.code || 'COMPUTER_LEASE_CONFLICT', recoverable: true, message: '其他助理正在使用云电脑,已恢复为只读旁观' });
339
+ }
340
+ }
341
+ const opened = await brokerDesktopRequest('/broker/desktop/resume', { sessionId: state.sessionId, botId: state.botId, taskId: state.taskId, leaseId: mode === 'control' ? state.leaseId || undefined : undefined, releaseLeaseOnIdle: mode === 'control' && state.autoLease });
342
+ if (state.closed) return releaseResources();
343
+ state.requestedMode = mode; activate(opened); return;
344
+ }
345
+ send({ type: 'error', code: 'DESKTOP_MESSAGE_UNSUPPORTED' });
346
+ } catch (error) {
347
+ if (state.handoff && (String(body.type).startsWith('control.') || body.type === 'input')) {
348
+ if (Number(error.status) === 409) { try { await syncControl(); } catch {} }
349
+ send({ type: 'error', status: error.status || 400, code: error.code || 'DESKTOP_CONTROL_FAILED', recoverable: true, readOnly: !state.leaseId, controlEpoch: state.controlEpoch, message: error.message || '控制权暂不可用,仍可继续旁观' });
350
+ return;
351
+ }
352
+ if (body.type === 'resume' && state.idle && state.autoLease && state.leaseId) {
353
+ try {
354
+ await brokerDesktopRequest('/broker/lease/release', { botId: state.botId, taskId: state.taskId, leaseId: state.leaseId });
355
+ state.autoLease = false; state.leaseId = state.externalLeaseId || null;
356
+ } catch { await close({ event: { reason: 'resume-cleanup-failed', status: 503 }, closeCode: 1011 }); return; }
357
+ }
358
+ if ((brokerStarted || body.type === 'input') && [404, 409].includes(Number(error?.status))) return fail(error, 'DESKTOP_REQUEST_FAILED');
359
+ if (!state.sessionId && state.autoLease && state.leaseId) {
360
+ await releaseResources();
361
+ }
362
+ send({ type: 'error', status: error?.status || 400, code: error?.code || 'DESKTOP_REQUEST_FAILED', message: String(error?.message || 'desktop request failed') });
363
+ }
364
+ };
365
+ socket.on('data', chunk => { try { parseFrames(state, chunk, (opcode, payload) => {
366
+ if (++state.pendingMessages > 64) { socket.destroy(); void close(); return; }
367
+ state.pending = state.pending.then(() => onMessage(opcode, payload)).catch(() => close()).finally(() => { state.pendingMessages -= 1; });
368
+ }); } catch { close(); } });
369
+ socket.on('close', () => { void close(); }); socket.on('error', () => { void close(); });
370
+ });
371
+ }
@@ -0,0 +1,17 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { loadPersonalAssistantManifest } from '../src/platform-manifest.mjs';
4
+ import { cloudModelDefinition } from '../src/cloud-model.mjs';
5
+
6
+ // 只借用固定 DSH 安装里的 YAML 解析器;配置中只写凭据变量名,不写密钥。
7
+ const env = process.env;
8
+ const manifest = loadPersonalAssistantManifest(env);
9
+ const baseURL = new URL(env.OPENAI_BASE_URL);
10
+ if (!['http:', 'https:'].includes(baseURL.protocol) || baseURL.username || baseURL.password || baseURL.search || baseURL.hash || !env.AGENTKIT_MODEL_API_KEY) throw new Error('MANAGED_MODEL_CONFIGURATION_INVALID');
11
+ const overlay = [
12
+ { id: 'llm-pi-ai', name: '@deepseek-ai/dsh-llm-pi-ai', config: { providers: { 'personal-assistant-cloud': { displayName: '个人助理云端模型', apiKeyEnv: 'AGENTKIT_MODEL_API_KEY', api: 'openai-completions', baseURL: baseURL.href.replace(/\/$/, ''), models: [cloudModelDefinition(manifest.model)] } } } },
13
+ { id: 'agent-default-model', config: { provider: 'personal-assistant-cloud', model: manifest.model } },
14
+ ];
15
+ await mkdir(env.DSH_HOME, { recursive: true, mode: 0o700 });
16
+ await writeFile(join(env.DSH_HOME, 'platform-model.patch.yml'), JSON.stringify(overlay, null, 2), { mode: 0o600 });
17
+ console.info(JSON.stringify({ event: 'personal-assistant.model.configured', model: manifest.model, manifestSha256: env.AGENTENGINE_MANIFEST_SHA256 }));
@@ -0,0 +1,144 @@
1
+ // 欢迎页本机桥(建议放在 personal-assistant-computer/runtime/welcome-server.mjs)
2
+ // 职责:
3
+ // 1) 在 127.0.0.1 上起一个只本机可达的 HTTP 服务,静态提供 web/ 目录(无目录穿越)
4
+ // 2) POST /launch 只接受 {"action":"browser"|"terminal"|"files"|"dsh"},再交给 launch() 执行
5
+ // 3) GET /dsh-web.json 返回云端 DSH 网页的本机回环地址(由 runtime 侧注入,不落盘、不写日志)
6
+ //
7
+ // 安全边界(与既有 launcher 一致):
8
+ // - 只监听 127.0.0.1,端口随机(port 0)
9
+ // - 只允许 GET/HEAD(静态)与 POST(/launch)
10
+ // - 请求体上限 256 字节,content-type 必须是 application/json
11
+ // - action 白名单硬编码,永不接受 URL / 路径 / 命令
12
+ // - 静态路径用 resolve + 前缀校验,且只允许白名单后缀
13
+
14
+ import { createServer } from 'node:http';
15
+ import { readFile } from 'node:fs/promises';
16
+ import { extname, join, resolve, sep } from 'node:path';
17
+
18
+ const ACTIONS = new Set(['browser', 'terminal', 'files', 'dsh']);
19
+ const MIME = {
20
+ '.html': 'text/html; charset=utf-8',
21
+ '.css': 'text/css; charset=utf-8',
22
+ '.js': 'text/javascript; charset=utf-8',
23
+ '.svg': 'image/svg+xml',
24
+ '.png': 'image/png',
25
+ '.webp': 'image/webp',
26
+ '.woff2': 'font/woff2',
27
+ };
28
+ const MAX_BODY = 256;
29
+ const failure = (code, message) => Object.assign(new Error(message), { code, expose: true });
30
+
31
+ const sendJson = (res, status, value) => {
32
+ const body = JSON.stringify(value);
33
+ res.writeHead(status, {
34
+ 'content-type': 'application/json; charset=utf-8',
35
+ 'content-length': Buffer.byteLength(body),
36
+ 'cache-control': 'no-store',
37
+ });
38
+ res.end(body);
39
+ };
40
+
41
+ export async function createWelcomeServer({ webDir, launch, timeoutMs = 8000, onError = () => {}, timeZone = process.env.AGENTKIT_DESKTOP_TIME_ZONE || 'Asia/Shanghai', dshWeb = null }) {
42
+ const root = resolve(webDir);
43
+ let closing = false;
44
+ // 欢迎页时钟按桌面时区显示:容器通常是 UTC,而用户按北京时间读表。
45
+ const config = Object.freeze({ timeZone: String(timeZone) });
46
+ // 云端 DSH 网页地址只允许本机回环:这是同一 Pod 内的服务,不能让欢迎页把用户带去外网。
47
+ const dshWebUrl = () => {
48
+ if (typeof dshWeb !== 'function') return null;
49
+ let value;
50
+ try { value = dshWeb(); } catch { return null; }
51
+ if (typeof value !== 'string' || !value) return null;
52
+ try {
53
+ const parsed = new URL(value);
54
+ if (!['http:', 'https:'].includes(parsed.protocol)) return null;
55
+ if (!['127.0.0.1', 'localhost', '[::1]'].includes(parsed.hostname)) return null;
56
+ return parsed.href;
57
+ } catch { return null; }
58
+ };
59
+
60
+ const serveStatic = async (pathname, res) => {
61
+ // 只映射到 root 内的白名单后缀文件
62
+ const rel = pathname === '/' || pathname === '/welcome.html' ? 'welcome.html' : pathname.replace(/^\/+/, '');
63
+ const target = resolve(join(root, rel));
64
+ if (target !== root && !target.startsWith(root + sep)) { res.writeHead(403).end(); return; }
65
+ const type = MIME[extname(target).toLowerCase()];
66
+ if (!type) { res.writeHead(404).end(); return; }
67
+ let data;
68
+ try { data = await readFile(target); } catch { res.writeHead(404).end(); return; }
69
+ res.writeHead(200, {
70
+ 'content-type': type,
71
+ 'content-length': data.length,
72
+ // 每次打开都要拿到最新页面:不缓存
73
+ 'cache-control': 'no-store, must-revalidate',
74
+ 'x-content-type-options': 'nosniff',
75
+ });
76
+ res.end(data);
77
+ };
78
+
79
+ const handleLaunch = (req, res) => {
80
+ const type = String(req.headers['content-type'] || '');
81
+ if (!type.toLowerCase().startsWith('application/json')) { sendJson(res, 415, { ok: false, code: 'WELCOME_CONTENT_TYPE' }); return; }
82
+ let body = Buffer.alloc(0);
83
+ let done = false;
84
+ const finish = (status, value) => { if (done) return; done = true; sendJson(res, status, value); };
85
+ req.on('data', chunk => {
86
+ if (done) return;
87
+ body = Buffer.concat([body, chunk]);
88
+ if (body.length > MAX_BODY) { req.destroy(); finish(413, { ok: false, code: 'WELCOME_BODY_TOO_LARGE' }); }
89
+ });
90
+ req.on('error', () => { if (!done) { done = true; res.destroy(); } });
91
+ req.on('end', () => {
92
+ if (done) return;
93
+ let action;
94
+ try { action = JSON.parse(body.toString('utf8')).action; } catch { finish(400, { ok: false, code: 'WELCOME_INVALID' }); return; }
95
+ if (!ACTIONS.has(action)) { finish(400, { ok: false, code: 'WELCOME_ACTION_DENIED' }); return; }
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
98
+ void (async () => {
99
+ try {
100
+ if (closing) throw failure('WELCOME_CLOSED', '桌面入口已停止');
101
+ await launch(action, { signal: controller.signal });
102
+ if (!done) finish(200, { ok: true });
103
+ } catch (error) {
104
+ const code = String(error && error.code || 'WELCOME_LAUNCH_FAILED').slice(0, 64);
105
+ if (!closing) onError({ action, code });
106
+ if (!done) finish(code === 'WELCOME_CLOSED' ? 503 : 500, { ok: false, code });
107
+ } finally { clearTimeout(timer); }
108
+ })();
109
+ });
110
+ };
111
+
112
+ const server = createServer((req, res) => {
113
+ if (closing) { sendJson(res, 503, { ok: false, code: 'WELCOME_CLOSED' }); return; }
114
+ const pathname = (req.url || '/').split('?')[0];
115
+ if (req.method === 'GET' || req.method === 'HEAD') {
116
+ if (pathname === '/config.json') { sendJson(res, 200, config); return; }
117
+ if (pathname === '/dsh-web.json') {
118
+ const url = dshWebUrl();
119
+ if (url) sendJson(res, 200, { url });
120
+ else sendJson(res, 404, { ok: false, code: 'DSH_WEB_UNAVAILABLE' });
121
+ return;
122
+ }
123
+ void serveStatic(pathname, res);
124
+ return;
125
+ }
126
+ if (req.method === 'POST' && pathname === '/launch') { handleLaunch(req, res); return; }
127
+ sendJson(res, 405, { ok: false, code: 'WELCOME_METHOD' });
128
+ });
129
+
130
+ await new Promise((resolvePromise, reject) => {
131
+ server.once('error', reject);
132
+ server.listen(0, '127.0.0.1', resolvePromise);
133
+ });
134
+ const { port } = server.address();
135
+
136
+ return Object.freeze({
137
+ port,
138
+ url: `http://127.0.0.1:${port}/welcome.html`,
139
+ async close() {
140
+ closing = true;
141
+ await new Promise(resolvePromise => server.close(() => resolvePromise()));
142
+ },
143
+ });
144
+ }
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ exec node "$(dirname "$0")/../runtime/assistant-runtime.mjs" "$@"
@@ -0,0 +1,81 @@
1
+ import { cp, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const exec = promisify(execFile);
9
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
+ const output = resolve(process.env.PA_ARTIFACT_OUTPUT || join(root, 'dist', 'artifacts'));
11
+ if (Number(process.versions.node.split('.')[0]) !== 22) throw new Error(`artifact build requires Node 22, got ${process.versions.node}`);
12
+
13
+ async function hash(path) {
14
+ return createHash('sha256').update(await readFile(path)).digest('hex');
15
+ }
16
+
17
+ async function pack(role, files) {
18
+ await mkdir(output, { recursive: true });
19
+ const stage = await mkdtemp(join(output, `.stage-${role}-`));
20
+ try {
21
+ for (const [relative, source] of files) {
22
+ const target = join(stage, relative);
23
+ await mkdir(dirname(target), { recursive: true });
24
+ await cp(source, target, { recursive: true });
25
+ }
26
+ const archive = join(output, `agentkit-personal-assistant-${role}.tar`);
27
+ try {
28
+ await exec('tar', ['--sort=name', '--mtime=@0', '--owner=0', '--group=0', '--numeric-owner', '-cf', archive, '-C', stage, '.']);
29
+ } catch (error) {
30
+ const detail = String(error?.stderr || error?.message);
31
+ if (!detail.includes('unrecognized option') && !detail.includes('not supported')) throw error;
32
+ await exec('python3', [join(root, 'scripts', 'deterministic_tar.py'), stage, archive]);
33
+ }
34
+ const context = join(output, `${role}-context`, 'artifact');
35
+ await mkdir(context, { recursive: true });
36
+ await cp(archive, join(context, archive.split('/').at(-1)));
37
+ return { role, archive, context: dirname(context), sha256: await hash(archive) };
38
+ } finally {
39
+ await rm(stage, { recursive: true, force: true });
40
+ }
41
+ }
42
+
43
+ const results = [
44
+ await pack('runtime', [
45
+ ['bin/assistant-runtime', join(root, 'scripts', 'assistant-runtime-launcher.sh')],
46
+ ['runtime/assistant-runtime.mjs', join(root, 'runtime', 'assistant-runtime.mjs')],
47
+ ['runtime/cloud-session-channel.mjs', join(root, 'runtime', 'cloud-session-channel.mjs')],
48
+ ['runtime/desktop-ws.mjs', join(root, 'runtime', 'desktop-ws.mjs')],
49
+ ['runtime/desktop-clipboard-limit.mjs', join(root, 'runtime', 'desktop-clipboard-limit.mjs')],
50
+ ['runtime/desktop-permit.mjs', join(root, 'runtime', 'desktop-permit.mjs')],
51
+ ['src', join(root, 'src')],
52
+ // The cloud runtime is a real DSH host. Keep a self-contained plugin
53
+ // package beside the stateful launcher so the DSH profile can resolve it
54
+ // without reaching an npm registry at pod start.
55
+ ['plugin/package.json', join(root, 'package.json')],
56
+ ['plugin/cordis.patch.yml', join(root, 'cordis.patch.yml')],
57
+ ['plugin/personal-assistant.plugin.json', join(root, 'personal-assistant.plugin.json')],
58
+ ['plugin/personal-assistant.plugin.schema.json', join(root, 'personal-assistant.plugin.schema.json')],
59
+ ['plugin/src', join(root, 'src')],
60
+ ['plugin/runtime/assistant-runtime.mjs', join(root, 'runtime', 'assistant-runtime.mjs')],
61
+ ['plugin/runtime/cloud-session-channel.mjs', join(root, 'runtime', 'cloud-session-channel.mjs')],
62
+ ['plugin/runtime/desktop-ws.mjs', join(root, 'runtime', 'desktop-ws.mjs')],
63
+ ['plugin/runtime/desktop-clipboard-limit.mjs', join(root, 'runtime', 'desktop-clipboard-limit.mjs')],
64
+ ['plugin/runtime/desktop-permit.mjs', join(root, 'runtime', 'desktop-permit.mjs')],
65
+ ['plugin/runtime/prepare-dsh-profile.mjs', join(root, 'runtime', 'prepare-dsh-profile.mjs')],
66
+ ['plugin/runtime/check-manifest.mjs', join(root, 'runtime', 'check-manifest.mjs')],
67
+ ]),
68
+ await pack('computer', [
69
+ ['bin/computer-agentd', join(root, 'scripts', 'computer-agentd-launcher.sh')],
70
+ ['runtime/computer-agentd.mjs', join(root, 'runtime', 'computer-agentd.mjs')],
71
+ ['runtime/desktop-permit.mjs', join(root, 'runtime', 'desktop-permit.mjs')],
72
+ ['runtime/cloud-browser.mjs', join(root, 'runtime', 'cloud-browser.mjs')],
73
+ ['runtime/cloud-browser-cdp.mjs', join(root, 'runtime', 'cloud-browser-cdp.mjs')],
74
+ ['runtime/cloud-execution.mjs', join(root, 'runtime', 'cloud-execution.mjs')],
75
+ ['runtime/desktop-session.mjs', join(root, 'runtime', 'desktop-session.mjs')],
76
+ ['runtime/desktop-launcher.mjs', join(root, 'runtime', 'desktop-launcher.mjs')],
77
+ ['runtime/welcome-server.mjs', join(root, 'runtime', 'welcome-server.mjs')],
78
+ ['web/welcome.html', join(root, 'web', 'welcome.html')],
79
+ ]),
80
+ ];
81
+ console.log(JSON.stringify({ node: process.versions.node, artifacts: results }, null, 2));