@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.
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +38 -2
- package/cordis.patch.yml +6 -0
- package/dist/client.js +6 -0
- package/package.json +83 -4
- package/personal-assistant.plugin.json +31 -0
- package/personal-assistant.plugin.schema.json +43 -0
- package/runtime/assistant-runtime.mjs +317 -0
- package/runtime/check-manifest.mjs +21 -0
- package/runtime/cloud-browser-cdp.mjs +313 -0
- package/runtime/cloud-browser.mjs +972 -0
- package/runtime/cloud-execution.mjs +320 -0
- package/runtime/cloud-session-channel.mjs +45 -0
- package/runtime/computer-agentd.mjs +488 -0
- package/runtime/desktop-clipboard-limit.mjs +2 -0
- package/runtime/desktop-launcher.mjs +106 -0
- package/runtime/desktop-permit.mjs +42 -0
- package/runtime/desktop-session.mjs +840 -0
- package/runtime/desktop-ws.mjs +371 -0
- package/runtime/prepare-dsh-profile.mjs +17 -0
- package/runtime/welcome-server.mjs +144 -0
- package/scripts/assistant-runtime-launcher.sh +3 -0
- package/scripts/build-artifacts.mjs +81 -0
- package/scripts/build-client.mjs +82 -0
- package/scripts/computer-agentd-launcher.sh +3 -0
- package/scripts/deterministic_tar.py +21 -0
- package/scripts/install-local.mjs +262 -0
- package/scripts/scan-secrets.mjs +41 -0
- package/scripts/sync-public.mjs +52 -0
- package/scripts/verification/gateway-desktop-client.mjs +142 -0
- package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
- package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
- package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
- package/scripts/verify-cloud-turn.mjs +18 -0
- package/scripts/verify-computer-broker.mjs +10 -0
- package/src/automation-tools.mjs +18 -0
- package/src/bot-private-tools.mjs +23 -0
- package/src/bot-registry.mjs +157 -0
- package/src/bot-tool-policy.mjs +48 -0
- package/src/bot-tool-scope.mjs +40 -0
- package/src/client/bot-directory.mjs +23 -0
- package/src/client/components/desktop-clipboard.mjs +194 -0
- package/src/client/conversation.mjs +558 -0
- package/src/client/desktop-control.mjs +70 -0
- package/src/client/desktop-view.mjs +68 -0
- package/src/client/entry.mjs +8 -0
- package/src/client/index.mjs +733 -0
- package/src/client/markdown.mjs +130 -0
- package/src/client/native-workbench.mjs +62 -0
- package/src/cloud-computer-tools.mjs +245 -0
- package/src/cloud-files.mjs +57 -0
- package/src/cloud-model.mjs +10 -0
- package/src/cloud-session-transport.mjs +76 -0
- package/src/computer-broker-protocol.mjs +116 -0
- package/src/computer-broker.mjs +220 -0
- package/src/desktop-tools.mjs +64 -0
- package/src/dsh-session-bridge.mjs +207 -0
- package/src/dsh-web-bridge.mjs +127 -0
- package/src/host-gateway.mjs +281 -0
- package/src/host-session-channel.mjs +410 -0
- package/src/implementation-capabilities.mjs +10 -0
- package/src/index.mjs +148 -0
- package/src/persistent-state.mjs +201 -0
- package/src/platform-manifest.mjs +24 -0
- package/src/plugin-manifest.mjs +82 -0
- package/src/schedule-manager.mjs +88 -0
- package/src/system-fetch.mjs +72 -0
- package/src/workspace-tools.mjs +22 -0
- package/src/workspace.mjs +22 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const MAX_RECORDS = 10_000;
|
|
6
|
+
const MAX_TEXT_BYTES = 64 * 1024;
|
|
7
|
+
const identifier = (value, field) => {
|
|
8
|
+
const result = String(value ?? '').trim();
|
|
9
|
+
if (!/^[a-zA-Z0-9._:-]{1,128}$/u.test(result)) throw Object.assign(new TypeError(`${field} is invalid`), { code: 'STATE_IDENTIFIER_INVALID' });
|
|
10
|
+
return result;
|
|
11
|
+
};
|
|
12
|
+
const text = (value, field, required = true) => {
|
|
13
|
+
const result = String(value ?? '').trim();
|
|
14
|
+
if (required && !result) throw Object.assign(new TypeError(`${field} is required`), { code: 'STATE_FIELD_REQUIRED' });
|
|
15
|
+
if (Buffer.byteLength(result, 'utf8') > MAX_TEXT_BYTES) throw Object.assign(new TypeError(`${field} is too large`), { code: 'STATE_FIELD_TOO_LARGE' });
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Durable profile-local state for sessions, memory and schedules.
|
|
21
|
+
*
|
|
22
|
+
* The state directory is supplied by runtime-service on the user's PVC. The
|
|
23
|
+
* plugin never removes it during disable/uninstall; DSH owns bundle selection
|
|
24
|
+
* while this store owns only the records created by the assistant runtime.
|
|
25
|
+
*/
|
|
26
|
+
export class PersonalAssistantStateStore {
|
|
27
|
+
constructor(root) {
|
|
28
|
+
this.root = String(root ?? '').trim();
|
|
29
|
+
if (!this.root.startsWith('/')) throw new TypeError('state root must be an absolute path');
|
|
30
|
+
this.pending = Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init() {
|
|
34
|
+
await mkdir(this.root, { recursive: true, mode: 0o700 });
|
|
35
|
+
for (const name of ['sessions', 'memory', 'schedules', 'messages', 'turns']) {
|
|
36
|
+
const path = this.file(name);
|
|
37
|
+
try { await readFile(path, 'utf8'); } catch (error) {
|
|
38
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
39
|
+
await writeFile(path, '[]', { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
file(name) { return join(this.root, `${name}.json`); }
|
|
46
|
+
|
|
47
|
+
async read(name) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(await readFile(this.file(name), 'utf8'));
|
|
50
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.code === 'ENOENT') return [];
|
|
53
|
+
throw Object.assign(new Error(`state file ${name} is invalid`), { code: 'STATE_CORRUPT', cause: error });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async write(name, rows) {
|
|
58
|
+
if (!Array.isArray(rows) || rows.length > MAX_RECORDS) throw new TypeError(`state collection ${name} is invalid`);
|
|
59
|
+
const target = this.file(name);
|
|
60
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
61
|
+
await writeFile(temporary, `${JSON.stringify(rows)}\n`, { mode: 0o600 });
|
|
62
|
+
await rename(temporary, target);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
mutate(name, operation) {
|
|
66
|
+
const run = this.pending.then(async () => {
|
|
67
|
+
const rows = await this.read(name);
|
|
68
|
+
const result = await operation(rows);
|
|
69
|
+
await this.write(name, rows);
|
|
70
|
+
return result;
|
|
71
|
+
});
|
|
72
|
+
this.pending = run.catch(() => {});
|
|
73
|
+
return run;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async sessions(botId) {
|
|
77
|
+
const id = identifier(botId, 'botId');
|
|
78
|
+
return (await this.read('sessions')).filter(row => row.botId === id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
messages({ botId, sessionId } = {}) {
|
|
82
|
+
const id = identifier(botId, 'botId');
|
|
83
|
+
const session = identifier(sessionId, 'sessionId');
|
|
84
|
+
return this.read('messages').then(rows => rows.filter(row => row.botId === id && row.sessionId === session));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
appendMessage({ botId, sessionId, role, content, metadata = {} } = {}) {
|
|
88
|
+
const id = identifier(botId, 'botId');
|
|
89
|
+
const session = identifier(sessionId, 'sessionId');
|
|
90
|
+
const messageRole = identifier(role, 'role');
|
|
91
|
+
if (!['system', 'user', 'assistant', 'tool'].includes(messageRole)) {
|
|
92
|
+
throw Object.assign(new TypeError('role is invalid'), { code: 'MESSAGE_ROLE_INVALID' });
|
|
93
|
+
}
|
|
94
|
+
const value = text(content, 'content');
|
|
95
|
+
return this.mutate('messages', rows => {
|
|
96
|
+
const message = Object.freeze({
|
|
97
|
+
messageId: `message-${randomUUID()}`,
|
|
98
|
+
botId: id,
|
|
99
|
+
sessionId: session,
|
|
100
|
+
role: messageRole,
|
|
101
|
+
content: value,
|
|
102
|
+
metadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? { ...metadata } : {},
|
|
103
|
+
createdAt: new Date().toISOString(),
|
|
104
|
+
});
|
|
105
|
+
rows.push(message);
|
|
106
|
+
return message;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
turns(botId, sessionId) {
|
|
111
|
+
const id = identifier(botId, 'botId');
|
|
112
|
+
const session = identifier(sessionId, 'sessionId');
|
|
113
|
+
return this.read('turns').then(rows => rows.filter(row => row.botId === id && row.sessionId === session));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
createTurn({ botId, sessionId, input, metadata = {} } = {}) {
|
|
117
|
+
const id = identifier(botId, 'botId');
|
|
118
|
+
const session = identifier(sessionId, 'sessionId');
|
|
119
|
+
const value = text(input, 'input');
|
|
120
|
+
return this.mutate('turns', rows => {
|
|
121
|
+
const turn = Object.freeze({
|
|
122
|
+
turnId: `turn-${randomUUID()}`,
|
|
123
|
+
botId: id,
|
|
124
|
+
sessionId: session,
|
|
125
|
+
input: value,
|
|
126
|
+
status: 'queued',
|
|
127
|
+
metadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? { ...metadata } : {},
|
|
128
|
+
createdAt: new Date().toISOString(),
|
|
129
|
+
updatedAt: new Date().toISOString(),
|
|
130
|
+
});
|
|
131
|
+
rows.push(turn);
|
|
132
|
+
return turn;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
createSession({ botId, title = '', metadata = {} } = {}) {
|
|
137
|
+
const id = identifier(botId, 'botId');
|
|
138
|
+
return this.mutate('sessions', rows => {
|
|
139
|
+
const session = Object.freeze({
|
|
140
|
+
sessionId: `session-${randomUUID()}`,
|
|
141
|
+
botId: id,
|
|
142
|
+
title: text(title, 'title', false),
|
|
143
|
+
metadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? { ...metadata } : {},
|
|
144
|
+
createdAt: new Date().toISOString(),
|
|
145
|
+
updatedAt: new Date().toISOString(),
|
|
146
|
+
});
|
|
147
|
+
rows.push(session);
|
|
148
|
+
return session;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
memory(botId, query = '') {
|
|
153
|
+
const id = identifier(botId, 'botId');
|
|
154
|
+
const needle = text(query, 'query', false).toLowerCase();
|
|
155
|
+
return this.read('memory').then(rows => rows.filter(row => row.botId === id && (!needle || `${row.key}\n${row.content}`.toLowerCase().includes(needle))));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
putMemory({ botId, key, content, metadata = {} } = {}) {
|
|
159
|
+
const id = identifier(botId, 'botId');
|
|
160
|
+
const memoryKey = identifier(key, 'key');
|
|
161
|
+
const value = text(content, 'content');
|
|
162
|
+
return this.mutate('memory', rows => {
|
|
163
|
+
const index = rows.findIndex(row => row.botId === id && row.key === memoryKey);
|
|
164
|
+
const next = { botId: id, key: memoryKey, content: value, metadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? { ...metadata } : {}, updatedAt: new Date().toISOString() };
|
|
165
|
+
if (index < 0) rows.push(next); else rows[index] = next;
|
|
166
|
+
return next;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
schedules(botId) {
|
|
171
|
+
const id = identifier(botId, 'botId');
|
|
172
|
+
return this.read('schedules').then(rows => rows.filter(row => row.botId === id));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
putSchedule({ botId, scheduleId, runAt, metadata = {} } = {}) {
|
|
176
|
+
const id = identifier(botId, 'botId');
|
|
177
|
+
const schedule = identifier(scheduleId, 'scheduleId');
|
|
178
|
+
const timestamp = Number(runAt);
|
|
179
|
+
if (!Number.isSafeInteger(timestamp) || timestamp < 0) throw Object.assign(new TypeError('runAt is invalid'), { code: 'SCHEDULE_INVALID' });
|
|
180
|
+
return this.mutate('schedules', rows => {
|
|
181
|
+
const index = rows.findIndex(row => row.botId === id && row.scheduleId === schedule);
|
|
182
|
+
const next = { botId: id, scheduleId: schedule, nextRunAt: timestamp, metadata: metadata && typeof metadata === 'object' && !Array.isArray(metadata) ? { ...metadata } : {}, updatedAt: new Date().toISOString() };
|
|
183
|
+
if (index < 0) rows.push(next); else rows[index] = next;
|
|
184
|
+
return next;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
removeSchedule(botId, scheduleId) {
|
|
189
|
+
const id = identifier(botId, 'botId');
|
|
190
|
+
const schedule = identifier(scheduleId, 'scheduleId');
|
|
191
|
+
return this.mutate('schedules', rows => {
|
|
192
|
+
const before = rows.length;
|
|
193
|
+
rows.splice(0, rows.length, ...rows.filter(row => !(row.botId === id && row.scheduleId === schedule)));
|
|
194
|
+
return before !== rows.length;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createPersonalAssistantStateStore(root) {
|
|
200
|
+
return new PersonalAssistantStateStore(root);
|
|
201
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
/** 只读取平台挂载的原始声明;不从 Studio 或本地 profile 获取云端提示词。 */
|
|
6
|
+
export function loadPersonalAssistantManifest(env, { parseDocument } = {}) {
|
|
7
|
+
const fail = code => { throw Object.assign(new Error(code), { code }); };
|
|
8
|
+
if (env.AGENTENGINE_MANIFEST_PROTOCOL !== 'runtime-manifest/v1' || env.AGENTENGINE_MANAGED_RUNTIME_NAME !== 'personal-assistant'
|
|
9
|
+
|| !env.AGENTENGINE_MANAGED_RUNTIME_VERSION || !/^[a-f0-9]{64}$/u.test(env.AGENTENGINE_MANIFEST_SHA256 || '')) fail('MANAGED_RUNTIME_IDENTITY_INVALID');
|
|
10
|
+
const raw = readFileSync(env.AGENTENGINE_MANIFEST_PATH);
|
|
11
|
+
if (raw.length > 900 * 1024 || createHash('sha256').update(raw).digest('hex') !== env.AGENTENGINE_MANIFEST_SHA256) fail('MANAGED_MANIFEST_DIGEST_MISMATCH');
|
|
12
|
+
let manifest;
|
|
13
|
+
try {
|
|
14
|
+
const parse = parseDocument ?? createRequire('/opt/agentkit/dsh-runtime/package.json')('yaml').parseDocument;
|
|
15
|
+
const document = parse(new TextDecoder('utf-8', { fatal: true }).decode(raw), { uniqueKeys: true, version: '1.2' });
|
|
16
|
+
if (document.errors.length || document.warnings.length) throw Error('invalid YAML');
|
|
17
|
+
manifest = document.toJS({ maxAliasCount: 0 });
|
|
18
|
+
} catch { fail('MANAGED_MANIFEST_INVALID'); }
|
|
19
|
+
if (!manifest || manifest.framework !== 'personal-assistant' || manifest.artifact_type !== 'ManagedRuntime'
|
|
20
|
+
|| manifest.runtime?.name !== 'personal-assistant' || manifest.runtime?.version !== env.AGENTENGINE_MANAGED_RUNTIME_VERSION) fail('MANAGED_RUNTIME_IDENTITY_MISMATCH');
|
|
21
|
+
if (typeof manifest.model !== 'string' || !manifest.model.trim() || manifest.model.length > 512) fail('MANAGED_MODEL_REQUIRED');
|
|
22
|
+
if (typeof manifest.prompt !== 'string' || Buffer.byteLength(manifest.prompt) > 900 * 1024 || manifest.prompt.includes('\0')) fail('MANAGED_PROMPT_INVALID');
|
|
23
|
+
return manifest;
|
|
24
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validators for the two manifest surfaces used by this project.
|
|
3
|
+
*
|
|
4
|
+
* `package.json.dsh` follows the DSH loader contract. The separate
|
|
5
|
+
* `personal-assistant.plugin.json` is an AgentKit application metadata file;
|
|
6
|
+
* DSH does not read its `metadata` values.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const FORMAT = 'agentkit-personal-assistant.plugin/v1';
|
|
10
|
+
const PLUGIN_ID = '@kingsoftcloud/agentkit-personal-assistant';
|
|
11
|
+
const REQUIRED_DSH = '0.2.1-rc.2';
|
|
12
|
+
const REQUIRED_HOST_API = `dsh-native/${REQUIRED_DSH}`;
|
|
13
|
+
|
|
14
|
+
function fail(message) {
|
|
15
|
+
throw new TypeError(`Invalid personal-assistant manifest: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function string(value, field) {
|
|
19
|
+
if (typeof value !== 'string' || value.trim() === '') fail(`${field} must be a non-empty string`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function strings(value, field) {
|
|
24
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || item.trim() === '')) fail(`${field} must be an array of non-empty strings`);
|
|
25
|
+
if (new Set(value).size !== value.length) fail(`${field} must not contain duplicates`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function object(value, field) {
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${field} must be an object`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Validate the subset of package.json.dsh consumed by DSH's loader. */
|
|
35
|
+
export function validateDshPackageManifest(manifest) {
|
|
36
|
+
const root = object(manifest, 'package.json');
|
|
37
|
+
string(root.name, 'package.json.name');
|
|
38
|
+
string(root.version, 'package.json.version');
|
|
39
|
+
const dsh = root.dsh === undefined ? {} : object(root.dsh, 'package.json.dsh');
|
|
40
|
+
const bundle = dsh.bundle === undefined ? undefined : object(dsh.bundle, 'package.json.dsh.bundle');
|
|
41
|
+
if (bundle !== undefined) {
|
|
42
|
+
const patch = bundle.patch;
|
|
43
|
+
if (!(typeof patch === 'string' && patch.trim() !== '') && !(Array.isArray(patch) && patch.length > 0 && patch.every(item => typeof item === 'string' && item.trim() !== ''))) {
|
|
44
|
+
fail('package.json.dsh.bundle.patch must be a path or non-empty path array');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const client = dsh.client === undefined ? undefined : object(dsh.client, 'package.json.dsh.client');
|
|
48
|
+
if (client !== undefined) {
|
|
49
|
+
string(client.platform, 'package.json.dsh.client.platform');
|
|
50
|
+
if (client.inject !== undefined) strings(client.inject, 'package.json.dsh.client.inject');
|
|
51
|
+
if (client.external !== undefined) strings(client.external, 'package.json.dsh.client.external');
|
|
52
|
+
if (client.immediately !== undefined && typeof client.immediately !== 'boolean') fail('package.json.dsh.client.immediately must be a boolean');
|
|
53
|
+
}
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Validate the application-owned personal-assistant.plugin.json shape. */
|
|
58
|
+
export function validatePersonalAssistantManifest(manifest) {
|
|
59
|
+
const root = object(manifest, 'personal-assistant.plugin.json');
|
|
60
|
+
const allowedRoot = new Set(['$schema', 'format', 'pluginId', 'version', 'compatibility', 'metadata']);
|
|
61
|
+
for (const key of Object.keys(root)) if (!allowedRoot.has(key)) fail(`unrecognized top-level field: ${key}`);
|
|
62
|
+
if (root.format !== FORMAT) fail(`format must be ${FORMAT}`);
|
|
63
|
+
if (root.pluginId !== PLUGIN_ID) fail(`pluginId must be ${PLUGIN_ID}`);
|
|
64
|
+
string(root.version, 'version');
|
|
65
|
+
const compatibility = object(root.compatibility, 'compatibility');
|
|
66
|
+
if (compatibility.requiredDsh !== REQUIRED_DSH) fail(`compatibility.requiredDsh must be ${REQUIRED_DSH}`);
|
|
67
|
+
if (compatibility.requiredHostApi !== REQUIRED_HOST_API) fail(`compatibility.requiredHostApi must be ${REQUIRED_HOST_API}`);
|
|
68
|
+
if (!Number.isInteger(compatibility.requiredHostContract) || compatibility.requiredHostContract < 1) fail('compatibility.requiredHostContract must be a positive integer');
|
|
69
|
+
const allowedMetadata = new Set(['defaultEnabled', 'autoConnect', 'capabilities', 'contributes', 'entrypoints']);
|
|
70
|
+
const metadata = object(root.metadata, 'metadata');
|
|
71
|
+
for (const key of Object.keys(metadata)) if (!allowedMetadata.has(key)) fail(`unrecognized metadata field: ${key}`);
|
|
72
|
+
if (typeof metadata.defaultEnabled !== 'boolean') fail('metadata.defaultEnabled must be a boolean');
|
|
73
|
+
if (typeof metadata.autoConnect !== 'boolean') fail('metadata.autoConnect must be a boolean');
|
|
74
|
+
strings(metadata.capabilities, 'metadata.capabilities');
|
|
75
|
+
strings(metadata.contributes, 'metadata.contributes');
|
|
76
|
+
const entrypoints = object(metadata.entrypoints, 'metadata.entrypoints');
|
|
77
|
+
for (const key of Object.keys(entrypoints)) if (!new Set(['host', 'client']).has(key)) fail(`unrecognized metadata.entrypoints field: ${key}`);
|
|
78
|
+
if (entrypoints.host !== '.' || entrypoints.client !== './client') fail('metadata.entrypoints must point to package exports . and ./client');
|
|
79
|
+
return root;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const PERSONAL_ASSISTANT_MANIFEST_FORMAT = FORMAT;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small deterministic schedule manager owned by one assistant profile.
|
|
3
|
+
*
|
|
4
|
+
* It stores schedule metadata and asks the host to run a callback at a future
|
|
5
|
+
* time. Durable persistence is supplied by the runtime image's state path;
|
|
6
|
+
* this class deliberately has no process-wide timers or cross-user state.
|
|
7
|
+
*/
|
|
8
|
+
export const PERSONAL_ASSISTANT_SCHEDULE_CAPABILITY = 'personal-assistant-schedule-manager/v1';
|
|
9
|
+
|
|
10
|
+
const ID = /^[a-z][a-z0-9._:-]{0,127}$/u;
|
|
11
|
+
function identifier(value, field) {
|
|
12
|
+
const result = String(value ?? '').trim();
|
|
13
|
+
if (!ID.test(result)) throw new TypeError(`${field} has an invalid format`);
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function at(value) {
|
|
17
|
+
const time = value instanceof Date ? value.getTime() : Number(value);
|
|
18
|
+
if (!Number.isSafeInteger(time) || time < 0) throw new TypeError('runAt must be a non-negative timestamp');
|
|
19
|
+
return time;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* In-memory scheduler with stable nextRunAt/cancel semantics. A host can
|
|
24
|
+
* persist `snapshot()` and restore it on restart before scheduling callbacks.
|
|
25
|
+
*/
|
|
26
|
+
export class ScheduleManager {
|
|
27
|
+
constructor({ now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout } = {}) {
|
|
28
|
+
this.now = now;
|
|
29
|
+
this.setTimeoutFn = setTimeoutFn;
|
|
30
|
+
this.clearTimeoutFn = clearTimeoutFn;
|
|
31
|
+
this.schedules = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
scheduleOnce({ scheduleId, runAt, task, metadata = {} } = {}) {
|
|
35
|
+
const id = identifier(scheduleId, 'scheduleId');
|
|
36
|
+
if (typeof task !== 'function') throw new TypeError('task must be a function');
|
|
37
|
+
const nextRunAt = at(runAt);
|
|
38
|
+
if (nextRunAt <= this.now()) throw new RangeError('runAt must be in the future');
|
|
39
|
+
this.cancel(id);
|
|
40
|
+
const record = { scheduleId: id, nextRunAt, metadata: Object.freeze({ ...metadata }), timer: null };
|
|
41
|
+
const delay = Math.max(1, nextRunAt - this.now());
|
|
42
|
+
record.timer = this.setTimeoutFn(async () => {
|
|
43
|
+
this.schedules.delete(id);
|
|
44
|
+
await task({ scheduleId: id, scheduledAt: nextRunAt });
|
|
45
|
+
}, delay);
|
|
46
|
+
record.timer?.unref?.();
|
|
47
|
+
this.schedules.set(id, record);
|
|
48
|
+
return this.describe(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cancel(scheduleId) {
|
|
52
|
+
const id = identifier(scheduleId, 'scheduleId');
|
|
53
|
+
const record = this.schedules.get(id);
|
|
54
|
+
if (!record) return false;
|
|
55
|
+
this.clearTimeoutFn(record.timer);
|
|
56
|
+
this.schedules.delete(id);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
nextRunAt(scheduleId) {
|
|
61
|
+
const id = identifier(scheduleId, 'scheduleId');
|
|
62
|
+
return this.schedules.get(id)?.nextRunAt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe(scheduleId) {
|
|
66
|
+
const id = identifier(scheduleId, 'scheduleId');
|
|
67
|
+
const record = this.schedules.get(id);
|
|
68
|
+
if (!record) return undefined;
|
|
69
|
+
return Object.freeze({ scheduleId: record.scheduleId, nextRunAt: record.nextRunAt, metadata: record.metadata });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
snapshot() {
|
|
73
|
+
return Object.freeze([...this.schedules.values()].map(record => ({
|
|
74
|
+
scheduleId: record.scheduleId,
|
|
75
|
+
nextRunAt: record.nextRunAt,
|
|
76
|
+
metadata: { ...record.metadata },
|
|
77
|
+
})));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
dispose() {
|
|
81
|
+
for (const record of this.schedules.values()) this.clearTimeoutFn(record.timer);
|
|
82
|
+
this.schedules.clear();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createScheduleManager(options) {
|
|
87
|
+
return new ScheduleManager(options);
|
|
88
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 带系统信任链的 fetch。
|
|
3
|
+
*
|
|
4
|
+
* 预发/内网网关(`pa-desktop-dev.agent-pre.kspmas.ksyun.com` 等)用的是内网 CA 签发的证书:
|
|
5
|
+
* macOS 钥匙串信任它,但 Node 自带的 CA 库不信任,于是 `fetch` 直接抛
|
|
6
|
+
* `TypeError: fetch failed`(cause: self-signed certificate in certificate chain),
|
|
7
|
+
* 面板就表现为「检查失败 · fetch failed」「暂时无法读取助理列表」。
|
|
8
|
+
*
|
|
9
|
+
* 这里用 `node:https` + `tls.getCACertificates('system')`(Node ≥ 22.15)把系统证书并入
|
|
10
|
+
* 信任链;只有 https 才走这条路径,plain http 与 data/相对 URL 仍交给全局 fetch。
|
|
11
|
+
*/
|
|
12
|
+
import https from 'node:https';
|
|
13
|
+
import tls from 'node:tls';
|
|
14
|
+
|
|
15
|
+
let cachedCA;
|
|
16
|
+
|
|
17
|
+
/** 系统信任链 + Node 内置根证书;取不到系统证书时退回内置根证书。 */
|
|
18
|
+
export function trustedCAs() {
|
|
19
|
+
if (cachedCA !== undefined) return cachedCA;
|
|
20
|
+
const bundled = Array.isArray(tls.rootCertificates) ? tls.rootCertificates : [];
|
|
21
|
+
let system = [];
|
|
22
|
+
try {
|
|
23
|
+
if (typeof tls.getCACertificates === 'function') system = tls.getCACertificates('system');
|
|
24
|
+
} catch { system = []; }
|
|
25
|
+
cachedCA = [...bundled, ...(Array.isArray(system) ? system : [])];
|
|
26
|
+
return cachedCA;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** HEADERS 输入可能是 Headers / 数组 / 普通对象,统一成普通对象交给 https.request。 */
|
|
30
|
+
function headerObject(input) {
|
|
31
|
+
if (!input) return {};
|
|
32
|
+
if (typeof Headers === 'function' && input instanceof Headers) return Object.fromEntries(input.entries());
|
|
33
|
+
if (Array.isArray(input)) return Object.fromEntries(input);
|
|
34
|
+
return { ...input };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 构造一个与全局 fetch 形状兼容、但对 https 使用系统信任链的实现。
|
|
39
|
+
* @param {typeof fetch} [fallback] 非 https 请求的兜底实现,默认全局 fetch。
|
|
40
|
+
* @returns {typeof fetch} fetch 兼容函数
|
|
41
|
+
*/
|
|
42
|
+
export function createSystemFetch(fallback = globalThis.fetch) {
|
|
43
|
+
return async function systemFetch(input, init = {}) {
|
|
44
|
+
const raw = typeof input === 'string' || input instanceof URL ? String(input) : String(input?.url ?? input);
|
|
45
|
+
let url;
|
|
46
|
+
try { url = new URL(raw); } catch { return fallback(input, init); }
|
|
47
|
+
if (url.protocol !== 'https:') return fallback(input, init);
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const request = https.request(url, {
|
|
50
|
+
method: init.method ?? 'GET',
|
|
51
|
+
headers: headerObject(init.headers),
|
|
52
|
+
ca: trustedCAs(),
|
|
53
|
+
signal: init.signal ?? undefined,
|
|
54
|
+
rejectUnauthorized: true,
|
|
55
|
+
}, response => {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
response.on('data', chunk => chunks.push(chunk));
|
|
58
|
+
response.on('end', () => {
|
|
59
|
+
const body = Buffer.concat(chunks);
|
|
60
|
+
resolve(new Response(body, { status: response.statusCode ?? 500, statusText: response.statusMessage ?? '', headers: headerObject(response.headers) }));
|
|
61
|
+
});
|
|
62
|
+
response.on('error', reject);
|
|
63
|
+
});
|
|
64
|
+
request.on('error', reject);
|
|
65
|
+
const body = init.body;
|
|
66
|
+
if (body === undefined || body === null) request.end();
|
|
67
|
+
else if (typeof body === 'string' || Buffer.isBuffer(body)) request.end(body);
|
|
68
|
+
else if (typeof body === 'object' && typeof body.pipe === 'function') body.pipe(request);
|
|
69
|
+
else request.end(String(body));
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// 文件和技能均由电脑上的相同 Landlock/租约链执行,不读取 Runtime 私有目录。
|
|
2
|
+
export function createWorkspaceTool({ action } = {}) {
|
|
3
|
+
return {
|
|
4
|
+
name: 'cloud_workspace',
|
|
5
|
+
description: 'List/read/write files in the shared cloud workspace, discover skill SKILL.md files, or read a chosen skill before running its commands with cloud_bash. Paths must be relative to the workspace. Text read/write is bounded to 64 KiB. Deliver generated files with a Markdown link [下载文件](cloud-file:URL_ENCODED_RELATIVE_PATH); downloads are limited to 512 KiB per file. Internet is available by default for installing public dependencies and skills.',
|
|
6
|
+
parameters: { type: 'object', required: ['action'], additionalProperties: false, properties: {
|
|
7
|
+
action: { type: 'string', enum: ['list', 'read', 'write', 'skills'] }, path: { type: 'string' }, text: { type: 'string' },
|
|
8
|
+
} },
|
|
9
|
+
output: { schema: { type: 'object', required: ['result'], properties: { result: { type: 'string' } } }, render: (_args, value) => [{ type: 'text', text: value.result }] },
|
|
10
|
+
async execute(args, execution) {
|
|
11
|
+
if (!['list', 'read', 'write', 'skills'].includes(args.action)) throw new TypeError('unsupported workspace action');
|
|
12
|
+
const path = String(args.path || '.');
|
|
13
|
+
if (path.startsWith('/') || path.split(/[\\/]/u).includes('..') || /[\u0000-\u001f]/u.test(path) || path.length > 2048) throw new TypeError('path must stay inside the cloud workspace');
|
|
14
|
+
if (args.action === 'write' && (typeof args.text !== 'string' || Buffer.byteLength(args.text) > 65536)) throw new TypeError('text exceeds 64 KiB');
|
|
15
|
+
const request = JSON.stringify({ ...args, path });
|
|
16
|
+
const code = `import json,pathlib,os\na=json.loads(${JSON.stringify(request)})\np=pathlib.Path(a['path'])\nroot=pathlib.Path.cwd().resolve()\np=p.resolve()\nassert p==root or root in p.parents,'path escapes workspace'\nif a['action']=='skills':\n rows=[]\n for folder,dirs,files in os.walk(root/'skills',followlinks=False):\n dirs[:]=[d for d in dirs if not pathlib.Path(folder,d).is_symlink()][:128]\n if 'SKILL.md' in files:\n rows.append(str(pathlib.Path(folder,'SKILL.md').relative_to(root)))\n if len(rows)>=100: break\n print(json.dumps({'skills':rows},ensure_ascii=False))\nelif a['action']=='list':\n print(json.dumps({'files':[{'name':q.name,'directory':q.is_dir()} for q in sorted(p.iterdir())[:200]]},ensure_ascii=False))\nelif a['action']=='read':\n with p.open('rb') as f: data=f.read(65537)\n assert len(data)<=65536,'file exceeds 64 KiB; use cloud_bash for bounded processing'\n print(json.dumps({'path':str(p.relative_to(root)),'text':data.decode('utf-8')},ensure_ascii=False))\nelse:\n p.parent.mkdir(parents=True,exist_ok=True)\n p.write_text(a['text'],encoding='utf-8')\n print(json.dumps({'path':str(p.relative_to(root)),'bytes':len(a['text'].encode())}))`;
|
|
17
|
+
const result = await action('terminal.exec', { language: 'python', code }, execution);
|
|
18
|
+
if (result.exitCode !== 0) throw new Error(result.stderr || 'cloud workspace operation failed');
|
|
19
|
+
return { result: result.stdout.trim() };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Workspace contract shared by runtime tools and the computer broker. */
|
|
2
|
+
export const PERSONAL_ASSISTANT_WORKSPACE_CAPABILITY = 'personal-assistant-workspace-managed/v1';
|
|
3
|
+
export const WORKSPACE_PATH_ENV = 'WORKSPACE_PATH';
|
|
4
|
+
|
|
5
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
|
|
8
|
+
export function workspaceDir(value = process.env.WORKSPACE_PATH ?? '/workspace') {
|
|
9
|
+
const result = String(value).trim();
|
|
10
|
+
if (!isAbsolute(result)) throw new TypeError('workspace path must be absolute');
|
|
11
|
+
return resolve(result);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function ensureWorkspace(value) {
|
|
15
|
+
const directory = workspaceDir(value);
|
|
16
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
17
|
+
return directory;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function workspaceMount({ path = process.env.WORKSPACE_PATH, readOnly = false } = {}) {
|
|
21
|
+
return Object.freeze({ path: workspaceDir(path), readOnly: Boolean(readOnly) });
|
|
22
|
+
}
|