@hellolynk-ai/codex-plugin 0.2.102-macos.1 → 0.2.103
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/README.md +17 -17
- package/bin/hellolynk-client-setup.mjs +7 -2
- package/hellolynk-client-pack-manifest.json +36 -56
- package/hellolynk-client-pack-pin.json +1 -1
- package/package.json +4 -8
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/hellolynk-codex/.mcp.json +19 -20
- package/plugins/hellolynk-codex/package.json +1 -1
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-darwin-host.mjs +45 -32
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +3 -3
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +47 -34
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +46 -16
- package/plugins/hellolynk-codex/runtime/core/hellolynk-darwin-host.mjs +45 -32
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +1 -1
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +34 -11
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +11 -27
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +1 -1
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +46 -10
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +2 -2
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -1193
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -33
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +42 -42
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/darwin/hellolynk-keychain-helper.swift +0 -149
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.bin +0 -0
- package/plugins/hellolynk-codex/runtime/platform/darwin/hellolynk-keychain-helper.swift +0 -149
|
@@ -1,1193 +1,1193 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { prepareModelInboxInvocation, isModelInboxTool } from "../runtime/core/hellolynk-model-inbox.mjs";
|
|
3
|
-
import { inspectCodexExecutorMetadata } from "./native-task-client.mjs";
|
|
4
|
-
import { nativeTool, optionalTarget, nativeAddressingError, replyHint, conversationGuidance, firstSendReceipt, paramText, resultText, sendArgumentKeys, sendRecipient, findContacts, writeFailure, failureText } from "../runtime/core/hellolynk-native-addressing-tools.cjs";
|
|
5
|
-
import { withHelloLynkModelNotices, captureHelloLynkNativeDeliveryStatus } from "../runtime/core/hellolynk-model-notices.cjs";
|
|
6
|
-
|
|
7
|
-
import readline from "node:readline";
|
|
8
|
-
import { projectInboxReadState } from "../runtime/core/hellolynk-inbox-read-state.mjs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { CODEX_WORKBENCH_RESOURCE_META } from "./workbench-runtime.mjs";
|
|
12
|
-
import packageMetadata from "../package.json" with { type: "json" };
|
|
13
|
-
|
|
14
|
-
if (packageMetadata.name !== "@hellolynk-ai/codex-plugin"
|
|
15
|
-
|| typeof packageMetadata.version !== "string" || !/^\d+\.\d+\.\d+$/.test(packageMetadata.version)) {
|
|
16
|
-
throw codeError("codex_package_metadata_invalid");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const WRITE_ID = Object.freeze({ type: "string", minLength: 3, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$" });
|
|
20
|
-
const WRITE_ID_PATTERN = new RegExp(WRITE_ID.pattern);
|
|
21
|
-
const CLIENT_MESSAGE_ID = Object.freeze({ ...WRITE_ID, examples: ["msg-a1b2c3-01"], description: paramText.clientMessageId });
|
|
22
|
-
const MESSAGE_TEXT = Object.freeze({ type: "string", minLength: 1, maxLength: 20000, description: paramText.text });
|
|
23
|
-
|
|
24
|
-
const PROTOCOL_VERSION = "2025-06-18";
|
|
25
|
-
const VERSIONED_WORKBENCH_URI_RE = /^ui:\/\/hellolynk\/workbench-codex-[a-f0-9]{64}\.html$/;
|
|
26
|
-
|
|
27
|
-
const TOOL_ANNOTATIONS = Object.freeze({
|
|
28
|
-
readOnlyHint: true,
|
|
29
|
-
openWorldHint: false,
|
|
30
|
-
destructiveHint: false,
|
|
31
|
-
idempotentHint: true,
|
|
32
|
-
});
|
|
33
|
-
const LOCAL_WRITE_ANNOTATIONS = Object.freeze({
|
|
34
|
-
readOnlyHint: false,
|
|
35
|
-
openWorldHint: false,
|
|
36
|
-
destructiveHint: false,
|
|
37
|
-
idempotentHint: true,
|
|
38
|
-
});
|
|
39
|
-
const WORKBENCH_COMMAND_ANNOTATIONS = Object.freeze({
|
|
40
|
-
readOnlyHint: false,
|
|
41
|
-
openWorldHint: true,
|
|
42
|
-
destructiveHint: true,
|
|
43
|
-
idempotentHint: false,
|
|
44
|
-
});
|
|
45
|
-
const SEND_ANNOTATIONS = Object.freeze({
|
|
46
|
-
readOnlyHint: false,
|
|
47
|
-
openWorldHint: true,
|
|
48
|
-
destructiveHint: false,
|
|
49
|
-
idempotentHint: false,
|
|
50
|
-
});
|
|
51
|
-
const WORKBENCH_CONNECT_TOOL = "hellolynk_workbench_connect";
|
|
52
|
-
const WORKBENCH_SNAPSHOT_TOOL = "hellolynk_workbench_snapshot";
|
|
53
|
-
const WORKBENCH_CHANGES_TOOL = "hellolynk_workbench_changes";
|
|
54
|
-
const WORKBENCH_ACTION_GRANT_TOOL = "hellolynk_workbench_action_grant";
|
|
55
|
-
const WORKBENCH_COMMAND_TOOL = "hellolynk_workbench_command";
|
|
56
|
-
const OPEN_HELLOLYNK_TOOL = "hellolynk_open";
|
|
57
|
-
|
|
58
|
-
// nativeTool adds the titles and descriptions every client shares.
|
|
59
|
-
const neutralTools = [
|
|
60
|
-
{
|
|
61
|
-
name: OPEN_HELLOLYNK_TOOL,
|
|
62
|
-
title: "打开 HelloLynk",
|
|
63
|
-
description: "在聊天里显示 HelloLynk 卡片。",
|
|
64
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
65
|
-
annotations: TOOL_ANNOTATIONS,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: "hellolynk_get_unread_count",
|
|
69
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
70
|
-
annotations: TOOL_ANNOTATIONS,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: "hellolynk_list_authorized_contacts",
|
|
74
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
75
|
-
annotations: TOOL_ANNOTATIONS,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
name: "hellolynk_send_authorized_message",
|
|
79
|
-
inputSchema: {
|
|
80
|
-
type: "object",
|
|
81
|
-
additionalProperties: false,
|
|
82
|
-
required: ["clientMessageId", "text"],
|
|
83
|
-
properties: {
|
|
84
|
-
contactId: WRITE_ID,
|
|
85
|
-
recipientDelegateId: WRITE_ID,
|
|
86
|
-
clientMessageId: CLIENT_MESSAGE_ID,
|
|
87
|
-
text: MESSAGE_TEXT,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
annotations: SEND_ANNOTATIONS,
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: "hellolynk_list_authorized_inbox_messages",
|
|
94
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
95
|
-
before: { type: "string", minLength: 3, maxLength: 198, description: paramText.before },
|
|
96
|
-
} },
|
|
97
|
-
annotations: TOOL_ANNOTATIONS,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: "hellolynk_read_authorized_inbox_message",
|
|
101
|
-
inputSchema: strictConversationSchema("localMessageId"), annotations: TOOL_ANNOTATIONS,
|
|
102
|
-
},
|
|
103
|
-
{ name: "hellolynk_list_pending_messages",
|
|
104
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {} }, annotations: LOCAL_WRITE_ANNOTATIONS },
|
|
105
|
-
{ name: "hellolynk_claim_message",
|
|
106
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
107
|
-
batchId: { type: "string", minLength: 3, maxLength: 128, description: paramText.batchId },
|
|
108
|
-
localMessageId: { type: "string", minLength: 3, maxLength: 240, description: paramText.localMessageId },
|
|
109
|
-
conversationKey: { type: "string", minLength: 3, maxLength: 240, description: paramText.conversationKey },
|
|
110
|
-
}, oneOf: [{ required: ["batchId"] }, { required: ["localMessageId", "conversationKey"] }] }, annotations: LOCAL_WRITE_ANNOTATIONS },
|
|
111
|
-
{ name: "hellolynk_reply_authorized_message",
|
|
112
|
-
inputSchema: { type: "object", additionalProperties: false, required: ["localMessageId", "conversationKey", "clientMessageId", "text"], properties: {
|
|
113
|
-
localMessageId: { ...WRITE_ID, description: paramText.localMessageId },
|
|
114
|
-
conversationKey: { ...WRITE_ID, description: paramText.conversationKey },
|
|
115
|
-
clientMessageId: CLIENT_MESSAGE_ID, text: MESSAGE_TEXT,
|
|
116
|
-
} }, annotations: SEND_ANNOTATIONS },
|
|
117
|
-
{
|
|
118
|
-
name: "hellolynk_list_authorized_contact_requests",
|
|
119
|
-
inputSchema: { type: "object", additionalProperties: false, properties: {} }, annotations: TOOL_ANNOTATIONS,
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: "hellolynk_read_authorized_contact_request",
|
|
123
|
-
inputSchema: strictConversationSchema("localRecordId"), annotations: TOOL_ANNOTATIONS,
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
].map(nativeTool);
|
|
127
|
-
|
|
128
|
-
export async function runStdioMcpServer(options = {}) {
|
|
129
|
-
const neutralAdapter = options.neutralAdapter || null;
|
|
130
|
-
const input = options.input || process.stdin;
|
|
131
|
-
const output = options.output || process.stdout;
|
|
132
|
-
const rl = readline.createInterface({
|
|
133
|
-
input,
|
|
134
|
-
crlfDelay: Number.POSITIVE_INFINITY,
|
|
135
|
-
});
|
|
136
|
-
const stopMcp = () => {
|
|
137
|
-
rl.close();
|
|
138
|
-
};
|
|
139
|
-
process.once("SIGINT", stopMcp);
|
|
140
|
-
process.once("SIGTERM", stopMcp);
|
|
141
|
-
let commands = Promise.resolve();
|
|
142
|
-
let appReads = Promise.resolve();
|
|
143
|
-
let failure;
|
|
144
|
-
let protocolWarmupStarted = false;
|
|
145
|
-
const serve = async (line) => {
|
|
146
|
-
const { message: handled, response } = await handleLine(line, output, {
|
|
147
|
-
neutralAdapter,
|
|
148
|
-
prepareAuthorizedWrite: options.prepareAuthorizedWrite,
|
|
149
|
-
readNativeDeliveryStatus: options.readNativeDeliveryStatus,
|
|
150
|
-
...(options.workbenchCandidate === undefined ? {} : { workbenchCandidate: options.workbenchCandidate }),
|
|
151
|
-
}) || {};
|
|
152
|
-
if (!protocolWarmupStarted && response?.result
|
|
153
|
-
&& ["initialize", "tools/list"].includes(handled?.method)
|
|
154
|
-
&& typeof options.onProtocolReady === "function") {
|
|
155
|
-
// Codex may cache tools/list entirely. Start once after a successful
|
|
156
|
-
// handshake (or catalog response), never on an invalid request and
|
|
157
|
-
// never in the response's critical path. Real calls can retry failure.
|
|
158
|
-
protocolWarmupStarted = true;
|
|
159
|
-
Promise.resolve()
|
|
160
|
-
.then(() => options.onProtocolReady())
|
|
161
|
-
.catch(() => {});
|
|
162
|
-
}
|
|
163
|
-
if (handled?.method === "tools/call"
|
|
164
|
-
&& !(WORKBENCH_READ_TOOLS.has(handled.params?.name) && handled.params?.name !== OPEN_HELLOLYNK_TOOL)
|
|
165
|
-
&& typeof options.onExecutorToolCall === "function") {
|
|
166
|
-
// Only the executor-owned MCP request envelope can identify the
|
|
167
|
-
// current Codex task. App-only polling is not a model turn and must
|
|
168
|
-
// not retarget its receiver while a different model turn is sending.
|
|
169
|
-
// The model-visible open tool still registers its executor context.
|
|
170
|
-
// External tool arguments are never inspected.
|
|
171
|
-
Promise.resolve()
|
|
172
|
-
.then(() => options.onExecutorToolCall(handled.params?._meta))
|
|
173
|
-
.catch(() => {});
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
try {
|
|
177
|
-
for await (const line of rl) {
|
|
178
|
-
if (!line.trim()) continue;
|
|
179
|
-
let message;
|
|
180
|
-
try { message = JSON.parse(line); } catch { /* handleLine reports parse_error */ }
|
|
181
|
-
const method = message?.method;
|
|
182
|
-
// HTML and protocol responses do not wait for Host startup. They remain
|
|
183
|
-
// available while a Host operation is waiting, so the UI can display its
|
|
184
|
-
// own bounded wait/error state. Business calls retain their original order;
|
|
185
|
-
// an unknown write result must never cause replay or concurrent execution.
|
|
186
|
-
if (method === undefined || STATIC_MCP_METHODS.has(method) || method?.startsWith?.("notifications/")) {
|
|
187
|
-
await serve(line);
|
|
188
|
-
} else if (method === "tools/call" && WORKBENCH_READ_TOOLS.has(message.params?.name)) {
|
|
189
|
-
// The card's read-only account/snapshot/change queries need not wait
|
|
190
|
-
// for a model send or verification. Keep reads ordered in their own
|
|
191
|
-
// lane; dispatch still checks account revision before and after reads,
|
|
192
|
-
// and Host snapshots retain their authorization/coherence checks.
|
|
193
|
-
appReads = appReads.then(() => failure ? undefined : serve(line)).catch((error) => {
|
|
194
|
-
failure = error;
|
|
195
|
-
stopMcp();
|
|
196
|
-
});
|
|
197
|
-
} else {
|
|
198
|
-
commands = commands.then(() => failure ? undefined : serve(line)).catch((error) => {
|
|
199
|
-
failure = error;
|
|
200
|
-
stopMcp();
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
await Promise.all([commands, appReads]);
|
|
205
|
-
if (failure) throw failure;
|
|
206
|
-
} finally {
|
|
207
|
-
process.removeListener("SIGINT", stopMcp);
|
|
208
|
-
process.removeListener("SIGTERM", stopMcp);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const STATIC_MCP_METHODS = new Set([
|
|
213
|
-
"initialize", "ping", "tools/list", "resources/list", "resources/templates/list", "resources/read",
|
|
214
|
-
]);
|
|
215
|
-
const WORKBENCH_READ_TOOLS = new Set([
|
|
216
|
-
OPEN_HELLOLYNK_TOOL,
|
|
217
|
-
"hellolynk_workbench_account", WORKBENCH_SNAPSHOT_TOOL, WORKBENCH_CHANGES_TOOL,
|
|
218
|
-
]);
|
|
219
|
-
|
|
220
|
-
async function handleLine(line, output = process.stdout, runtime = {}) {
|
|
221
|
-
let message;
|
|
222
|
-
try {
|
|
223
|
-
message = JSON.parse(line);
|
|
224
|
-
} catch {
|
|
225
|
-
write(jsonRpcError(null, -32700, "parse_error"), output);
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
const response = await handleJsonRpcMessage(message, runtime);
|
|
229
|
-
if (response) write(response, output);
|
|
230
|
-
return { message, response };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export async function handleJsonRpcMessage(message, runtime = {}) {
|
|
234
|
-
if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
235
|
-
return jsonRpcError(message?.id ?? null, -32600, "invalid_request");
|
|
236
|
-
}
|
|
237
|
-
if (message.method.startsWith("notifications/")) return null;
|
|
238
|
-
try {
|
|
239
|
-
const result = await dispatch(message.method, message.params || {}, { ...runtime,
|
|
240
|
-
executorMetadata: message.params?._meta });
|
|
241
|
-
return message.id === undefined ? null : { jsonrpc: "2.0", id: message.id, result };
|
|
242
|
-
} catch (error) {
|
|
243
|
-
return message.id === undefined
|
|
244
|
-
? null
|
|
245
|
-
: jsonRpcError(message.id, -32603, safeErrorCode(error));
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export async function dispatch(method, params, runtime = {}) {
|
|
250
|
-
const run = async () => {
|
|
251
|
-
const result = await dispatchWithoutModelNotices(method, params, runtime);
|
|
252
|
-
if (method === "tools/call" && !isOpenHelloLynkToolName(params.name)
|
|
253
|
-
&& neutralTools.some(tool => tool.name === params.name)) {
|
|
254
|
-
// Do not delay UI refreshes or change a send result if diagnostics fail.
|
|
255
|
-
try { captureHelloLynkNativeDeliveryStatus(await runtime.readNativeDeliveryStatus?.()); } catch {}
|
|
256
|
-
}
|
|
257
|
-
return result;
|
|
258
|
-
};
|
|
259
|
-
return method === "tools/call" ? withHelloLynkModelNotices(run) : run();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async function dispatchWithoutModelNotices(method, params, runtime = {}) {
|
|
263
|
-
const workbenchCandidate = runtimeWorkbenchCandidate(runtime);
|
|
264
|
-
const workbenchUnavailable = !workbenchCandidate;
|
|
265
|
-
if (method === "initialize") {
|
|
266
|
-
return {
|
|
267
|
-
// This server implements one protocol version; unknown offers receive
|
|
268
|
-
// that supported counteroffer instead of an arbitrary string echo.
|
|
269
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
270
|
-
capabilities: {
|
|
271
|
-
tools: { listChanged: false },
|
|
272
|
-
resources: { subscribe: false, listChanged: false },
|
|
273
|
-
},
|
|
274
|
-
serverInfo: {
|
|
275
|
-
name: "hellolynk-codex-inbox",
|
|
276
|
-
title: "HelloLynk 通讯",
|
|
277
|
-
version: packageMetadata.version,
|
|
278
|
-
},
|
|
279
|
-
instructions: conversationGuidance + "“打开 HelloLynk”时请用户点 Codex 左侧栏的 HelloLynk;要在聊天里显示卡片时调用 hellolynk_open。",
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
if (method === "ping") return {};
|
|
283
|
-
if (method === "tools/list") {
|
|
284
|
-
const tools = neutralTools.map((tool) => isOpenHelloLynkToolName(tool.name)
|
|
285
|
-
? workbenchCandidate ? workbenchOpenTool(tool, workbenchCandidate) : unavailableWorkbenchOpenTool(tool)
|
|
286
|
-
: tool);
|
|
287
|
-
return {
|
|
288
|
-
tools: workbenchCandidate
|
|
289
|
-
? [
|
|
290
|
-
...tools,
|
|
291
|
-
workbenchCandidateTool(workbenchCandidate),
|
|
292
|
-
...(typeof workbenchCandidate.readChanges === "function" ? [workbenchChangesToolDefinition(workbenchCandidate)] : []),
|
|
293
|
-
...(typeof workbenchCandidate.connectAccount === "function" ? [workbenchConnectTool()] : []),
|
|
294
|
-
...(workbenchCandidate.connectAccount?.readAccountState ? [workbenchAccountTool(false), workbenchAccountTool(true)] : []),
|
|
295
|
-
...(typeof workbenchCandidate.executeCommand === "function"
|
|
296
|
-
? [workbenchActionGrantTool(workbenchCandidate), workbenchCommandTool(workbenchCandidate)]
|
|
297
|
-
: []),
|
|
298
|
-
].map((tool) => withAccountRevision(tool, workbenchCandidate))
|
|
299
|
-
: tools,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
if (method === "tools/call") return callTool(params, runtime);
|
|
303
|
-
if (method === "resources/list") {
|
|
304
|
-
return {
|
|
305
|
-
resources: workbenchCandidate ? [{
|
|
306
|
-
uri: workbenchCandidate.resourceUri,
|
|
307
|
-
name: "HelloLynk 通讯界面",
|
|
308
|
-
title: "HelloLynk 通讯",
|
|
309
|
-
description: typeof workbenchCandidate.executeCommand === "function"
|
|
310
|
-
? "阶段 F MCP App;只显示本机 HelloLynk 报告的真实能力,缺失能力由界面隐藏或由本机 HelloLynk 拒绝。"
|
|
311
|
-
: "阶段 F 共享 HelloLynk 通讯只读 MCP App。",
|
|
312
|
-
mimeType: "text/html;profile=mcp-app",
|
|
313
|
-
}] : [],
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
if (method === "resources/templates/list") return { resourceTemplates: [] };
|
|
317
|
-
if (method === "resources/read") {
|
|
318
|
-
if (workbenchCandidate) {
|
|
319
|
-
// Codex may retain a previous invocation's URI after a plugin upgrade.
|
|
320
|
-
// Keep advertising the current content hash for cache refresh, but serve
|
|
321
|
-
// the installed UI for this same entry's older hashes. No old UI is read.
|
|
322
|
-
const retainedWorkbenchEntry = typeof params.uri === "string"
|
|
323
|
-
&& VERSIONED_WORKBENCH_URI_RE.test(params.uri)
|
|
324
|
-
&& VERSIONED_WORKBENCH_URI_RE.test(workbenchCandidate.resourceUri);
|
|
325
|
-
if (params.uri !== workbenchCandidate.resourceUri && !retainedWorkbenchEntry) throw codeError("resource_not_found");
|
|
326
|
-
return {
|
|
327
|
-
contents: [{
|
|
328
|
-
uri: params.uri,
|
|
329
|
-
mimeType: "text/html;profile=mcp-app",
|
|
330
|
-
text: workbenchCandidate.html,
|
|
331
|
-
_meta: CODEX_WORKBENCH_RESOURCE_META,
|
|
332
|
-
}],
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
throw codeError("resource_not_found");
|
|
336
|
-
}
|
|
337
|
-
throw codeError("method_not_found");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
async function callTool(params, runtime = {}) {
|
|
341
|
-
if (!runtime.neutralAdapter || !isModelInboxTool(params?.name)) return callToolWithoutInboxScope(params, runtime);
|
|
342
|
-
try {
|
|
343
|
-
const source = inspectCodexExecutorMetadata(runtime.executorMetadata);
|
|
344
|
-
const invocation = await prepareModelInboxInvocation(runtime.neutralAdapter, {
|
|
345
|
-
name: params.name, args: params.arguments || {},
|
|
346
|
-
caller: !source.conflict && source.threadId ? { threadId: source.threadId } : undefined,
|
|
347
|
-
});
|
|
348
|
-
return invocation.finish(await callToolWithoutInboxScope({ ...params, arguments: invocation.args },
|
|
349
|
-
{ ...runtime, neutralAdapter: invocation.adapter }));
|
|
350
|
-
} catch (error) {
|
|
351
|
-
if (params.name === 'hellolynk_reply_authorized_message') return authorizedWriteToolError(error, undefined,
|
|
352
|
-
'authorized_reply_result_unknown', error?.code === 'model_inbox_scope_invalid' ? 'input_validation' : 'preparation');
|
|
353
|
-
return nativeAddressingError(error) || toolError(safeErrorCode(error));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function callToolWithoutInboxScope(params, runtime = {}) {
|
|
358
|
-
const name = String(params?.name || "");
|
|
359
|
-
const workbenchCandidate = runtimeWorkbenchCandidate(runtime);
|
|
360
|
-
const workbenchUnavailable = !workbenchCandidate;
|
|
361
|
-
const accounts = workbenchCandidate?.connectAccount;
|
|
362
|
-
const surfaceError = name === WORKBENCH_CHANGES_TOOL ? workbenchChangesError : workbenchSurfaceToolError;
|
|
363
|
-
if (accounts?.readAccountState && ["hellolynk_workbench_account", "hellolynk_workbench_disconnect"].includes(name)) {
|
|
364
|
-
try {
|
|
365
|
-
const disconnect = name === "hellolynk_workbench_disconnect";
|
|
366
|
-
assertExactArgumentKeys(params?.arguments, disconnect ? ["revision"] : []);
|
|
367
|
-
const result = disconnect ? await accounts.disconnectAccount(params.arguments) : accounts.readAccountState();
|
|
368
|
-
return { content: [], structuredContent: { ok: true }, _meta: { hellolynkWorkbench: { [disconnect ? "disconnection" : "account"]: result } }, isError: false };
|
|
369
|
-
} catch (error) { return workbenchSurfaceToolError(error?.code || "workbench_account_unavailable", "账号操作"); }
|
|
370
|
-
}
|
|
371
|
-
const revision = params?.arguments?._hellolynkAccountRevision;
|
|
372
|
-
if (accounts?.readAccountState && [WORKBENCH_CONNECT_TOOL, WORKBENCH_SNAPSHOT_TOOL, WORKBENCH_CHANGES_TOOL, WORKBENCH_ACTION_GRANT_TOOL, WORKBENCH_COMMAND_TOOL].includes(name)) {
|
|
373
|
-
try { accounts.assertRevision(revision); }
|
|
374
|
-
catch (error) { return surfaceError(error.code, "账号检查"); }
|
|
375
|
-
const args = { ...params.arguments }; delete args._hellolynkAccountRevision;
|
|
376
|
-
params = { ...params, arguments: args };
|
|
377
|
-
}
|
|
378
|
-
const accountOperation = async (operation, mutation = false) => {
|
|
379
|
-
try {
|
|
380
|
-
if (!accounts?.readAccountState) return await operation();
|
|
381
|
-
if (mutation) return await accounts.runForAccount(revision, operation);
|
|
382
|
-
accounts.assertRevision(revision);
|
|
383
|
-
const result = await operation();
|
|
384
|
-
accounts.assertRevision(revision);
|
|
385
|
-
return result;
|
|
386
|
-
} catch (error) { return surfaceError(error?.code || "workbench_account_unavailable", "账号检查"); }
|
|
387
|
-
};
|
|
388
|
-
if (workbenchUnavailable && isOpenHelloLynkToolName(name)) {
|
|
389
|
-
return workbenchSurfaceToolError("workbench_unavailable", "启动");
|
|
390
|
-
}
|
|
391
|
-
if (workbenchCandidate && isOpenHelloLynkToolName(name)) {
|
|
392
|
-
return workbenchOpenToolCall(params?.arguments);
|
|
393
|
-
}
|
|
394
|
-
if (workbenchCandidate && name === WORKBENCH_CONNECT_TOOL && typeof workbenchCandidate.connectAccount === "function") {
|
|
395
|
-
try {
|
|
396
|
-
assertExactArgumentKeys(params?.arguments, ["hellolynkId", "instanceName"]);
|
|
397
|
-
const connection = await workbenchCandidate.connectAccount(params.arguments, revision);
|
|
398
|
-
return { content: [], structuredContent: { ok: true }, _meta: { hellolynkWorkbench: { connection } }, isError: false };
|
|
399
|
-
} catch (error) { return workbenchSurfaceToolError(error?.code || "workbench_connect_failed", "连接"); }
|
|
400
|
-
}
|
|
401
|
-
if (workbenchCandidate && name === WORKBENCH_SNAPSHOT_TOOL) {
|
|
402
|
-
return accountOperation(() => workbenchSnapshotTool(params?.arguments, workbenchCandidate));
|
|
403
|
-
}
|
|
404
|
-
if (name === WORKBENCH_CHANGES_TOOL) {
|
|
405
|
-
if (typeof workbenchCandidate?.readChanges !== "function") return workbenchChangesError("workbench_changes_unavailable");
|
|
406
|
-
return accountOperation(() => workbenchChangesToolCall(params?.arguments, workbenchCandidate));
|
|
407
|
-
}
|
|
408
|
-
if (workbenchCandidate && name === WORKBENCH_ACTION_GRANT_TOOL
|
|
409
|
-
&& typeof workbenchCandidate.issueActionGrant === "function") {
|
|
410
|
-
return accountOperation(() => workbenchActionGrantToolCall(params?.arguments, workbenchCandidate), true);
|
|
411
|
-
}
|
|
412
|
-
if (workbenchCandidate && name === WORKBENCH_COMMAND_TOOL
|
|
413
|
-
&& typeof workbenchCandidate.executeCommand === "function") {
|
|
414
|
-
return accountOperation(() => workbenchCommandToolCall(params?.arguments, workbenchCandidate), true);
|
|
415
|
-
}
|
|
416
|
-
if (name === WORKBENCH_ACTION_GRANT_TOOL || name === WORKBENCH_COMMAND_TOOL) {
|
|
417
|
-
return workbenchSurfaceToolError("workbench_action_grant_unavailable", "通讯操作");
|
|
418
|
-
}
|
|
419
|
-
const neutral = runtime.neutralAdapter;
|
|
420
|
-
if (neutral) {
|
|
421
|
-
if (name === "hellolynk_list_authorized_contacts") return neutralListAuthorizedContactsTool(params?.arguments, neutral);
|
|
422
|
-
if (name === "hellolynk_send_authorized_message") return neutralSendAuthorizedMessageTool(params?.arguments, neutral, runtime);
|
|
423
|
-
if (name === "hellolynk_list_authorized_inbox_messages") return neutralListAuthorizedInboxMessagesTool(params?.arguments, neutral);
|
|
424
|
-
if (name === "hellolynk_read_authorized_inbox_message") return neutralReadAuthorizedInboxMessageTool(params?.arguments, neutral);
|
|
425
|
-
if (name === "hellolynk_list_pending_messages") return neutralListPendingMessagesTool(params?.arguments, neutral);
|
|
426
|
-
if (name === "hellolynk_claim_message") return neutralClaimMessageTool(params?.arguments, neutral);
|
|
427
|
-
if (name === "hellolynk_reply_authorized_message") return neutralReplyAuthorizedMessageTool(params?.arguments, neutral, runtime);
|
|
428
|
-
if (name === "hellolynk_list_authorized_contact_requests") return neutralListAuthorizedContactRequestsTool(params?.arguments, neutral);
|
|
429
|
-
if (name === "hellolynk_read_authorized_contact_request") return neutralReadAuthorizedContactRequestTool(params?.arguments, neutral);
|
|
430
|
-
if (![OPEN_HELLOLYNK_TOOL, "hellolynk_get_unread_count"].includes(name)) return toolError("unknown_tool");
|
|
431
|
-
}
|
|
432
|
-
if (![OPEN_HELLOLYNK_TOOL, "hellolynk_get_unread_count"].includes(name)) return toolError("unknown_tool");
|
|
433
|
-
if (!plainEmptyObject(params?.arguments)) return toolError("arguments_not_allowed");
|
|
434
|
-
try {
|
|
435
|
-
if (name === "hellolynk_get_unread_count") return neutralGetUnreadCountTool(neutral);
|
|
436
|
-
if (!neutral) return toolError("neutral_adapter_unavailable");
|
|
437
|
-
return workbenchUnavailable
|
|
438
|
-
? workbenchSurfaceToolError("workbench_unavailable", "启动")
|
|
439
|
-
: workbenchOpenToolCall(params?.arguments);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
return toolError(safeErrorCode(error));
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function runtimeWorkbenchCandidate(runtime) {
|
|
446
|
-
const value = runtime?.workbenchCandidate;
|
|
447
|
-
if (value === undefined) return null;
|
|
448
|
-
const keys = value && typeof value === "object" && !Array.isArray(value)
|
|
449
|
-
? Object.keys(value).filter((key) => !["connectAccount", "readChanges"].includes(key)).sort().join(",")
|
|
450
|
-
: "";
|
|
451
|
-
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
452
|
-
|| ![
|
|
453
|
-
"html,readSnapshot,resourceUri,validateReadRequest,validateSnapshot",
|
|
454
|
-
"commandContractId,commandSchemaVersion,commandTypes,executeCommand,html,issueActionGrant,readSnapshot,resourceUri,resultContractId,resultSchemaVersion,snapshotContractId,snapshotSchemaVersion,validateCommand,validateCommandResult,validateReadRequest,validateSnapshot",
|
|
455
|
-
].includes(keys)
|
|
456
|
-
|| typeof value.html !== "string" || value.html.length < 500 || value.html.length > 2_000_000
|
|
457
|
-
|| (Object.hasOwn(value, "connectAccount") && typeof value.connectAccount !== "function")
|
|
458
|
-
|| (Object.hasOwn(value, "readChanges") && typeof value.readChanges !== "function")
|
|
459
|
-
|| typeof value.readSnapshot !== "function"
|
|
460
|
-
|| typeof value.validateReadRequest !== "function"
|
|
461
|
-
|| typeof value.validateSnapshot !== "function"
|
|
462
|
-
|| (Object.hasOwn(value, "executeCommand") && typeof value.executeCommand !== "function")
|
|
463
|
-
|| (Object.hasOwn(value, "executeCommand") && typeof value.issueActionGrant !== "function")
|
|
464
|
-
|| (Object.hasOwn(value, "executeCommand") && value.executeCommand.length < 2)
|
|
465
|
-
|| (Object.hasOwn(value, "executeCommand") && value.issueActionGrant.length < 1)
|
|
466
|
-
|| (Object.hasOwn(value, "executeCommand") && typeof value.validateCommand !== "function")
|
|
467
|
-
|| (Object.hasOwn(value, "executeCommand") && typeof value.validateCommandResult !== "function")
|
|
468
|
-
|| (Object.hasOwn(value, "executeCommand")
|
|
469
|
-
&& (value.snapshotContractId !== "hellolynk-workbench-snapshot-v12"
|
|
470
|
-
|| value.snapshotSchemaVersion !== 12
|
|
471
|
-
|| value.commandContractId !== "hellolynk-workbench-command-v6"
|
|
472
|
-
|| value.commandSchemaVersion !== 6
|
|
473
|
-
|| value.resultContractId !== "hellolynk-workbench-command-result-v6"
|
|
474
|
-
|| value.resultSchemaVersion !== 6))
|
|
475
|
-
|| (Object.hasOwn(value, "executeCommand") && !validWorkbenchCommandTypes(value.commandTypes))
|
|
476
|
-
|| !/^ui:\/\/hellolynk\/[a-z0-9][a-z0-9._/-]{1,120}\.html$/.test(String(value.resourceUri || ""))) {
|
|
477
|
-
throw codeError("workbench_candidate_runtime_invalid");
|
|
478
|
-
}
|
|
479
|
-
return value;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function validWorkbenchCommandTypes(value) {
|
|
483
|
-
return Array.isArray(value) && value.length > 0 && value.length <= 100
|
|
484
|
-
&& new Set(value).size === value.length
|
|
485
|
-
&& value.every((item) => typeof item === "string"
|
|
486
|
-
&& /^[a-z][a-z0-9.-]{2,80}$/.test(item));
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function workbenchOpenTool(baseTool, candidate) {
|
|
490
|
-
return {
|
|
491
|
-
...baseTool,
|
|
492
|
-
_meta: {
|
|
493
|
-
...baseTool._meta,
|
|
494
|
-
ui: {
|
|
495
|
-
...(baseTool._meta?.ui || {}),
|
|
496
|
-
visibility: ["model", "app"],
|
|
497
|
-
resourceUri: candidate.resourceUri,
|
|
498
|
-
},
|
|
499
|
-
"openai/ui": { entrypoints: [{ type: "global" }] },
|
|
500
|
-
"openai/outputTemplate": candidate.resourceUri,
|
|
501
|
-
"openai/toolInvocation/invoking": "正在打开 HelloLynk 通讯",
|
|
502
|
-
"openai/toolInvocation/invoked": "HelloLynk 通讯已打开",
|
|
503
|
-
},
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function workbenchOpenToolCall(args) {
|
|
508
|
-
if (!plainEmptyObject(args)) return workbenchSurfaceToolError("arguments_not_allowed", "通讯启动");
|
|
509
|
-
return {
|
|
510
|
-
content: [{
|
|
511
|
-
type: "text",
|
|
512
|
-
text: "HelloLynk 卡片已就绪。",
|
|
513
|
-
}],
|
|
514
|
-
structuredContent: { ok: true, status: "ready" },
|
|
515
|
-
isError: false,
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function unavailableWorkbenchOpenTool(baseTool) {
|
|
520
|
-
const {
|
|
521
|
-
"openai/outputTemplate": _legacyTemplate,
|
|
522
|
-
"openai/toolInvocation/invoking": _legacyInvoking,
|
|
523
|
-
"openai/toolInvocation/invoked": _legacyInvoked,
|
|
524
|
-
...remainingMeta
|
|
525
|
-
} = baseTool._meta || {};
|
|
526
|
-
const safeMeta = {
|
|
527
|
-
...remainingMeta,
|
|
528
|
-
ui: { visibility: ["model", "app"] },
|
|
529
|
-
};
|
|
530
|
-
return { ...baseTool, _meta: safeMeta };
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function isOpenHelloLynkToolName(name) {
|
|
534
|
-
return name === OPEN_HELLOLYNK_TOOL;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function workbenchSurfaceToolError(code, operation) {
|
|
538
|
-
const safe = safeErrorCode({ code });
|
|
539
|
-
return {
|
|
540
|
-
content: [{ type: "text", text: safe === "neutral_host_release_superseded" ? failureText(safe) : `HelloLynk${operation}失败:${safe}。` }],
|
|
541
|
-
structuredContent: {
|
|
542
|
-
ok: false,
|
|
543
|
-
status: "blocked",
|
|
544
|
-
errorCode: safe,
|
|
545
|
-
},
|
|
546
|
-
isError: true,
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function workbenchConnectTool() {
|
|
551
|
-
return {
|
|
552
|
-
name: WORKBENCH_CONNECT_TOOL, title: "连接 HelloLynk",
|
|
553
|
-
description: "由 HelloLynk 聊天卡片发起首次连接;返回官网确认入口。确认账号设备后才建立连接。",
|
|
554
|
-
inputSchema: { type: "object", additionalProperties: false, required: ["hellolynkId", "instanceName"], properties: {
|
|
555
|
-
hellolynkId: { type: "string", pattern: "^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$" },
|
|
556
|
-
instanceName: { type: "string", maxLength: 80 },
|
|
557
|
-
} },
|
|
558
|
-
annotations: { readOnlyHint: false, openWorldHint: true, destructiveHint: false, idempotentHint: true },
|
|
559
|
-
_meta: { ui: { visibility: ["app"] } },
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function workbenchAccountTool(disconnect) {
|
|
564
|
-
return {
|
|
565
|
-
name: disconnect ? "hellolynk_workbench_disconnect" : "hellolynk_workbench_account",
|
|
566
|
-
title: disconnect ? "解除设备连接" : "读取当前客户端连接状态",
|
|
567
|
-
description: "仅供 HelloLynk 聊天卡片使用。解除只影响当前客户端,保留旧账号资料。",
|
|
568
|
-
inputSchema: { type: "object", additionalProperties: false, required: disconnect ? ["revision"] : [],
|
|
569
|
-
properties: disconnect ? { revision: { type: "string", pattern: "^(initial|[a-f0-9]{32})$" } } : {} },
|
|
570
|
-
annotations: { readOnlyHint: !disconnect, openWorldHint: false, destructiveHint: disconnect, idempotentHint: true },
|
|
571
|
-
_meta: { ui: { visibility: ["app"] } },
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function withAccountRevision(tool, candidate) {
|
|
576
|
-
if (!candidate.connectAccount?.readAccountState
|
|
577
|
-
|| ![WORKBENCH_CONNECT_TOOL, WORKBENCH_SNAPSHOT_TOOL, WORKBENCH_CHANGES_TOOL, WORKBENCH_ACTION_GRANT_TOOL, WORKBENCH_COMMAND_TOOL].includes(tool.name)) return tool;
|
|
578
|
-
return { ...tool, inputSchema: { ...tool.inputSchema,
|
|
579
|
-
required: [...(tool.inputSchema.required || []), "_hellolynkAccountRevision"],
|
|
580
|
-
properties: { ...tool.inputSchema.properties, _hellolynkAccountRevision: { type: "string", pattern: "^(initial|[a-f0-9]{32})$" } } } };
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function workbenchChangesToolDefinition(candidate) {
|
|
584
|
-
return {
|
|
585
|
-
name: WORKBENCH_CHANGES_TOOL,
|
|
586
|
-
title: "读取HelloLynk界面变化游标",
|
|
587
|
-
description: "仅供 HelloLynk MCP App 静默检查本机变化;不返回正文或事件身份,不触发模型轮次。",
|
|
588
|
-
inputSchema: { type: "object", additionalProperties: false, required: ["afterCursor"],
|
|
589
|
-
properties: { afterCursor: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER } } },
|
|
590
|
-
annotations: TOOL_ANNOTATIONS,
|
|
591
|
-
_meta: { ui: { resourceUri: candidate.resourceUri, visibility: ["app"] } },
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function workbenchChangesError(code) {
|
|
596
|
-
return { content: [], structuredContent: { ok: true },
|
|
597
|
-
_meta: { hellolynkWorkbench: { changeError: { code: safeErrorCode({ code }) } } }, isError: true };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
async function workbenchChangesToolCall(args, candidate) {
|
|
601
|
-
try {
|
|
602
|
-
assertExactArgumentKeys(args, ["afterCursor"]);
|
|
603
|
-
if (!Number.isSafeInteger(args?.afterCursor) || args.afterCursor < 0) throw codeError("host_app_change_cursor_invalid");
|
|
604
|
-
const batch = await candidate.readChanges({ afterCursor: args.afterCursor });
|
|
605
|
-
if (!batch || !Number.isSafeInteger(batch.cursor) || batch.cursor < 0
|
|
606
|
-
|| !/^[a-f0-9]{48}$/.test(batch.hostGeneration)
|
|
607
|
-
|| typeof batch.snapshotRequired !== "boolean" || !Array.isArray(batch.events) || batch.events.length > 256) {
|
|
608
|
-
throw codeError("workbench_changes_invalid");
|
|
609
|
-
}
|
|
610
|
-
return { content: [], structuredContent: { ok: true }, _meta: { hellolynkWorkbench: { changes: {
|
|
611
|
-
cursor: batch.cursor, hostGeneration: batch.hostGeneration,
|
|
612
|
-
snapshotRequired: batch.snapshotRequired, changed: batch.events.length > 0,
|
|
613
|
-
} } }, isError: false };
|
|
614
|
-
} catch (error) { return workbenchChangesError(error?.code || "workbench_changes_failed"); }
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function workbenchCandidateTool(candidate) {
|
|
618
|
-
return {
|
|
619
|
-
name: WORKBENCH_SNAPSHOT_TOOL,
|
|
620
|
-
title: "读取HelloLynk通讯快照",
|
|
621
|
-
description: "供HelloLynk共享 MCP App 读取本机 HelloLynk 的只读快照;消息正文和联系申请正文不会进入模型可见结果,原始设备核验值不得进入 Codex 结果。",
|
|
622
|
-
inputSchema: {
|
|
623
|
-
type: "object",
|
|
624
|
-
additionalProperties: false,
|
|
625
|
-
properties: {
|
|
626
|
-
threadId: {
|
|
627
|
-
type: "string",
|
|
628
|
-
minLength: 2,
|
|
629
|
-
maxLength: 199,
|
|
630
|
-
pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$",
|
|
631
|
-
},
|
|
632
|
-
threadCursor: {
|
|
633
|
-
type: "string",
|
|
634
|
-
minLength: 1,
|
|
635
|
-
maxLength: 255,
|
|
636
|
-
pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$",
|
|
637
|
-
},
|
|
638
|
-
messageCursor: {
|
|
639
|
-
type: "string",
|
|
640
|
-
minLength: 1,
|
|
641
|
-
maxLength: 800,
|
|
642
|
-
pattern: "^[A-Za-z0-9._-]{1,800}$",
|
|
643
|
-
},
|
|
644
|
-
contactRequestCursor: {
|
|
645
|
-
anyOf: [
|
|
646
|
-
{ type: "null" },
|
|
647
|
-
{ type: "string", minLength: 20, maxLength: 512, pattern: "^[A-Za-z0-9_-]{20,512}$" },
|
|
648
|
-
],
|
|
649
|
-
},
|
|
650
|
-
historyBefore: { type: ["string", "null"], maxLength: 30 },
|
|
651
|
-
query: { type: "string", maxLength: 512, pattern: "^[^\\u0000]*$" },
|
|
652
|
-
filter: { type: "string", enum: ["all", "unread", "archived"] },
|
|
653
|
-
},
|
|
654
|
-
},
|
|
655
|
-
annotations: TOOL_ANNOTATIONS,
|
|
656
|
-
_meta: {
|
|
657
|
-
ui: { resourceUri: candidate.resourceUri, visibility: ["app"] },
|
|
658
|
-
"openai/outputTemplate": candidate.resourceUri,
|
|
659
|
-
},
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function workbenchCommandTool(candidate) {
|
|
664
|
-
return {
|
|
665
|
-
name: WORKBENCH_COMMAND_TOOL,
|
|
666
|
-
title: "执行HelloLynk通讯操作",
|
|
667
|
-
description: "仅供HelloLynk MCP App 转交人类界面操作;写操作携带本机 HelloLynk 已签发的一次性私有授权,只读发送状态查询不签发授权,命令正文和完整结果不会进入普通工具结果。",
|
|
668
|
-
inputSchema: {
|
|
669
|
-
type: "object",
|
|
670
|
-
additionalProperties: false,
|
|
671
|
-
required: ["command"],
|
|
672
|
-
properties: {
|
|
673
|
-
command: workbenchCommandDtoSchema(candidate),
|
|
674
|
-
_hellolynkUiActionGrant: opaqueWorkbenchActionGrantSchema(),
|
|
675
|
-
},
|
|
676
|
-
},
|
|
677
|
-
annotations: WORKBENCH_COMMAND_ANNOTATIONS,
|
|
678
|
-
_meta: { ui: { resourceUri: candidate.resourceUri, visibility: ["app"] } },
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function workbenchActionGrantTool(candidate) {
|
|
683
|
-
return {
|
|
684
|
-
name: WORKBENCH_ACTION_GRANT_TOOL,
|
|
685
|
-
title: "准备HelloLynk通讯操作授权",
|
|
686
|
-
description: "仅供HelloLynk MCP App 把当前人类界面操作交给本机 HelloLynk 审核并准备一次性私有授权;授权只返回应用私有元数据。",
|
|
687
|
-
inputSchema: {
|
|
688
|
-
type: "object",
|
|
689
|
-
additionalProperties: false,
|
|
690
|
-
required: ["command"],
|
|
691
|
-
properties: { command: workbenchCommandDtoSchema(candidate) },
|
|
692
|
-
},
|
|
693
|
-
annotations: LOCAL_WRITE_ANNOTATIONS,
|
|
694
|
-
_meta: { ui: { resourceUri: candidate.resourceUri, visibility: ["app"] } },
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function workbenchCommandDtoSchema(candidate) {
|
|
699
|
-
return {
|
|
700
|
-
type: "object",
|
|
701
|
-
additionalProperties: false,
|
|
702
|
-
required: [
|
|
703
|
-
"contractId", "schemaVersion", "command", "requestId",
|
|
704
|
-
"idempotencyKey", "expectedRevision", "payload",
|
|
705
|
-
],
|
|
706
|
-
properties: {
|
|
707
|
-
contractId: { const: candidate.commandContractId },
|
|
708
|
-
schemaVersion: { const: candidate.commandSchemaVersion },
|
|
709
|
-
command: { type: "string", enum: [...candidate.commandTypes] },
|
|
710
|
-
requestId: { type: "string", minLength: 2, maxLength: 199 },
|
|
711
|
-
idempotencyKey: {
|
|
712
|
-
type: "string", maxLength: 199,
|
|
713
|
-
pattern: "^wb5\\.[a-f0-9]{32}\\.[1-9][0-9]{0,15}\\.[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$",
|
|
714
|
-
},
|
|
715
|
-
expectedRevision: {
|
|
716
|
-
anyOf: [
|
|
717
|
-
{ type: "null" },
|
|
718
|
-
{ type: "string", minLength: 1, maxLength: 255 },
|
|
719
|
-
],
|
|
720
|
-
},
|
|
721
|
-
payload: { type: "object" },
|
|
722
|
-
},
|
|
723
|
-
};
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function opaqueWorkbenchActionGrantSchema() {
|
|
727
|
-
return {
|
|
728
|
-
type: "string",
|
|
729
|
-
minLength: 32,
|
|
730
|
-
maxLength: 512,
|
|
731
|
-
pattern: "^[A-Za-z0-9._~-]{32,512}$",
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Preserve only established public read errors. A code-shaped private detail
|
|
736
|
-
// is not safe merely because safeErrorCode can normalize it.
|
|
737
|
-
const WORKBENCH_SNAPSHOT_READ_ERRORS = new Set([
|
|
738
|
-
"workbench_snapshot_timeout", "workbench_connection_required", "workbench_account_changed",
|
|
739
|
-
"host_request_unauthorized", "host_request_replayed", "host_adapter_route_forbidden",
|
|
740
|
-
"host_nonce_capability_cache_full", "host_nonce_cache_full",
|
|
741
|
-
"host_app_authorization_required", "host_capability_scope_denied",
|
|
742
|
-
"host_capability_human_surface_required", "host_capability_inactive",
|
|
743
|
-
"host_capability_invalid", "host_capability_id_mismatch", "host_capability_resolver_required",
|
|
744
|
-
"host_attachment_not_found", "host_attachment_expired", "host_attachment_ambiguous",
|
|
745
|
-
"neutral_host_discovery_generation_mismatch", "neutral_host_discovery_stale",
|
|
746
|
-
"neutral_host_discovery_invalid", "neutral_host_discovery_owner_mismatch",
|
|
747
|
-
"neutral_host_discovery_unsafe_path", "neutral_host_not_found",
|
|
748
|
-
"neutral_host_launcher_unavailable", "neutral_host_adapter_unavailable", "neutral_adapter_closed",
|
|
749
|
-
"neutral_codex_binding_invalid", "neutral_codex_capability_binding_mismatch",
|
|
750
|
-
"neutral_codex_discovery_timeout", "neutral_codex_launch_failed", "neutral_codex_launch_lock_timeout",
|
|
751
|
-
"neutral_codex_runner_integrity_invalid", "neutral_codex_runner_unavailable",
|
|
752
|
-
"host_not_running", "host_app_unavailable", "host_app_action_failed", "host_app_response_invalid",
|
|
753
|
-
"host_app_workbench_snapshot_failed", "host_app_workbench_thread_not_found",
|
|
754
|
-
"host_app_workbench_cursor_invalid", "host_app_workbench_read_invalid", "host_app_state_conflict",
|
|
755
|
-
"host_app_relationship_status_unavailable", "neutral_host_release_superseded",
|
|
756
|
-
]);
|
|
757
|
-
|
|
758
|
-
function workbenchSnapshotReadError(error) {
|
|
759
|
-
const code = error?.code;
|
|
760
|
-
if (WORKBENCH_SNAPSHOT_READ_ERRORS.has(code)) return code;
|
|
761
|
-
if (code === "neutral_host_http_401") return "host_request_unauthorized";
|
|
762
|
-
if (code === "neutral_host_http_403") return "host_app_authorization_required";
|
|
763
|
-
const networkCode = code ?? error?.cause?.code;
|
|
764
|
-
if (networkCode === "ECONNREFUSED") return "neutral_host_connection_refused";
|
|
765
|
-
if (networkCode === "ECONNRESET") return "neutral_host_connection_reset";
|
|
766
|
-
return "workbench_snapshot_unavailable";
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
async function workbenchSnapshotTool(args, candidate) {
|
|
770
|
-
const input = args ?? {};
|
|
771
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
772
|
-
return workbenchSurfaceToolError("invalid_params", "通讯读取");
|
|
773
|
-
}
|
|
774
|
-
let readRequest;
|
|
775
|
-
try {
|
|
776
|
-
readRequest = candidate.validateReadRequest(input);
|
|
777
|
-
} catch {
|
|
778
|
-
return workbenchSurfaceToolError("invalid_params", "通讯读取");
|
|
779
|
-
}
|
|
780
|
-
let rawSnapshot;
|
|
781
|
-
try {
|
|
782
|
-
rawSnapshot = await candidate.readSnapshot(readRequest);
|
|
783
|
-
} catch (error) {
|
|
784
|
-
return workbenchSurfaceToolError(workbenchSnapshotReadError(error), "通讯读取");
|
|
785
|
-
}
|
|
786
|
-
let snapshot;
|
|
787
|
-
try {
|
|
788
|
-
snapshot = candidate.validateSnapshot(rawSnapshot);
|
|
789
|
-
assertNoRawSafetyCodeFields(snapshot);
|
|
790
|
-
if (snapshot.source.mode !== "neutral-host" || snapshot.source.transport !== "mcp-app-rpc") {
|
|
791
|
-
throw codeError("workbench_snapshot_invalid");
|
|
792
|
-
}
|
|
793
|
-
} catch {
|
|
794
|
-
return workbenchSurfaceToolError("workbench_snapshot_invalid", "通讯读取");
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
content: [],
|
|
798
|
-
structuredContent: { ok: true },
|
|
799
|
-
_meta: { hellolynkWorkbench: { snapshot } },
|
|
800
|
-
isError: false,
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
async function workbenchActionGrantToolCall(args, candidate) {
|
|
805
|
-
try {
|
|
806
|
-
assertExactArgumentKeys(args, ["command"]);
|
|
807
|
-
const command = candidate.validateCommand(args.command);
|
|
808
|
-
if (command.command === "message.send-status") throw codeError("workbench_action_grant_invalid");
|
|
809
|
-
const issued = await candidate.issueActionGrant(command);
|
|
810
|
-
assertExactArgumentKeys(issued, ["actionGrant"]);
|
|
811
|
-
const actionGrant = safeOpaqueWorkbenchActionGrant(issued.actionGrant);
|
|
812
|
-
return {
|
|
813
|
-
content: [],
|
|
814
|
-
structuredContent: { ok: true },
|
|
815
|
-
_meta: { hellolynkWorkbench: { actionGrant } },
|
|
816
|
-
isError: false,
|
|
817
|
-
};
|
|
818
|
-
} catch (error) {
|
|
819
|
-
return workbenchSurfaceToolError(
|
|
820
|
-
error?.code === "workbench_action_grant_timeout"
|
|
821
|
-
? error.code
|
|
822
|
-
: "workbench_action_grant_invalid",
|
|
823
|
-
"通讯授权准备",
|
|
824
|
-
);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
async function workbenchCommandToolCall(args, candidate) {
|
|
829
|
-
try {
|
|
830
|
-
const command = candidate.validateCommand(args.command);
|
|
831
|
-
const statusRead = command.command === "message.send-status";
|
|
832
|
-
assertExactArgumentKeys(args, statusRead ? ["command"] : ["command", "_hellolynkUiActionGrant"]);
|
|
833
|
-
const actionGrant = statusRead ? null : safeOpaqueWorkbenchActionGrant(args._hellolynkUiActionGrant);
|
|
834
|
-
const result = candidate.validateCommandResult(
|
|
835
|
-
await candidate.executeCommand(command, statusRead ? {} : { actionGrant }),
|
|
836
|
-
command.command,
|
|
837
|
-
);
|
|
838
|
-
assertNoRawSafetyCodeFields(result);
|
|
839
|
-
if (result.requestId !== command.requestId) throw codeError("workbench_candidate_command_result_invalid");
|
|
840
|
-
return {
|
|
841
|
-
content: [],
|
|
842
|
-
structuredContent: { ok: true },
|
|
843
|
-
_meta: { hellolynkWorkbench: { commandResult: result } },
|
|
844
|
-
isError: false,
|
|
845
|
-
};
|
|
846
|
-
} catch {
|
|
847
|
-
return workbenchSurfaceToolError("workbench_command_invalid", "通讯操作");
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
async function neutralGetUnreadCountTool(adapter) {
|
|
852
|
-
if (!adapter || typeof adapter.readUnreadCount !== "function") {
|
|
853
|
-
return toolError("neutral_adapter_unavailable");
|
|
854
|
-
}
|
|
855
|
-
try {
|
|
856
|
-
const result = await adapter.readUnreadCount();
|
|
857
|
-
const unreadCount = result?.unreadCount;
|
|
858
|
-
if (!Number.isSafeInteger(unreadCount) || unreadCount < 0) {
|
|
859
|
-
return toolError("neutral_unread_count_invalid");
|
|
860
|
-
}
|
|
861
|
-
const unreadMessages = unreadCount;
|
|
862
|
-
return {
|
|
863
|
-
content: [{ type: "text", text: resultText.unread(unreadMessages) }],
|
|
864
|
-
structuredContent: {
|
|
865
|
-
ok: true,
|
|
866
|
-
configured: true,
|
|
867
|
-
status: "running",
|
|
868
|
-
counts: { unreadMessages },
|
|
869
|
-
},
|
|
870
|
-
isError: false,
|
|
871
|
-
};
|
|
872
|
-
} catch (error) {
|
|
873
|
-
return toolError(safeErrorCode(error));
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
async function neutralListAuthorizedContactsTool(args, adapter) {
|
|
878
|
-
try {
|
|
879
|
-
const result = await findContacts(args, query => adapter.listAuthorizedContacts(query));
|
|
880
|
-
const contacts = Array.isArray(result?.contacts) ? result.contacts.map((contact) => ({
|
|
881
|
-
contactId: safeIdentifier(contact?.contactId, "contact_id_invalid", 200),
|
|
882
|
-
displayName: safeIdentifier(contact?.displayName, "contact_display_name_invalid", 160),
|
|
883
|
-
recipientDelegateId: safeIdentifier(contact?.recipientDelegateId, "recipient_delegate_id_invalid", 200),
|
|
884
|
-
instanceDisplayName: safeIdentifier(contact?.instanceDisplayName, "instance_display_name_invalid", 160),
|
|
885
|
-
})) : [];
|
|
886
|
-
return {
|
|
887
|
-
content: [{ type: "text", text: resultText.contacts(contacts.length) }],
|
|
888
|
-
structuredContent: { contacts, ...(result.nativeConversations ? { nativeConversations: result.nativeConversations } : {}) },
|
|
889
|
-
isError: false,
|
|
890
|
-
};
|
|
891
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
async function neutralSendAuthorizedMessageTool(args, adapter, runtime) {
|
|
895
|
-
let clientMessageId;
|
|
896
|
-
let phase = "input_validation";
|
|
897
|
-
try {
|
|
898
|
-
if (!args || typeof args !== "object" || Array.isArray(args)) throw codeError("arguments_invalid");
|
|
899
|
-
assertExactArgumentKeys(args, sendArgumentKeys(args));
|
|
900
|
-
if (Object.values(args).some((value) => typeof value !== "string")) throw codeError("arguments_invalid");
|
|
901
|
-
if (!Object.hasOwn(args, "contact")) {
|
|
902
|
-
authorizedWriteIdentifier(args.contactId, "contact_id_invalid");
|
|
903
|
-
authorizedWriteIdentifier(args.recipientDelegateId, "recipient_delegate_id_invalid");
|
|
904
|
-
}
|
|
905
|
-
optionalTarget(args);
|
|
906
|
-
clientMessageId = authorizedWriteIdentifier(args.clientMessageId, "client_message_id_invalid");
|
|
907
|
-
const text = args.text;
|
|
908
|
-
if (!text || text.includes("\u0000") || [...text].length > 20_000 || Buffer.byteLength(text, "utf8") > 20 * 1024) {
|
|
909
|
-
throw codeError("message_text_invalid");
|
|
910
|
-
}
|
|
911
|
-
phase = "preparation";
|
|
912
|
-
const { contactId, recipientDelegateId } = await sendRecipient(args, query => adapter.listAuthorizedContacts(query));
|
|
913
|
-
await runtime?.prepareAuthorizedWrite?.(runtime.executorMetadata, { clientMessageId, peerDelegateId: recipientDelegateId });
|
|
914
|
-
phase = "adapter_call";
|
|
915
|
-
const result = await adapter.sendAuthorizedMessage({ contactId, recipientDelegateId, clientMessageId, text, ...optionalTarget(args) });
|
|
916
|
-
phase = "response_validation";
|
|
917
|
-
if (result?.accepted !== true || result?.ok === false || result?.authorization !== "contact_delegate_allowlist"
|
|
918
|
-
|| result?.clientMessageId !== clientMessageId || result?.contactId !== contactId
|
|
919
|
-
|| !["accepted", "delivered_to_live_connection", "acknowledged_by_recipient_local_store"].includes(result?.status)) throw codeError("send_result_invalid");
|
|
920
|
-
const receipt = firstSendReceipt(args, result.status);
|
|
921
|
-
return {
|
|
922
|
-
content: [{ type: "text", text: receipt.text }],
|
|
923
|
-
structuredContent: {
|
|
924
|
-
accepted: true,
|
|
925
|
-
clientMessageId,
|
|
926
|
-
contactId,
|
|
927
|
-
recipientDelegateId,
|
|
928
|
-
status: result.status,
|
|
929
|
-
destination: receipt.destination,
|
|
930
|
-
},
|
|
931
|
-
isError: false,
|
|
932
|
-
};
|
|
933
|
-
} catch (error) { return authorizedWriteToolError(error, clientMessageId, "authorized_send_result_unknown", phase); }
|
|
934
|
-
}
|
|
935
|
-
async function neutralListAuthorizedInboxMessagesTool(args, adapter) {
|
|
936
|
-
const older = args != null && typeof args === "object" && !Array.isArray(args) && Object.hasOwn(args, "before");
|
|
937
|
-
if (!older && !plainEmptyObject(args)) return toolError("arguments_not_allowed");
|
|
938
|
-
try {
|
|
939
|
-
if (older) assertExactArgumentKeys(args, ["before"]);
|
|
940
|
-
const result = await adapter.listAuthorizedInboxMessages(older
|
|
941
|
-
? { before: safeIdentifier(args.before, "authorized_inbox_message_id_invalid", 240) } : {});
|
|
942
|
-
const messages = Array.isArray(result?.messages)
|
|
943
|
-
? result.messages.map(value => ({ ...projectAuthorizedInboxMetadata(value), ...replyHint(value) }))
|
|
944
|
-
: [];
|
|
945
|
-
return {
|
|
946
|
-
content: [{ type: "text", text: resultText.inbox(messages.length, older) }],
|
|
947
|
-
structuredContent: { messages },
|
|
948
|
-
isError: false,
|
|
949
|
-
};
|
|
950
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
async function neutralReadAuthorizedInboxMessageTool(args, adapter) {
|
|
954
|
-
try {
|
|
955
|
-
assertExactArgumentKeys(args, ["localMessageId", "conversationKey"]);
|
|
956
|
-
const message = projectAuthorizedInboxRead(await adapter.readAuthorizedInboxMessage(
|
|
957
|
-
safeIdentifier(args.localMessageId, "authorized_inbox_message_id_invalid", 240),
|
|
958
|
-
safeIdentifier(args.conversationKey, "authorized_inbox_conversation_key_invalid", 240),
|
|
959
|
-
));
|
|
960
|
-
return { content: [{ type: "text", text: JSON.stringify(message) }], structuredContent: message, isError: false };
|
|
961
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
async function neutralListPendingMessagesTool(args, adapter) {
|
|
965
|
-
if (!plainEmptyObject(args)) return toolError("arguments_not_allowed");
|
|
966
|
-
try {
|
|
967
|
-
const result = await adapter.listPendingMessages();
|
|
968
|
-
if (!Array.isArray(result?.messages) || result.messages.length > 1000) throw codeError("pending_messages_invalid");
|
|
969
|
-
const messages = result.messages.map((value) => {
|
|
970
|
-
if (value?.wakeEligible !== true) throw codeError("pending_message_ineligible");
|
|
971
|
-
return projectAuthorizedInboxMetadata(value);
|
|
972
|
-
});
|
|
973
|
-
return { content: [{ type: "text", text: resultText.pending(messages.length) }], structuredContent: { messages }, isError: false };
|
|
974
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
async function neutralClaimMessageTool(args, adapter) {
|
|
978
|
-
try {
|
|
979
|
-
if (args?.batchId != null) {
|
|
980
|
-
assertExactArgumentKeys(args, ["batchId"]);
|
|
981
|
-
const batchId = safeIdentifier(args.batchId, "authorized_inbox_batch_id_invalid", 128);
|
|
982
|
-
const result = await adapter.claimBatch(batchId);
|
|
983
|
-
if (!Array.isArray(result?.messages) || result.claimed !== (result.messages.length > 0)
|
|
984
|
-
|| !Number.isSafeInteger(result.expiredCount) || result.expiredCount < 0) throw codeError("claim_result_invalid");
|
|
985
|
-
const messages = result.messages.map(projectAuthorizedInboxRead);
|
|
986
|
-
if (messages.reduce((n, m) => n + Array.from(m.text).length, 0) > 20000) throw codeError("claim_result_invalid");
|
|
987
|
-
const output = { claimed: result.claimed, messages, expiredCount: result.expiredCount,
|
|
988
|
-
notice: result.claimed ? (result.expiredCount ? resultText.claimed(messages.length, result.expiredCount) : null) : "这批来信已读取,无需重复读取。" };
|
|
989
|
-
return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output, isError: false };
|
|
990
|
-
}
|
|
991
|
-
assertExactArgumentKeys(args, ["localMessageId", "conversationKey"]);
|
|
992
|
-
const localMessageId = safeIdentifier(args.localMessageId, "authorized_inbox_message_id_invalid", 240);
|
|
993
|
-
const conversationKey = safeIdentifier(args.conversationKey, "authorized_inbox_conversation_key_invalid", 240);
|
|
994
|
-
const result = await adapter.claimMessage(localMessageId, conversationKey);
|
|
995
|
-
if (result?.claimed === false && result.claimId === null && result.message === null) {
|
|
996
|
-
return { content: [{ type: "text", text: "此来信已被领取或处理。" }], structuredContent: { claimed: false, claimId: null, message: null }, isError: false };
|
|
997
|
-
}
|
|
998
|
-
if (result?.claimed !== true || !/^inclaim_[a-f0-9]{48}$/.test(result.claimId)) throw codeError("claim_result_invalid");
|
|
999
|
-
const message = projectAuthorizedInboxRead(result.message);
|
|
1000
|
-
if (message.localMessageId !== localMessageId || message.conversationKey !== conversationKey
|
|
1001
|
-
|| message.source !== "hellolynk_external_message"
|
|
1002
|
-
|| !projectInboxReadState(message) || message.wakeEligible !== true) throw codeError("claim_result_invalid");
|
|
1003
|
-
return { content: [{ type: "text", text: JSON.stringify({ claimId: result.claimId, message }) }],
|
|
1004
|
-
structuredContent: { claimed: true, claimId: result.claimId, message }, isError: false };
|
|
1005
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
async function neutralReplyAuthorizedMessageTool(args, adapter, runtime) {
|
|
1009
|
-
let clientMessageId;
|
|
1010
|
-
let phase = "input_validation";
|
|
1011
|
-
try {
|
|
1012
|
-
assertExactArgumentKeys(args, ["localMessageId", "conversationKey", "clientMessageId", "text"]);
|
|
1013
|
-
if (Object.values(args).some((value) => typeof value !== "string")) throw codeError("arguments_invalid");
|
|
1014
|
-
const localMessageId = authorizedWriteIdentifier(args.localMessageId, "authorized_inbox_message_id_invalid");
|
|
1015
|
-
const conversationKey = authorizedWriteIdentifier(args.conversationKey, "authorized_inbox_conversation_key_invalid");
|
|
1016
|
-
clientMessageId = authorizedWriteIdentifier(args.clientMessageId, "client_message_id_invalid");
|
|
1017
|
-
const text = args.text;
|
|
1018
|
-
if (!text || text.includes("\u0000") || [...text].length > 20_000 || Buffer.byteLength(text, "utf8") > 20 * 1024) throw codeError("message_text_invalid");
|
|
1019
|
-
phase = "preparation";
|
|
1020
|
-
await adapter.assertModelInboxReply({ localMessageId, conversationKey });
|
|
1021
|
-
await runtime?.prepareAuthorizedWrite?.(runtime.executorMetadata, { clientMessageId, conversationKey });
|
|
1022
|
-
phase = "adapter_call";
|
|
1023
|
-
const result = await adapter.replyAuthorizedMessage({ localMessageId, conversationKey, clientMessageId, text });
|
|
1024
|
-
phase = "response_validation";
|
|
1025
|
-
if (result?.accepted !== true || result?.ok === false || result?.authorization !== "contact_delegate_allowlist"
|
|
1026
|
-
|| result?.clientMessageId !== clientMessageId
|
|
1027
|
-
|| !["accepted", "delivered_to_live_connection", "acknowledged_by_recipient_local_store"].includes(result?.status)) throw codeError("reply_result_invalid");
|
|
1028
|
-
const status = safeIdentifier(result.status, "send_status_invalid", 80);
|
|
1029
|
-
return { content: [{ type: "text", text: resultText.sent(status, clientMessageId) }],
|
|
1030
|
-
structuredContent: { accepted: true, clientMessageId,
|
|
1031
|
-
contactId: safeIdentifier(result.contactId, "contact_id_invalid", 200), status }, isError: false };
|
|
1032
|
-
} catch (error) { return authorizedWriteToolError(error, clientMessageId, "authorized_reply_result_unknown", phase); }
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
async function neutralListAuthorizedContactRequestsTool(args, adapter) {
|
|
1036
|
-
if (!plainEmptyObject(args)) return toolError("arguments_not_allowed");
|
|
1037
|
-
try {
|
|
1038
|
-
const result = await adapter.listAuthorizedContactRequests();
|
|
1039
|
-
const requests = (result?.requests || []).map(projectAuthorizedContactRequestMetadata);
|
|
1040
|
-
return { content: [{ type: "text", text: `联系申请 ${requests.length} 条。` }], structuredContent: { requests }, isError: false };
|
|
1041
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
async function neutralReadAuthorizedContactRequestTool(args, adapter) {
|
|
1045
|
-
try {
|
|
1046
|
-
assertExactArgumentKeys(args, ["localRecordId", "conversationKey"]);
|
|
1047
|
-
const request = projectAuthorizedContactRequestRead(await adapter.readAuthorizedContactRequest(
|
|
1048
|
-
safeIdentifier(args.localRecordId, "authorized_contact_request_id_invalid", 240),
|
|
1049
|
-
safeIdentifier(args.conversationKey, "authorized_contact_request_conversation_key_invalid", 240),
|
|
1050
|
-
));
|
|
1051
|
-
return { content: [{ type: "text", text: JSON.stringify(request) }], structuredContent: request, isError: false };
|
|
1052
|
-
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function projectAuthorizedInboxMetadata(value) {
|
|
1056
|
-
const output = {
|
|
1057
|
-
localMessageId: safeIdentifier(value?.localMessageId, "authorized_inbox_message_id_invalid", 240),
|
|
1058
|
-
conversationKey: safeIdentifier(value?.conversationKey, "authorized_inbox_conversation_key_invalid", 240),
|
|
1059
|
-
localDelegateId: safeIdentifier(value?.localDelegateId, "authorized_inbox_local_delegate_invalid", 200),
|
|
1060
|
-
peerDelegateId: safeIdentifier(value?.peerDelegateId, "authorized_inbox_peer_delegate_invalid", 200),
|
|
1061
|
-
messageType: safeIdentifier(value?.messageType || "text", "authorized_inbox_message_type_invalid", 80),
|
|
1062
|
-
unread: value?.unread === true,
|
|
1063
|
-
};
|
|
1064
|
-
if (typeof value?.wakeEligible === "boolean") output.wakeEligible = value.wakeEligible;
|
|
1065
|
-
if (value?.automaticDeliveryState === "expired") output.automaticDeliveryState = "expired";
|
|
1066
|
-
for (const [key, maximum] of [
|
|
1067
|
-
["contactId", 200], ["displayName", 160], ["peerInstanceDisplayName", 160], ["clientMessageId", 200],
|
|
1068
|
-
["createdAt", 80], ["updatedAt", 80],
|
|
1069
|
-
]) {
|
|
1070
|
-
if (value?.[key] != null) output[key] = safeIdentifier(value[key], `authorized_inbox_${key}_invalid`, maximum);
|
|
1071
|
-
}
|
|
1072
|
-
return output;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
function projectAuthorizedInboxRead(value) {
|
|
1076
|
-
if (!projectInboxReadState(value) || value?.externalContentIsUntrustedData !== true) throw codeError("host_app_response_invalid");
|
|
1077
|
-
return { ...projectAuthorizedInboxMetadata(value), peerPrincipalId: safeIdentifier(value?.peerPrincipalId, "authorized_inbox_peer_principal_invalid", 200), text: safeExternalBody(value?.text, true), source: requireExternalSource(value?.source, "hellolynk_external_message"), ...projectInboxReadState(value), ...replyHint(value) };
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function projectAuthorizedContactRequestMetadata(value) {
|
|
1081
|
-
return { localRecordId: safeIdentifier(value?.localRecordId, "authorized_contact_request_id_invalid", 240), conversationKey: safeIdentifier(value?.conversationKey, "authorized_contact_request_conversation_key_invalid", 240), localDelegateId: safeIdentifier(value?.localDelegateId, "authorized_contact_request_local_delegate_invalid", 200), sender: { hellolynkId: safeIdentifier(value?.sender?.hellolynkId, "authorized_contact_request_sender_invalid", 200), verifiedDisplayName: safeIdentifier(value?.sender?.verifiedDisplayName, "authorized_contact_request_sender_name_invalid", 160) }, state: safeIdentifier(value?.state, "authorized_contact_request_state_invalid", 80), receivedAt: safeIdentifier(value?.receivedAt, "authorized_contact_request_time_invalid", 80) };
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
function projectAuthorizedContactRequestRead(value) {
|
|
1085
|
-
if (value?.externalContentIsUntrustedData !== true) throw codeError("host_app_response_invalid");
|
|
1086
|
-
return { ...projectAuthorizedContactRequestMetadata(value), introText: safeContactRequestIntro(value?.introText), source: requireExternalSource(value?.source, "hellolynk_contact_request"), readDoesNotMarkRead: value?.readDoesNotMarkRead === true };
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
function strictConversationSchema(id) {
|
|
1090
|
-
const message = id === "localMessageId";
|
|
1091
|
-
return { type: "object", additionalProperties: false, required: [id, "conversationKey"], properties: {
|
|
1092
|
-
[id]: { type: "string", minLength: 3, maxLength: 240, ...(message ? { description: paramText.localMessageId } : {}) },
|
|
1093
|
-
conversationKey: { type: "string", minLength: 3, maxLength: 240, ...(message ? { description: paramText.conversationKey } : {}) } } };
|
|
1094
|
-
}
|
|
1095
|
-
function safeContactRequestIntro(value) { return value === "" ? "" : safeExternalBody(value); }
|
|
1096
|
-
function safeExternalBody(value, countCodePoints = false) { const text = String(value ?? ""); if (!text || text.includes("\u0000") || (countCodePoints ? Array.from(text).length : text.length) > 20000 || Buffer.byteLength(text, "utf8") > 20 * 1024) throw codeError("external_content_text_invalid"); return text; }
|
|
1097
|
-
function requireExternalSource(value, expected) { if (value !== expected) throw codeError("external_content_source_invalid"); return expected; }
|
|
1098
|
-
|
|
1099
|
-
function toolError(code) {
|
|
1100
|
-
const nativeError = nativeAddressingError({ code });
|
|
1101
|
-
if (nativeError) return nativeError;
|
|
1102
|
-
const safe = safeErrorCode({ code });
|
|
1103
|
-
const deleted = safe === "host_app_reply_target_deleted";
|
|
1104
|
-
return {
|
|
1105
|
-
content: [{ type: "text", text: failureText(safe) }],
|
|
1106
|
-
structuredContent: { ok: false, configured: true, status: deleted ? "not_delivered" : "blocked", errorCode: safe,
|
|
1107
|
-
...(deleted ? { automaticRetryAllowed: false } : {}) },
|
|
1108
|
-
isError: true,
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
function authorizedWriteToolError(error, clientMessageId, timeoutCode, phase) {
|
|
1112
|
-
const nativeError = nativeAddressingError(error, { clientMessageId, phase });
|
|
1113
|
-
if (nativeError) return nativeError;
|
|
1114
|
-
// A timeout after the send began is an unknown result, whatever code it carries.
|
|
1115
|
-
const timedOut = clientMessageId && ["adapter_call", "response_validation"].includes(phase) && error?.name === "TimeoutError";
|
|
1116
|
-
const code = timedOut ? timeoutCode : safeErrorCode(error);
|
|
1117
|
-
// Phase and retry policy come only from this invocation, never from an adapter error.
|
|
1118
|
-
const { text, ...outcome } = writeFailure(code, phase);
|
|
1119
|
-
return { content: [{ type: "text", text }], structuredContent: { ok: false, configured: true, errorCode: code, ...outcome,
|
|
1120
|
-
phase, ...(clientMessageId ? { clientMessageId } : {}) }, isError: true };
|
|
1121
|
-
}
|
|
1122
|
-
function authorizedWriteIdentifier(value, code) { if (typeof value !== "string" || !WRITE_ID_PATTERN.test(value)) throw codeError(code); return value; }
|
|
1123
|
-
function safeIdentifier(value, code, maximum) {
|
|
1124
|
-
const normalized = String(value ?? "").trim();
|
|
1125
|
-
if (!normalized || normalized.length > maximum || /[\r\n\u0000-\u001f\u007f]/.test(normalized)) {
|
|
1126
|
-
throw codeError(code);
|
|
1127
|
-
}
|
|
1128
|
-
return normalized;
|
|
1129
|
-
}function safeOpaqueWorkbenchActionGrant(value) {
|
|
1130
|
-
if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{32,512}$/.test(value)) {
|
|
1131
|
-
throw codeError("workbench_action_grant_invalid");
|
|
1132
|
-
}
|
|
1133
|
-
return value;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function assertNoRawSafetyCodeFields(value, seen = new WeakSet(), depth = 0) {
|
|
1137
|
-
if (!value || typeof value !== "object") return;
|
|
1138
|
-
if (depth > 32 || seen.has(value)) throw codeError("workbench_snapshot_invalid");
|
|
1139
|
-
seen.add(value);
|
|
1140
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
1141
|
-
const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1142
|
-
if (normalized === "safetycode" || normalized === "localdevicesafetycode") {
|
|
1143
|
-
throw codeError("workbench_raw_safety_code_forbidden");
|
|
1144
|
-
}
|
|
1145
|
-
assertNoRawSafetyCodeFields(nested, seen, depth + 1);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
function assertExactArgumentKeys(value, expected) {
|
|
1150
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw codeError("arguments_invalid");
|
|
1151
|
-
const actual = Object.keys(value).sort();
|
|
1152
|
-
const wanted = [...expected].sort();
|
|
1153
|
-
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
1154
|
-
throw codeError("arguments_invalid");
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function plainEmptyObject(value) {
|
|
1159
|
-
if (value === undefined || value === null) return true;
|
|
1160
|
-
return typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
function safeErrorCode(error) {
|
|
1164
|
-
const value = String(error?.code || error?.message || "inbox_error");
|
|
1165
|
-
const normalized = value
|
|
1166
|
-
.replace(/[^A-Za-z0-9._:-]+/g, "_")
|
|
1167
|
-
.replace(/_+/g, "_")
|
|
1168
|
-
.replace(/^_+|_+$/g, "")
|
|
1169
|
-
.slice(0, 120);
|
|
1170
|
-
return normalized || "inbox_error";
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function codeError(code) {
|
|
1174
|
-
const error = new Error(code);
|
|
1175
|
-
error.code = code;
|
|
1176
|
-
return error;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
function jsonRpcError(id, code, message) {
|
|
1180
|
-
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function write(message, output = process.stdout) {
|
|
1184
|
-
output.write(`${JSON.stringify(message)}\n`);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1188
|
-
if (isMain) {
|
|
1189
|
-
runStdioMcpServer().catch((error) => {
|
|
1190
|
-
process.stderr.write(`${JSON.stringify({ ok: false, error: safeErrorCode(error) })}\n`);
|
|
1191
|
-
process.exitCode = 1;
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { prepareModelInboxInvocation, isModelInboxTool } from "../runtime/core/hellolynk-model-inbox.mjs";
|
|
3
|
+
import { inspectCodexExecutorMetadata } from "./native-task-client.mjs";
|
|
4
|
+
import { nativeTool, optionalTarget, nativeAddressingError, replyHint, conversationGuidance, firstSendReceipt, paramText, resultText, sendArgumentKeys, sendRecipient, findContacts, writeFailure, failureText } from "../runtime/core/hellolynk-native-addressing-tools.cjs";
|
|
5
|
+
import { withHelloLynkModelNotices, captureHelloLynkNativeDeliveryStatus } from "../runtime/core/hellolynk-model-notices.cjs";
|
|
6
|
+
|
|
7
|
+
import readline from "node:readline";
|
|
8
|
+
import { projectInboxReadState } from "../runtime/core/hellolynk-inbox-read-state.mjs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { CODEX_WORKBENCH_RESOURCE_META } from "./workbench-runtime.mjs";
|
|
12
|
+
import packageMetadata from "../package.json" with { type: "json" };
|
|
13
|
+
|
|
14
|
+
if (packageMetadata.name !== "@hellolynk-ai/codex-plugin"
|
|
15
|
+
|| typeof packageMetadata.version !== "string" || !/^\d+\.\d+\.\d+$/.test(packageMetadata.version)) {
|
|
16
|
+
throw codeError("codex_package_metadata_invalid");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const WRITE_ID = Object.freeze({ type: "string", minLength: 3, maxLength: 128, pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{2,127}$" });
|
|
20
|
+
const WRITE_ID_PATTERN = new RegExp(WRITE_ID.pattern);
|
|
21
|
+
const CLIENT_MESSAGE_ID = Object.freeze({ ...WRITE_ID, examples: ["msg-a1b2c3-01"], description: paramText.clientMessageId });
|
|
22
|
+
const MESSAGE_TEXT = Object.freeze({ type: "string", minLength: 1, maxLength: 20000, description: paramText.text });
|
|
23
|
+
|
|
24
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
25
|
+
const VERSIONED_WORKBENCH_URI_RE = /^ui:\/\/hellolynk\/workbench-codex-[a-f0-9]{64}\.html$/;
|
|
26
|
+
|
|
27
|
+
const TOOL_ANNOTATIONS = Object.freeze({
|
|
28
|
+
readOnlyHint: true,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
destructiveHint: false,
|
|
31
|
+
idempotentHint: true,
|
|
32
|
+
});
|
|
33
|
+
const LOCAL_WRITE_ANNOTATIONS = Object.freeze({
|
|
34
|
+
readOnlyHint: false,
|
|
35
|
+
openWorldHint: false,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: true,
|
|
38
|
+
});
|
|
39
|
+
const WORKBENCH_COMMAND_ANNOTATIONS = Object.freeze({
|
|
40
|
+
readOnlyHint: false,
|
|
41
|
+
openWorldHint: true,
|
|
42
|
+
destructiveHint: true,
|
|
43
|
+
idempotentHint: false,
|
|
44
|
+
});
|
|
45
|
+
const SEND_ANNOTATIONS = Object.freeze({
|
|
46
|
+
readOnlyHint: false,
|
|
47
|
+
openWorldHint: true,
|
|
48
|
+
destructiveHint: false,
|
|
49
|
+
idempotentHint: false,
|
|
50
|
+
});
|
|
51
|
+
const WORKBENCH_CONNECT_TOOL = "hellolynk_workbench_connect";
|
|
52
|
+
const WORKBENCH_SNAPSHOT_TOOL = "hellolynk_workbench_snapshot";
|
|
53
|
+
const WORKBENCH_CHANGES_TOOL = "hellolynk_workbench_changes";
|
|
54
|
+
const WORKBENCH_ACTION_GRANT_TOOL = "hellolynk_workbench_action_grant";
|
|
55
|
+
const WORKBENCH_COMMAND_TOOL = "hellolynk_workbench_command";
|
|
56
|
+
const OPEN_HELLOLYNK_TOOL = "hellolynk_open";
|
|
57
|
+
|
|
58
|
+
// nativeTool adds the titles and descriptions every client shares.
|
|
59
|
+
const neutralTools = [
|
|
60
|
+
{
|
|
61
|
+
name: OPEN_HELLOLYNK_TOOL,
|
|
62
|
+
title: "打开 HelloLynk",
|
|
63
|
+
description: "在聊天里显示 HelloLynk 卡片。",
|
|
64
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
65
|
+
annotations: TOOL_ANNOTATIONS,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "hellolynk_get_unread_count",
|
|
69
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
70
|
+
annotations: TOOL_ANNOTATIONS,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "hellolynk_list_authorized_contacts",
|
|
74
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
75
|
+
annotations: TOOL_ANNOTATIONS,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "hellolynk_send_authorized_message",
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: "object",
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
required: ["clientMessageId", "text"],
|
|
83
|
+
properties: {
|
|
84
|
+
contactId: WRITE_ID,
|
|
85
|
+
recipientDelegateId: WRITE_ID,
|
|
86
|
+
clientMessageId: CLIENT_MESSAGE_ID,
|
|
87
|
+
text: MESSAGE_TEXT,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
annotations: SEND_ANNOTATIONS,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "hellolynk_list_authorized_inbox_messages",
|
|
94
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
95
|
+
before: { type: "string", minLength: 3, maxLength: 198, description: paramText.before },
|
|
96
|
+
} },
|
|
97
|
+
annotations: TOOL_ANNOTATIONS,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "hellolynk_read_authorized_inbox_message",
|
|
101
|
+
inputSchema: strictConversationSchema("localMessageId"), annotations: TOOL_ANNOTATIONS,
|
|
102
|
+
},
|
|
103
|
+
{ name: "hellolynk_list_pending_messages",
|
|
104
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} }, annotations: LOCAL_WRITE_ANNOTATIONS },
|
|
105
|
+
{ name: "hellolynk_claim_message",
|
|
106
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
107
|
+
batchId: { type: "string", minLength: 3, maxLength: 128, description: paramText.batchId },
|
|
108
|
+
localMessageId: { type: "string", minLength: 3, maxLength: 240, description: paramText.localMessageId },
|
|
109
|
+
conversationKey: { type: "string", minLength: 3, maxLength: 240, description: paramText.conversationKey },
|
|
110
|
+
}, oneOf: [{ required: ["batchId"] }, { required: ["localMessageId", "conversationKey"] }] }, annotations: LOCAL_WRITE_ANNOTATIONS },
|
|
111
|
+
{ name: "hellolynk_reply_authorized_message",
|
|
112
|
+
inputSchema: { type: "object", additionalProperties: false, required: ["localMessageId", "conversationKey", "clientMessageId", "text"], properties: {
|
|
113
|
+
localMessageId: { ...WRITE_ID, description: paramText.localMessageId },
|
|
114
|
+
conversationKey: { ...WRITE_ID, description: paramText.conversationKey },
|
|
115
|
+
clientMessageId: CLIENT_MESSAGE_ID, text: MESSAGE_TEXT,
|
|
116
|
+
} }, annotations: SEND_ANNOTATIONS },
|
|
117
|
+
{
|
|
118
|
+
name: "hellolynk_list_authorized_contact_requests",
|
|
119
|
+
inputSchema: { type: "object", additionalProperties: false, properties: {} }, annotations: TOOL_ANNOTATIONS,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "hellolynk_read_authorized_contact_request",
|
|
123
|
+
inputSchema: strictConversationSchema("localRecordId"), annotations: TOOL_ANNOTATIONS,
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
].map(nativeTool);
|
|
127
|
+
|
|
128
|
+
export async function runStdioMcpServer(options = {}) {
|
|
129
|
+
const neutralAdapter = options.neutralAdapter || null;
|
|
130
|
+
const input = options.input || process.stdin;
|
|
131
|
+
const output = options.output || process.stdout;
|
|
132
|
+
const rl = readline.createInterface({
|
|
133
|
+
input,
|
|
134
|
+
crlfDelay: Number.POSITIVE_INFINITY,
|
|
135
|
+
});
|
|
136
|
+
const stopMcp = () => {
|
|
137
|
+
rl.close();
|
|
138
|
+
};
|
|
139
|
+
process.once("SIGINT", stopMcp);
|
|
140
|
+
process.once("SIGTERM", stopMcp);
|
|
141
|
+
let commands = Promise.resolve();
|
|
142
|
+
let appReads = Promise.resolve();
|
|
143
|
+
let failure;
|
|
144
|
+
let protocolWarmupStarted = false;
|
|
145
|
+
const serve = async (line) => {
|
|
146
|
+
const { message: handled, response } = await handleLine(line, output, {
|
|
147
|
+
neutralAdapter,
|
|
148
|
+
prepareAuthorizedWrite: options.prepareAuthorizedWrite,
|
|
149
|
+
readNativeDeliveryStatus: options.readNativeDeliveryStatus,
|
|
150
|
+
...(options.workbenchCandidate === undefined ? {} : { workbenchCandidate: options.workbenchCandidate }),
|
|
151
|
+
}) || {};
|
|
152
|
+
if (!protocolWarmupStarted && response?.result
|
|
153
|
+
&& ["initialize", "tools/list"].includes(handled?.method)
|
|
154
|
+
&& typeof options.onProtocolReady === "function") {
|
|
155
|
+
// Codex may cache tools/list entirely. Start once after a successful
|
|
156
|
+
// handshake (or catalog response), never on an invalid request and
|
|
157
|
+
// never in the response's critical path. Real calls can retry failure.
|
|
158
|
+
protocolWarmupStarted = true;
|
|
159
|
+
Promise.resolve()
|
|
160
|
+
.then(() => options.onProtocolReady())
|
|
161
|
+
.catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
if (handled?.method === "tools/call"
|
|
164
|
+
&& !(WORKBENCH_READ_TOOLS.has(handled.params?.name) && handled.params?.name !== OPEN_HELLOLYNK_TOOL)
|
|
165
|
+
&& typeof options.onExecutorToolCall === "function") {
|
|
166
|
+
// Only the executor-owned MCP request envelope can identify the
|
|
167
|
+
// current Codex task. App-only polling is not a model turn and must
|
|
168
|
+
// not retarget its receiver while a different model turn is sending.
|
|
169
|
+
// The model-visible open tool still registers its executor context.
|
|
170
|
+
// External tool arguments are never inspected.
|
|
171
|
+
Promise.resolve()
|
|
172
|
+
.then(() => options.onExecutorToolCall(handled.params?._meta))
|
|
173
|
+
.catch(() => {});
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
for await (const line of rl) {
|
|
178
|
+
if (!line.trim()) continue;
|
|
179
|
+
let message;
|
|
180
|
+
try { message = JSON.parse(line); } catch { /* handleLine reports parse_error */ }
|
|
181
|
+
const method = message?.method;
|
|
182
|
+
// HTML and protocol responses do not wait for Host startup. They remain
|
|
183
|
+
// available while a Host operation is waiting, so the UI can display its
|
|
184
|
+
// own bounded wait/error state. Business calls retain their original order;
|
|
185
|
+
// an unknown write result must never cause replay or concurrent execution.
|
|
186
|
+
if (method === undefined || STATIC_MCP_METHODS.has(method) || method?.startsWith?.("notifications/")) {
|
|
187
|
+
await serve(line);
|
|
188
|
+
} else if (method === "tools/call" && WORKBENCH_READ_TOOLS.has(message.params?.name)) {
|
|
189
|
+
// The card's read-only account/snapshot/change queries need not wait
|
|
190
|
+
// for a model send or verification. Keep reads ordered in their own
|
|
191
|
+
// lane; dispatch still checks account revision before and after reads,
|
|
192
|
+
// and Host snapshots retain their authorization/coherence checks.
|
|
193
|
+
appReads = appReads.then(() => failure ? undefined : serve(line)).catch((error) => {
|
|
194
|
+
failure = error;
|
|
195
|
+
stopMcp();
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
commands = commands.then(() => failure ? undefined : serve(line)).catch((error) => {
|
|
199
|
+
failure = error;
|
|
200
|
+
stopMcp();
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
await Promise.all([commands, appReads]);
|
|
205
|
+
if (failure) throw failure;
|
|
206
|
+
} finally {
|
|
207
|
+
process.removeListener("SIGINT", stopMcp);
|
|
208
|
+
process.removeListener("SIGTERM", stopMcp);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const STATIC_MCP_METHODS = new Set([
|
|
213
|
+
"initialize", "ping", "tools/list", "resources/list", "resources/templates/list", "resources/read",
|
|
214
|
+
]);
|
|
215
|
+
const WORKBENCH_READ_TOOLS = new Set([
|
|
216
|
+
OPEN_HELLOLYNK_TOOL,
|
|
217
|
+
"hellolynk_workbench_account", WORKBENCH_SNAPSHOT_TOOL, WORKBENCH_CHANGES_TOOL,
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
async function handleLine(line, output = process.stdout, runtime = {}) {
|
|
221
|
+
let message;
|
|
222
|
+
try {
|
|
223
|
+
message = JSON.parse(line);
|
|
224
|
+
} catch {
|
|
225
|
+
write(jsonRpcError(null, -32700, "parse_error"), output);
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const response = await handleJsonRpcMessage(message, runtime);
|
|
229
|
+
if (response) write(response, output);
|
|
230
|
+
return { message, response };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function handleJsonRpcMessage(message, runtime = {}) {
|
|
234
|
+
if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
235
|
+
return jsonRpcError(message?.id ?? null, -32600, "invalid_request");
|
|
236
|
+
}
|
|
237
|
+
if (message.method.startsWith("notifications/")) return null;
|
|
238
|
+
try {
|
|
239
|
+
const result = await dispatch(message.method, message.params || {}, { ...runtime,
|
|
240
|
+
executorMetadata: message.params?._meta });
|
|
241
|
+
return message.id === undefined ? null : { jsonrpc: "2.0", id: message.id, result };
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return message.id === undefined
|
|
244
|
+
? null
|
|
245
|
+
: jsonRpcError(message.id, -32603, safeErrorCode(error));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function dispatch(method, params, runtime = {}) {
|
|
250
|
+
const run = async () => {
|
|
251
|
+
const result = await dispatchWithoutModelNotices(method, params, runtime);
|
|
252
|
+
if (method === "tools/call" && !isOpenHelloLynkToolName(params.name)
|
|
253
|
+
&& neutralTools.some(tool => tool.name === params.name)) {
|
|
254
|
+
// Do not delay UI refreshes or change a send result if diagnostics fail.
|
|
255
|
+
try { captureHelloLynkNativeDeliveryStatus(await runtime.readNativeDeliveryStatus?.()); } catch {}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
};
|
|
259
|
+
return method === "tools/call" ? withHelloLynkModelNotices(run) : run();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function dispatchWithoutModelNotices(method, params, runtime = {}) {
|
|
263
|
+
const workbenchCandidate = runtimeWorkbenchCandidate(runtime);
|
|
264
|
+
const workbenchUnavailable = !workbenchCandidate;
|
|
265
|
+
if (method === "initialize") {
|
|
266
|
+
return {
|
|
267
|
+
// This server implements one protocol version; unknown offers receive
|
|
268
|
+
// that supported counteroffer instead of an arbitrary string echo.
|
|
269
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
270
|
+
capabilities: {
|
|
271
|
+
tools: { listChanged: false },
|
|
272
|
+
resources: { subscribe: false, listChanged: false },
|
|
273
|
+
},
|
|
274
|
+
serverInfo: {
|
|
275
|
+
name: "hellolynk-codex-inbox",
|
|
276
|
+
title: "HelloLynk 通讯",
|
|
277
|
+
version: packageMetadata.version,
|
|
278
|
+
},
|
|
279
|
+
instructions: conversationGuidance + "“打开 HelloLynk”时请用户点 Codex 左侧栏的 HelloLynk;要在聊天里显示卡片时调用 hellolynk_open。",
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (method === "ping") return {};
|
|
283
|
+
if (method === "tools/list") {
|
|
284
|
+
const tools = neutralTools.map((tool) => isOpenHelloLynkToolName(tool.name)
|
|
285
|
+
? workbenchCandidate ? workbenchOpenTool(tool, workbenchCandidate) : unavailableWorkbenchOpenTool(tool)
|
|
286
|
+
: tool);
|
|
287
|
+
return {
|
|
288
|
+
tools: workbenchCandidate
|
|
289
|
+
? [
|
|
290
|
+
...tools,
|
|
291
|
+
workbenchCandidateTool(workbenchCandidate),
|
|
292
|
+
...(typeof workbenchCandidate.readChanges === "function" ? [workbenchChangesToolDefinition(workbenchCandidate)] : []),
|
|
293
|
+
...(typeof workbenchCandidate.connectAccount === "function" ? [workbenchConnectTool()] : []),
|
|
294
|
+
...(workbenchCandidate.connectAccount?.readAccountState ? [workbenchAccountTool(false), workbenchAccountTool(true)] : []),
|
|
295
|
+
...(typeof workbenchCandidate.executeCommand === "function"
|
|
296
|
+
? [workbenchActionGrantTool(workbenchCandidate), workbenchCommandTool(workbenchCandidate)]
|
|
297
|
+
: []),
|
|
298
|
+
].map((tool) => withAccountRevision(tool, workbenchCandidate))
|
|
299
|
+
: tools,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (method === "tools/call") return callTool(params, runtime);
|
|
303
|
+
if (method === "resources/list") {
|
|
304
|
+
return {
|
|
305
|
+
resources: workbenchCandidate ? [{
|
|
306
|
+
uri: workbenchCandidate.resourceUri,
|
|
307
|
+
name: "HelloLynk 通讯界面",
|
|
308
|
+
title: "HelloLynk 通讯",
|
|
309
|
+
description: typeof workbenchCandidate.executeCommand === "function"
|
|
310
|
+
? "阶段 F MCP App;只显示本机 HelloLynk 报告的真实能力,缺失能力由界面隐藏或由本机 HelloLynk 拒绝。"
|
|
311
|
+
: "阶段 F 共享 HelloLynk 通讯只读 MCP App。",
|
|
312
|
+
mimeType: "text/html;profile=mcp-app",
|
|
313
|
+
}] : [],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (method === "resources/templates/list") return { resourceTemplates: [] };
|
|
317
|
+
if (method === "resources/read") {
|
|
318
|
+
if (workbenchCandidate) {
|
|
319
|
+
// Codex may retain a previous invocation's URI after a plugin upgrade.
|
|
320
|
+
// Keep advertising the current content hash for cache refresh, but serve
|
|
321
|
+
// the installed UI for this same entry's older hashes. No old UI is read.
|
|
322
|
+
const retainedWorkbenchEntry = typeof params.uri === "string"
|
|
323
|
+
&& VERSIONED_WORKBENCH_URI_RE.test(params.uri)
|
|
324
|
+
&& VERSIONED_WORKBENCH_URI_RE.test(workbenchCandidate.resourceUri);
|
|
325
|
+
if (params.uri !== workbenchCandidate.resourceUri && !retainedWorkbenchEntry) throw codeError("resource_not_found");
|
|
326
|
+
return {
|
|
327
|
+
contents: [{
|
|
328
|
+
uri: params.uri,
|
|
329
|
+
mimeType: "text/html;profile=mcp-app",
|
|
330
|
+
text: workbenchCandidate.html,
|
|
331
|
+
_meta: CODEX_WORKBENCH_RESOURCE_META,
|
|
332
|
+
}],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
throw codeError("resource_not_found");
|
|
336
|
+
}
|
|
337
|
+
throw codeError("method_not_found");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function callTool(params, runtime = {}) {
|
|
341
|
+
if (!runtime.neutralAdapter || !isModelInboxTool(params?.name)) return callToolWithoutInboxScope(params, runtime);
|
|
342
|
+
try {
|
|
343
|
+
const source = inspectCodexExecutorMetadata(runtime.executorMetadata);
|
|
344
|
+
const invocation = await prepareModelInboxInvocation(runtime.neutralAdapter, {
|
|
345
|
+
name: params.name, args: params.arguments || {},
|
|
346
|
+
caller: !source.conflict && source.threadId ? { threadId: source.threadId } : undefined,
|
|
347
|
+
});
|
|
348
|
+
return invocation.finish(await callToolWithoutInboxScope({ ...params, arguments: invocation.args },
|
|
349
|
+
{ ...runtime, neutralAdapter: invocation.adapter }));
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (params.name === 'hellolynk_reply_authorized_message') return authorizedWriteToolError(error, undefined,
|
|
352
|
+
'authorized_reply_result_unknown', error?.code === 'model_inbox_scope_invalid' ? 'input_validation' : 'preparation');
|
|
353
|
+
return nativeAddressingError(error) || toolError(safeErrorCode(error));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function callToolWithoutInboxScope(params, runtime = {}) {
|
|
358
|
+
const name = String(params?.name || "");
|
|
359
|
+
const workbenchCandidate = runtimeWorkbenchCandidate(runtime);
|
|
360
|
+
const workbenchUnavailable = !workbenchCandidate;
|
|
361
|
+
const accounts = workbenchCandidate?.connectAccount;
|
|
362
|
+
const surfaceError = name === WORKBENCH_CHANGES_TOOL ? workbenchChangesError : workbenchSurfaceToolError;
|
|
363
|
+
if (accounts?.readAccountState && ["hellolynk_workbench_account", "hellolynk_workbench_disconnect"].includes(name)) {
|
|
364
|
+
try {
|
|
365
|
+
const disconnect = name === "hellolynk_workbench_disconnect";
|
|
366
|
+
assertExactArgumentKeys(params?.arguments, disconnect ? ["revision"] : []);
|
|
367
|
+
const result = disconnect ? await accounts.disconnectAccount(params.arguments) : accounts.readAccountState();
|
|
368
|
+
return { content: [], structuredContent: { ok: true }, _meta: { hellolynkWorkbench: { [disconnect ? "disconnection" : "account"]: result } }, isError: false };
|
|
369
|
+
} catch (error) { return workbenchSurfaceToolError(error?.code || "workbench_account_unavailable", "账号操作"); }
|
|
370
|
+
}
|
|
371
|
+
const revision = params?.arguments?._hellolynkAccountRevision;
|
|
372
|
+
if (accounts?.readAccountState && [WORKBENCH_CONNECT_TOOL, WORKBENCH_SNAPSHOT_TOOL, WORKBENCH_CHANGES_TOOL, WORKBENCH_ACTION_GRANT_TOOL, WORKBENCH_COMMAND_TOOL].includes(name)) {
|
|
373
|
+
try { accounts.assertRevision(revision); }
|
|
374
|
+
catch (error) { return surfaceError(error.code, "账号检查"); }
|
|
375
|
+
const args = { ...params.arguments }; delete args._hellolynkAccountRevision;
|
|
376
|
+
params = { ...params, arguments: args };
|
|
377
|
+
}
|
|
378
|
+
const accountOperation = async (operation, mutation = false) => {
|
|
379
|
+
try {
|
|
380
|
+
if (!accounts?.readAccountState) return await operation();
|
|
381
|
+
if (mutation) return await accounts.runForAccount(revision, operation);
|
|
382
|
+
accounts.assertRevision(revision);
|
|
383
|
+
const result = await operation();
|
|
384
|
+
accounts.assertRevision(revision);
|
|
385
|
+
return result;
|
|
386
|
+
} catch (error) { return surfaceError(error?.code || "workbench_account_unavailable", "账号检查"); }
|
|
387
|
+
};
|
|
388
|
+
if (workbenchUnavailable && isOpenHelloLynkToolName(name)) {
|
|
389
|
+
return workbenchSurfaceToolError("workbench_unavailable", "启动");
|
|
390
|
+
}
|
|
391
|
+
if (workbenchCandidate && isOpenHelloLynkToolName(name)) {
|
|
392
|
+
return workbenchOpenToolCall(params?.arguments);
|
|
393
|
+
}
|
|
394
|
+
if (workbenchCandidate && name === WORKBENCH_CONNECT_TOOL && typeof workbenchCandidate.connectAccount === "function") {
|
|
395
|
+
try {
|
|
396
|
+
assertExactArgumentKeys(params?.arguments, ["hellolynkId", "instanceName"]);
|
|
397
|
+
const connection = await workbenchCandidate.connectAccount(params.arguments, revision);
|
|
398
|
+
return { content: [], structuredContent: { ok: true }, _meta: { hellolynkWorkbench: { connection } }, isError: false };
|
|
399
|
+
} catch (error) { return workbenchSurfaceToolError(error?.code || "workbench_connect_failed", "连接"); }
|
|
400
|
+
}
|
|
401
|
+
if (workbenchCandidate && name === WORKBENCH_SNAPSHOT_TOOL) {
|
|
402
|
+
return accountOperation(() => workbenchSnapshotTool(params?.arguments, workbenchCandidate));
|
|
403
|
+
}
|
|
404
|
+
if (name === WORKBENCH_CHANGES_TOOL) {
|
|
405
|
+
if (typeof workbenchCandidate?.readChanges !== "function") return workbenchChangesError("workbench_changes_unavailable");
|
|
406
|
+
return accountOperation(() => workbenchChangesToolCall(params?.arguments, workbenchCandidate));
|
|
407
|
+
}
|
|
408
|
+
if (workbenchCandidate && name === WORKBENCH_ACTION_GRANT_TOOL
|
|
409
|
+
&& typeof workbenchCandidate.issueActionGrant === "function") {
|
|
410
|
+
return accountOperation(() => workbenchActionGrantToolCall(params?.arguments, workbenchCandidate), true);
|
|
411
|
+
}
|
|
412
|
+
if (workbenchCandidate && name === WORKBENCH_COMMAND_TOOL
|
|
413
|
+
&& typeof workbenchCandidate.executeCommand === "function") {
|
|
414
|
+
return accountOperation(() => workbenchCommandToolCall(params?.arguments, workbenchCandidate), true);
|
|
415
|
+
}
|
|
416
|
+
if (name === WORKBENCH_ACTION_GRANT_TOOL || name === WORKBENCH_COMMAND_TOOL) {
|
|
417
|
+
return workbenchSurfaceToolError("workbench_action_grant_unavailable", "通讯操作");
|
|
418
|
+
}
|
|
419
|
+
const neutral = runtime.neutralAdapter;
|
|
420
|
+
if (neutral) {
|
|
421
|
+
if (name === "hellolynk_list_authorized_contacts") return neutralListAuthorizedContactsTool(params?.arguments, neutral);
|
|
422
|
+
if (name === "hellolynk_send_authorized_message") return neutralSendAuthorizedMessageTool(params?.arguments, neutral, runtime);
|
|
423
|
+
if (name === "hellolynk_list_authorized_inbox_messages") return neutralListAuthorizedInboxMessagesTool(params?.arguments, neutral);
|
|
424
|
+
if (name === "hellolynk_read_authorized_inbox_message") return neutralReadAuthorizedInboxMessageTool(params?.arguments, neutral);
|
|
425
|
+
if (name === "hellolynk_list_pending_messages") return neutralListPendingMessagesTool(params?.arguments, neutral);
|
|
426
|
+
if (name === "hellolynk_claim_message") return neutralClaimMessageTool(params?.arguments, neutral);
|
|
427
|
+
if (name === "hellolynk_reply_authorized_message") return neutralReplyAuthorizedMessageTool(params?.arguments, neutral, runtime);
|
|
428
|
+
if (name === "hellolynk_list_authorized_contact_requests") return neutralListAuthorizedContactRequestsTool(params?.arguments, neutral);
|
|
429
|
+
if (name === "hellolynk_read_authorized_contact_request") return neutralReadAuthorizedContactRequestTool(params?.arguments, neutral);
|
|
430
|
+
if (![OPEN_HELLOLYNK_TOOL, "hellolynk_get_unread_count"].includes(name)) return toolError("unknown_tool");
|
|
431
|
+
}
|
|
432
|
+
if (![OPEN_HELLOLYNK_TOOL, "hellolynk_get_unread_count"].includes(name)) return toolError("unknown_tool");
|
|
433
|
+
if (!plainEmptyObject(params?.arguments)) return toolError("arguments_not_allowed");
|
|
434
|
+
try {
|
|
435
|
+
if (name === "hellolynk_get_unread_count") return neutralGetUnreadCountTool(neutral);
|
|
436
|
+
if (!neutral) return toolError("neutral_adapter_unavailable");
|
|
437
|
+
return workbenchUnavailable
|
|
438
|
+
? workbenchSurfaceToolError("workbench_unavailable", "启动")
|
|
439
|
+
: workbenchOpenToolCall(params?.arguments);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
return toolError(safeErrorCode(error));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function runtimeWorkbenchCandidate(runtime) {
|
|
446
|
+
const value = runtime?.workbenchCandidate;
|
|
447
|
+
if (value === undefined) return null;
|
|
448
|
+
const keys = value && typeof value === "object" && !Array.isArray(value)
|
|
449
|
+
? Object.keys(value).filter((key) => !["connectAccount", "readChanges"].includes(key)).sort().join(",")
|
|
450
|
+
: "";
|
|
451
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
452
|
+
|| ![
|
|
453
|
+
"html,readSnapshot,resourceUri,validateReadRequest,validateSnapshot",
|
|
454
|
+
"commandContractId,commandSchemaVersion,commandTypes,executeCommand,html,issueActionGrant,readSnapshot,resourceUri,resultContractId,resultSchemaVersion,snapshotContractId,snapshotSchemaVersion,validateCommand,validateCommandResult,validateReadRequest,validateSnapshot",
|
|
455
|
+
].includes(keys)
|
|
456
|
+
|| typeof value.html !== "string" || value.html.length < 500 || value.html.length > 2_000_000
|
|
457
|
+
|| (Object.hasOwn(value, "connectAccount") && typeof value.connectAccount !== "function")
|
|
458
|
+
|| (Object.hasOwn(value, "readChanges") && typeof value.readChanges !== "function")
|
|
459
|
+
|| typeof value.readSnapshot !== "function"
|
|
460
|
+
|| typeof value.validateReadRequest !== "function"
|
|
461
|
+
|| typeof value.validateSnapshot !== "function"
|
|
462
|
+
|| (Object.hasOwn(value, "executeCommand") && typeof value.executeCommand !== "function")
|
|
463
|
+
|| (Object.hasOwn(value, "executeCommand") && typeof value.issueActionGrant !== "function")
|
|
464
|
+
|| (Object.hasOwn(value, "executeCommand") && value.executeCommand.length < 2)
|
|
465
|
+
|| (Object.hasOwn(value, "executeCommand") && value.issueActionGrant.length < 1)
|
|
466
|
+
|| (Object.hasOwn(value, "executeCommand") && typeof value.validateCommand !== "function")
|
|
467
|
+
|| (Object.hasOwn(value, "executeCommand") && typeof value.validateCommandResult !== "function")
|
|
468
|
+
|| (Object.hasOwn(value, "executeCommand")
|
|
469
|
+
&& (value.snapshotContractId !== "hellolynk-workbench-snapshot-v12"
|
|
470
|
+
|| value.snapshotSchemaVersion !== 12
|
|
471
|
+
|| value.commandContractId !== "hellolynk-workbench-command-v6"
|
|
472
|
+
|| value.commandSchemaVersion !== 6
|
|
473
|
+
|| value.resultContractId !== "hellolynk-workbench-command-result-v6"
|
|
474
|
+
|| value.resultSchemaVersion !== 6))
|
|
475
|
+
|| (Object.hasOwn(value, "executeCommand") && !validWorkbenchCommandTypes(value.commandTypes))
|
|
476
|
+
|| !/^ui:\/\/hellolynk\/[a-z0-9][a-z0-9._/-]{1,120}\.html$/.test(String(value.resourceUri || ""))) {
|
|
477
|
+
throw codeError("workbench_candidate_runtime_invalid");
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function validWorkbenchCommandTypes(value) {
|
|
483
|
+
return Array.isArray(value) && value.length > 0 && value.length <= 100
|
|
484
|
+
&& new Set(value).size === value.length
|
|
485
|
+
&& value.every((item) => typeof item === "string"
|
|
486
|
+
&& /^[a-z][a-z0-9.-]{2,80}$/.test(item));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function workbenchOpenTool(baseTool, candidate) {
|
|
490
|
+
return {
|
|
491
|
+
...baseTool,
|
|
492
|
+
_meta: {
|
|
493
|
+
...baseTool._meta,
|
|
494
|
+
ui: {
|
|
495
|
+
...(baseTool._meta?.ui || {}),
|
|
496
|
+
visibility: ["model", "app"],
|
|
497
|
+
resourceUri: candidate.resourceUri,
|
|
498
|
+
},
|
|
499
|
+
"openai/ui": { entrypoints: [{ type: "global" }] },
|
|
500
|
+
"openai/outputTemplate": candidate.resourceUri,
|
|
501
|
+
"openai/toolInvocation/invoking": "正在打开 HelloLynk 通讯",
|
|
502
|
+
"openai/toolInvocation/invoked": "HelloLynk 通讯已打开",
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function workbenchOpenToolCall(args) {
|
|
508
|
+
if (!plainEmptyObject(args)) return workbenchSurfaceToolError("arguments_not_allowed", "通讯启动");
|
|
509
|
+
return {
|
|
510
|
+
content: [{
|
|
511
|
+
type: "text",
|
|
512
|
+
text: "HelloLynk 卡片已就绪。",
|
|
513
|
+
}],
|
|
514
|
+
structuredContent: { ok: true, status: "ready" },
|
|
515
|
+
isError: false,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function unavailableWorkbenchOpenTool(baseTool) {
|
|
520
|
+
const {
|
|
521
|
+
"openai/outputTemplate": _legacyTemplate,
|
|
522
|
+
"openai/toolInvocation/invoking": _legacyInvoking,
|
|
523
|
+
"openai/toolInvocation/invoked": _legacyInvoked,
|
|
524
|
+
...remainingMeta
|
|
525
|
+
} = baseTool._meta || {};
|
|
526
|
+
const safeMeta = {
|
|
527
|
+
...remainingMeta,
|
|
528
|
+
ui: { visibility: ["model", "app"] },
|
|
529
|
+
};
|
|
530
|
+
return { ...baseTool, _meta: safeMeta };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isOpenHelloLynkToolName(name) {
|
|
534
|
+
return name === OPEN_HELLOLYNK_TOOL;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function workbenchSurfaceToolError(code, operation) {
|
|
538
|
+
const safe = safeErrorCode({ code });
|
|
539
|
+
return {
|
|
540
|
+
content: [{ type: "text", text: safe === "neutral_host_release_superseded" ? failureText(safe) : `HelloLynk${operation}失败:${safe}。` }],
|
|
541
|
+
structuredContent: {
|
|
542
|
+
ok: false,
|
|
543
|
+
status: "blocked",
|
|
544
|
+
errorCode: safe,
|
|
545
|
+
},
|
|
546
|
+
isError: true,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function workbenchConnectTool() {
|
|
551
|
+
return {
|
|
552
|
+
name: WORKBENCH_CONNECT_TOOL, title: "连接 HelloLynk",
|
|
553
|
+
description: "由 HelloLynk 聊天卡片发起首次连接;返回官网确认入口。确认账号设备后才建立连接。",
|
|
554
|
+
inputSchema: { type: "object", additionalProperties: false, required: ["hellolynkId", "instanceName"], properties: {
|
|
555
|
+
hellolynkId: { type: "string", pattern: "^hellolynk:(intl|cn):[a-z0-9][a-z0-9._-]{2,179}$" },
|
|
556
|
+
instanceName: { type: "string", maxLength: 80 },
|
|
557
|
+
} },
|
|
558
|
+
annotations: { readOnlyHint: false, openWorldHint: true, destructiveHint: false, idempotentHint: true },
|
|
559
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function workbenchAccountTool(disconnect) {
|
|
564
|
+
return {
|
|
565
|
+
name: disconnect ? "hellolynk_workbench_disconnect" : "hellolynk_workbench_account",
|
|
566
|
+
title: disconnect ? "解除设备连接" : "读取当前客户端连接状态",
|
|
567
|
+
description: "仅供 HelloLynk 聊天卡片使用。解除只影响当前客户端,保留旧账号资料。",
|
|
568
|
+
inputSchema: { type: "object", additionalProperties: false, required: disconnect ? ["revision"] : [],
|
|
569
|
+
properties: disconnect ? { revision: { type: "string", pattern: "^(initial|[a-f0-9]{32})$" } } : {} },
|
|
570
|
+
annotations: { readOnlyHint: !disconnect, openWorldHint: false, destructiveHint: disconnect, idempotentHint: true },
|
|
571
|
+
_meta: { ui: { visibility: ["app"] } },
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function withAccountRevision(tool, candidate) {
|
|
576
|
+
if (!candidate.connectAccount?.readAccountState
|
|
577
|
+
|| ![WORKBENCH_CONNECT_TOOL, WORKBENCH_SNAPSHOT_TOOL, WORKBENCH_CHANGES_TOOL, WORKBENCH_ACTION_GRANT_TOOL, WORKBENCH_COMMAND_TOOL].includes(tool.name)) return tool;
|
|
578
|
+
return { ...tool, inputSchema: { ...tool.inputSchema,
|
|
579
|
+
required: [...(tool.inputSchema.required || []), "_hellolynkAccountRevision"],
|
|
580
|
+
properties: { ...tool.inputSchema.properties, _hellolynkAccountRevision: { type: "string", pattern: "^(initial|[a-f0-9]{32})$" } } } };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function workbenchChangesToolDefinition(candidate) {
|
|
584
|
+
return {
|
|
585
|
+
name: WORKBENCH_CHANGES_TOOL,
|
|
586
|
+
title: "读取HelloLynk界面变化游标",
|
|
587
|
+
description: "仅供 HelloLynk MCP App 静默检查本机变化;不返回正文或事件身份,不触发模型轮次。",
|
|
588
|
+
inputSchema: { type: "object", additionalProperties: false, required: ["afterCursor"],
|
|
589
|
+
properties: { afterCursor: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER } } },
|
|
590
|
+
annotations: TOOL_ANNOTATIONS,
|
|
591
|
+
_meta: { ui: { resourceUri: candidate.resourceUri, visibility: ["app"] } },
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function workbenchChangesError(code) {
|
|
596
|
+
return { content: [], structuredContent: { ok: true },
|
|
597
|
+
_meta: { hellolynkWorkbench: { changeError: { code: safeErrorCode({ code }) } } }, isError: true };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function workbenchChangesToolCall(args, candidate) {
|
|
601
|
+
try {
|
|
602
|
+
assertExactArgumentKeys(args, ["afterCursor"]);
|
|
603
|
+
if (!Number.isSafeInteger(args?.afterCursor) || args.afterCursor < 0) throw codeError("host_app_change_cursor_invalid");
|
|
604
|
+
const batch = await candidate.readChanges({ afterCursor: args.afterCursor });
|
|
605
|
+
if (!batch || !Number.isSafeInteger(batch.cursor) || batch.cursor < 0
|
|
606
|
+
|| !/^[a-f0-9]{48}$/.test(batch.hostGeneration)
|
|
607
|
+
|| typeof batch.snapshotRequired !== "boolean" || !Array.isArray(batch.events) || batch.events.length > 256) {
|
|
608
|
+
throw codeError("workbench_changes_invalid");
|
|
609
|
+
}
|
|
610
|
+
return { content: [], structuredContent: { ok: true }, _meta: { hellolynkWorkbench: { changes: {
|
|
611
|
+
cursor: batch.cursor, hostGeneration: batch.hostGeneration,
|
|
612
|
+
snapshotRequired: batch.snapshotRequired, changed: batch.events.length > 0,
|
|
613
|
+
} } }, isError: false };
|
|
614
|
+
} catch (error) { return workbenchChangesError(error?.code || "workbench_changes_failed"); }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function workbenchCandidateTool(candidate) {
|
|
618
|
+
return {
|
|
619
|
+
name: WORKBENCH_SNAPSHOT_TOOL,
|
|
620
|
+
title: "读取HelloLynk通讯快照",
|
|
621
|
+
description: "供HelloLynk共享 MCP App 读取本机 HelloLynk 的只读快照;消息正文和联系申请正文不会进入模型可见结果,原始设备核验值不得进入 Codex 结果。",
|
|
622
|
+
inputSchema: {
|
|
623
|
+
type: "object",
|
|
624
|
+
additionalProperties: false,
|
|
625
|
+
properties: {
|
|
626
|
+
threadId: {
|
|
627
|
+
type: "string",
|
|
628
|
+
minLength: 2,
|
|
629
|
+
maxLength: 199,
|
|
630
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$",
|
|
631
|
+
},
|
|
632
|
+
threadCursor: {
|
|
633
|
+
type: "string",
|
|
634
|
+
minLength: 1,
|
|
635
|
+
maxLength: 255,
|
|
636
|
+
pattern: "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,254}$",
|
|
637
|
+
},
|
|
638
|
+
messageCursor: {
|
|
639
|
+
type: "string",
|
|
640
|
+
minLength: 1,
|
|
641
|
+
maxLength: 800,
|
|
642
|
+
pattern: "^[A-Za-z0-9._-]{1,800}$",
|
|
643
|
+
},
|
|
644
|
+
contactRequestCursor: {
|
|
645
|
+
anyOf: [
|
|
646
|
+
{ type: "null" },
|
|
647
|
+
{ type: "string", minLength: 20, maxLength: 512, pattern: "^[A-Za-z0-9_-]{20,512}$" },
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
historyBefore: { type: ["string", "null"], maxLength: 30 },
|
|
651
|
+
query: { type: "string", maxLength: 512, pattern: "^[^\\u0000]*$" },
|
|
652
|
+
filter: { type: "string", enum: ["all", "unread", "archived"] },
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
annotations: TOOL_ANNOTATIONS,
|
|
656
|
+
_meta: {
|
|
657
|
+
ui: { resourceUri: candidate.resourceUri, visibility: ["app"] },
|
|
658
|
+
"openai/outputTemplate": candidate.resourceUri,
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function workbenchCommandTool(candidate) {
|
|
664
|
+
return {
|
|
665
|
+
name: WORKBENCH_COMMAND_TOOL,
|
|
666
|
+
title: "执行HelloLynk通讯操作",
|
|
667
|
+
description: "仅供HelloLynk MCP App 转交人类界面操作;写操作携带本机 HelloLynk 已签发的一次性私有授权,只读发送状态查询不签发授权,命令正文和完整结果不会进入普通工具结果。",
|
|
668
|
+
inputSchema: {
|
|
669
|
+
type: "object",
|
|
670
|
+
additionalProperties: false,
|
|
671
|
+
required: ["command"],
|
|
672
|
+
properties: {
|
|
673
|
+
command: workbenchCommandDtoSchema(candidate),
|
|
674
|
+
_hellolynkUiActionGrant: opaqueWorkbenchActionGrantSchema(),
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
annotations: WORKBENCH_COMMAND_ANNOTATIONS,
|
|
678
|
+
_meta: { ui: { resourceUri: candidate.resourceUri, visibility: ["app"] } },
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function workbenchActionGrantTool(candidate) {
|
|
683
|
+
return {
|
|
684
|
+
name: WORKBENCH_ACTION_GRANT_TOOL,
|
|
685
|
+
title: "准备HelloLynk通讯操作授权",
|
|
686
|
+
description: "仅供HelloLynk MCP App 把当前人类界面操作交给本机 HelloLynk 审核并准备一次性私有授权;授权只返回应用私有元数据。",
|
|
687
|
+
inputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
additionalProperties: false,
|
|
690
|
+
required: ["command"],
|
|
691
|
+
properties: { command: workbenchCommandDtoSchema(candidate) },
|
|
692
|
+
},
|
|
693
|
+
annotations: LOCAL_WRITE_ANNOTATIONS,
|
|
694
|
+
_meta: { ui: { resourceUri: candidate.resourceUri, visibility: ["app"] } },
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function workbenchCommandDtoSchema(candidate) {
|
|
699
|
+
return {
|
|
700
|
+
type: "object",
|
|
701
|
+
additionalProperties: false,
|
|
702
|
+
required: [
|
|
703
|
+
"contractId", "schemaVersion", "command", "requestId",
|
|
704
|
+
"idempotencyKey", "expectedRevision", "payload",
|
|
705
|
+
],
|
|
706
|
+
properties: {
|
|
707
|
+
contractId: { const: candidate.commandContractId },
|
|
708
|
+
schemaVersion: { const: candidate.commandSchemaVersion },
|
|
709
|
+
command: { type: "string", enum: [...candidate.commandTypes] },
|
|
710
|
+
requestId: { type: "string", minLength: 2, maxLength: 199 },
|
|
711
|
+
idempotencyKey: {
|
|
712
|
+
type: "string", maxLength: 199,
|
|
713
|
+
pattern: "^wb5\\.[a-f0-9]{32}\\.[1-9][0-9]{0,15}\\.[A-Za-z0-9][A-Za-z0-9_.:-]{1,198}$",
|
|
714
|
+
},
|
|
715
|
+
expectedRevision: {
|
|
716
|
+
anyOf: [
|
|
717
|
+
{ type: "null" },
|
|
718
|
+
{ type: "string", minLength: 1, maxLength: 255 },
|
|
719
|
+
],
|
|
720
|
+
},
|
|
721
|
+
payload: { type: "object" },
|
|
722
|
+
},
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function opaqueWorkbenchActionGrantSchema() {
|
|
727
|
+
return {
|
|
728
|
+
type: "string",
|
|
729
|
+
minLength: 32,
|
|
730
|
+
maxLength: 512,
|
|
731
|
+
pattern: "^[A-Za-z0-9._~-]{32,512}$",
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Preserve only established public read errors. A code-shaped private detail
|
|
736
|
+
// is not safe merely because safeErrorCode can normalize it.
|
|
737
|
+
const WORKBENCH_SNAPSHOT_READ_ERRORS = new Set([
|
|
738
|
+
"workbench_snapshot_timeout", "workbench_connection_required", "workbench_account_changed",
|
|
739
|
+
"host_request_unauthorized", "host_request_replayed", "host_adapter_route_forbidden",
|
|
740
|
+
"host_nonce_capability_cache_full", "host_nonce_cache_full",
|
|
741
|
+
"host_app_authorization_required", "host_capability_scope_denied",
|
|
742
|
+
"host_capability_human_surface_required", "host_capability_inactive",
|
|
743
|
+
"host_capability_invalid", "host_capability_id_mismatch", "host_capability_resolver_required",
|
|
744
|
+
"host_attachment_not_found", "host_attachment_expired", "host_attachment_ambiguous",
|
|
745
|
+
"neutral_host_discovery_generation_mismatch", "neutral_host_discovery_stale",
|
|
746
|
+
"neutral_host_discovery_invalid", "neutral_host_discovery_owner_mismatch",
|
|
747
|
+
"neutral_host_discovery_unsafe_path", "neutral_host_not_found",
|
|
748
|
+
"neutral_host_launcher_unavailable", "neutral_host_adapter_unavailable", "neutral_adapter_closed",
|
|
749
|
+
"neutral_codex_binding_invalid", "neutral_codex_capability_binding_mismatch",
|
|
750
|
+
"neutral_codex_discovery_timeout", "neutral_codex_launch_failed", "neutral_codex_launch_lock_timeout",
|
|
751
|
+
"neutral_codex_runner_integrity_invalid", "neutral_codex_runner_unavailable",
|
|
752
|
+
"host_not_running", "host_app_unavailable", "host_app_action_failed", "host_app_response_invalid",
|
|
753
|
+
"host_app_workbench_snapshot_failed", "host_app_workbench_thread_not_found",
|
|
754
|
+
"host_app_workbench_cursor_invalid", "host_app_workbench_read_invalid", "host_app_state_conflict",
|
|
755
|
+
"host_app_relationship_status_unavailable", "neutral_host_release_superseded",
|
|
756
|
+
]);
|
|
757
|
+
|
|
758
|
+
function workbenchSnapshotReadError(error) {
|
|
759
|
+
const code = error?.code;
|
|
760
|
+
if (WORKBENCH_SNAPSHOT_READ_ERRORS.has(code)) return code;
|
|
761
|
+
if (code === "neutral_host_http_401") return "host_request_unauthorized";
|
|
762
|
+
if (code === "neutral_host_http_403") return "host_app_authorization_required";
|
|
763
|
+
const networkCode = code ?? error?.cause?.code;
|
|
764
|
+
if (networkCode === "ECONNREFUSED") return "neutral_host_connection_refused";
|
|
765
|
+
if (networkCode === "ECONNRESET") return "neutral_host_connection_reset";
|
|
766
|
+
return "workbench_snapshot_unavailable";
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function workbenchSnapshotTool(args, candidate) {
|
|
770
|
+
const input = args ?? {};
|
|
771
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
772
|
+
return workbenchSurfaceToolError("invalid_params", "通讯读取");
|
|
773
|
+
}
|
|
774
|
+
let readRequest;
|
|
775
|
+
try {
|
|
776
|
+
readRequest = candidate.validateReadRequest(input);
|
|
777
|
+
} catch {
|
|
778
|
+
return workbenchSurfaceToolError("invalid_params", "通讯读取");
|
|
779
|
+
}
|
|
780
|
+
let rawSnapshot;
|
|
781
|
+
try {
|
|
782
|
+
rawSnapshot = await candidate.readSnapshot(readRequest);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
return workbenchSurfaceToolError(workbenchSnapshotReadError(error), "通讯读取");
|
|
785
|
+
}
|
|
786
|
+
let snapshot;
|
|
787
|
+
try {
|
|
788
|
+
snapshot = candidate.validateSnapshot(rawSnapshot);
|
|
789
|
+
assertNoRawSafetyCodeFields(snapshot);
|
|
790
|
+
if (snapshot.source.mode !== "neutral-host" || snapshot.source.transport !== "mcp-app-rpc") {
|
|
791
|
+
throw codeError("workbench_snapshot_invalid");
|
|
792
|
+
}
|
|
793
|
+
} catch {
|
|
794
|
+
return workbenchSurfaceToolError("workbench_snapshot_invalid", "通讯读取");
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
content: [],
|
|
798
|
+
structuredContent: { ok: true },
|
|
799
|
+
_meta: { hellolynkWorkbench: { snapshot } },
|
|
800
|
+
isError: false,
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function workbenchActionGrantToolCall(args, candidate) {
|
|
805
|
+
try {
|
|
806
|
+
assertExactArgumentKeys(args, ["command"]);
|
|
807
|
+
const command = candidate.validateCommand(args.command);
|
|
808
|
+
if (command.command === "message.send-status") throw codeError("workbench_action_grant_invalid");
|
|
809
|
+
const issued = await candidate.issueActionGrant(command);
|
|
810
|
+
assertExactArgumentKeys(issued, ["actionGrant"]);
|
|
811
|
+
const actionGrant = safeOpaqueWorkbenchActionGrant(issued.actionGrant);
|
|
812
|
+
return {
|
|
813
|
+
content: [],
|
|
814
|
+
structuredContent: { ok: true },
|
|
815
|
+
_meta: { hellolynkWorkbench: { actionGrant } },
|
|
816
|
+
isError: false,
|
|
817
|
+
};
|
|
818
|
+
} catch (error) {
|
|
819
|
+
return workbenchSurfaceToolError(
|
|
820
|
+
error?.code === "workbench_action_grant_timeout"
|
|
821
|
+
? error.code
|
|
822
|
+
: "workbench_action_grant_invalid",
|
|
823
|
+
"通讯授权准备",
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function workbenchCommandToolCall(args, candidate) {
|
|
829
|
+
try {
|
|
830
|
+
const command = candidate.validateCommand(args.command);
|
|
831
|
+
const statusRead = command.command === "message.send-status";
|
|
832
|
+
assertExactArgumentKeys(args, statusRead ? ["command"] : ["command", "_hellolynkUiActionGrant"]);
|
|
833
|
+
const actionGrant = statusRead ? null : safeOpaqueWorkbenchActionGrant(args._hellolynkUiActionGrant);
|
|
834
|
+
const result = candidate.validateCommandResult(
|
|
835
|
+
await candidate.executeCommand(command, statusRead ? {} : { actionGrant }),
|
|
836
|
+
command.command,
|
|
837
|
+
);
|
|
838
|
+
assertNoRawSafetyCodeFields(result);
|
|
839
|
+
if (result.requestId !== command.requestId) throw codeError("workbench_candidate_command_result_invalid");
|
|
840
|
+
return {
|
|
841
|
+
content: [],
|
|
842
|
+
structuredContent: { ok: true },
|
|
843
|
+
_meta: { hellolynkWorkbench: { commandResult: result } },
|
|
844
|
+
isError: false,
|
|
845
|
+
};
|
|
846
|
+
} catch {
|
|
847
|
+
return workbenchSurfaceToolError("workbench_command_invalid", "通讯操作");
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function neutralGetUnreadCountTool(adapter) {
|
|
852
|
+
if (!adapter || typeof adapter.readUnreadCount !== "function") {
|
|
853
|
+
return toolError("neutral_adapter_unavailable");
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
const result = await adapter.readUnreadCount();
|
|
857
|
+
const unreadCount = result?.unreadCount;
|
|
858
|
+
if (!Number.isSafeInteger(unreadCount) || unreadCount < 0) {
|
|
859
|
+
return toolError("neutral_unread_count_invalid");
|
|
860
|
+
}
|
|
861
|
+
const unreadMessages = unreadCount;
|
|
862
|
+
return {
|
|
863
|
+
content: [{ type: "text", text: resultText.unread(unreadMessages) }],
|
|
864
|
+
structuredContent: {
|
|
865
|
+
ok: true,
|
|
866
|
+
configured: true,
|
|
867
|
+
status: "running",
|
|
868
|
+
counts: { unreadMessages },
|
|
869
|
+
},
|
|
870
|
+
isError: false,
|
|
871
|
+
};
|
|
872
|
+
} catch (error) {
|
|
873
|
+
return toolError(safeErrorCode(error));
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async function neutralListAuthorizedContactsTool(args, adapter) {
|
|
878
|
+
try {
|
|
879
|
+
const result = await findContacts(args, query => adapter.listAuthorizedContacts(query));
|
|
880
|
+
const contacts = Array.isArray(result?.contacts) ? result.contacts.map((contact) => ({
|
|
881
|
+
contactId: safeIdentifier(contact?.contactId, "contact_id_invalid", 200),
|
|
882
|
+
displayName: safeIdentifier(contact?.displayName, "contact_display_name_invalid", 160),
|
|
883
|
+
recipientDelegateId: safeIdentifier(contact?.recipientDelegateId, "recipient_delegate_id_invalid", 200),
|
|
884
|
+
instanceDisplayName: safeIdentifier(contact?.instanceDisplayName, "instance_display_name_invalid", 160),
|
|
885
|
+
})) : [];
|
|
886
|
+
return {
|
|
887
|
+
content: [{ type: "text", text: resultText.contacts(contacts.length) }],
|
|
888
|
+
structuredContent: { contacts, ...(result.nativeConversations ? { nativeConversations: result.nativeConversations } : {}) },
|
|
889
|
+
isError: false,
|
|
890
|
+
};
|
|
891
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function neutralSendAuthorizedMessageTool(args, adapter, runtime) {
|
|
895
|
+
let clientMessageId;
|
|
896
|
+
let phase = "input_validation";
|
|
897
|
+
try {
|
|
898
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) throw codeError("arguments_invalid");
|
|
899
|
+
assertExactArgumentKeys(args, sendArgumentKeys(args));
|
|
900
|
+
if (Object.values(args).some((value) => typeof value !== "string")) throw codeError("arguments_invalid");
|
|
901
|
+
if (!Object.hasOwn(args, "contact")) {
|
|
902
|
+
authorizedWriteIdentifier(args.contactId, "contact_id_invalid");
|
|
903
|
+
authorizedWriteIdentifier(args.recipientDelegateId, "recipient_delegate_id_invalid");
|
|
904
|
+
}
|
|
905
|
+
optionalTarget(args);
|
|
906
|
+
clientMessageId = authorizedWriteIdentifier(args.clientMessageId, "client_message_id_invalid");
|
|
907
|
+
const text = args.text;
|
|
908
|
+
if (!text || text.includes("\u0000") || [...text].length > 20_000 || Buffer.byteLength(text, "utf8") > 20 * 1024) {
|
|
909
|
+
throw codeError("message_text_invalid");
|
|
910
|
+
}
|
|
911
|
+
phase = "preparation";
|
|
912
|
+
const { contactId, recipientDelegateId } = await sendRecipient(args, query => adapter.listAuthorizedContacts(query));
|
|
913
|
+
await runtime?.prepareAuthorizedWrite?.(runtime.executorMetadata, { clientMessageId, peerDelegateId: recipientDelegateId });
|
|
914
|
+
phase = "adapter_call";
|
|
915
|
+
const result = await adapter.sendAuthorizedMessage({ contactId, recipientDelegateId, clientMessageId, text, ...optionalTarget(args) });
|
|
916
|
+
phase = "response_validation";
|
|
917
|
+
if (result?.accepted !== true || result?.ok === false || result?.authorization !== "contact_delegate_allowlist"
|
|
918
|
+
|| result?.clientMessageId !== clientMessageId || result?.contactId !== contactId
|
|
919
|
+
|| !["accepted", "delivered_to_live_connection", "acknowledged_by_recipient_local_store"].includes(result?.status)) throw codeError("send_result_invalid");
|
|
920
|
+
const receipt = firstSendReceipt(args, result.status);
|
|
921
|
+
return {
|
|
922
|
+
content: [{ type: "text", text: receipt.text }],
|
|
923
|
+
structuredContent: {
|
|
924
|
+
accepted: true,
|
|
925
|
+
clientMessageId,
|
|
926
|
+
contactId,
|
|
927
|
+
recipientDelegateId,
|
|
928
|
+
status: result.status,
|
|
929
|
+
destination: receipt.destination,
|
|
930
|
+
},
|
|
931
|
+
isError: false,
|
|
932
|
+
};
|
|
933
|
+
} catch (error) { return authorizedWriteToolError(error, clientMessageId, "authorized_send_result_unknown", phase); }
|
|
934
|
+
}
|
|
935
|
+
async function neutralListAuthorizedInboxMessagesTool(args, adapter) {
|
|
936
|
+
const older = args != null && typeof args === "object" && !Array.isArray(args) && Object.hasOwn(args, "before");
|
|
937
|
+
if (!older && !plainEmptyObject(args)) return toolError("arguments_not_allowed");
|
|
938
|
+
try {
|
|
939
|
+
if (older) assertExactArgumentKeys(args, ["before"]);
|
|
940
|
+
const result = await adapter.listAuthorizedInboxMessages(older
|
|
941
|
+
? { before: safeIdentifier(args.before, "authorized_inbox_message_id_invalid", 240) } : {});
|
|
942
|
+
const messages = Array.isArray(result?.messages)
|
|
943
|
+
? result.messages.map(value => ({ ...projectAuthorizedInboxMetadata(value), ...replyHint(value) }))
|
|
944
|
+
: [];
|
|
945
|
+
return {
|
|
946
|
+
content: [{ type: "text", text: resultText.inbox(messages.length, older) }],
|
|
947
|
+
structuredContent: { messages },
|
|
948
|
+
isError: false,
|
|
949
|
+
};
|
|
950
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async function neutralReadAuthorizedInboxMessageTool(args, adapter) {
|
|
954
|
+
try {
|
|
955
|
+
assertExactArgumentKeys(args, ["localMessageId", "conversationKey"]);
|
|
956
|
+
const message = projectAuthorizedInboxRead(await adapter.readAuthorizedInboxMessage(
|
|
957
|
+
safeIdentifier(args.localMessageId, "authorized_inbox_message_id_invalid", 240),
|
|
958
|
+
safeIdentifier(args.conversationKey, "authorized_inbox_conversation_key_invalid", 240),
|
|
959
|
+
));
|
|
960
|
+
return { content: [{ type: "text", text: JSON.stringify(message) }], structuredContent: message, isError: false };
|
|
961
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function neutralListPendingMessagesTool(args, adapter) {
|
|
965
|
+
if (!plainEmptyObject(args)) return toolError("arguments_not_allowed");
|
|
966
|
+
try {
|
|
967
|
+
const result = await adapter.listPendingMessages();
|
|
968
|
+
if (!Array.isArray(result?.messages) || result.messages.length > 1000) throw codeError("pending_messages_invalid");
|
|
969
|
+
const messages = result.messages.map((value) => {
|
|
970
|
+
if (value?.wakeEligible !== true) throw codeError("pending_message_ineligible");
|
|
971
|
+
return projectAuthorizedInboxMetadata(value);
|
|
972
|
+
});
|
|
973
|
+
return { content: [{ type: "text", text: resultText.pending(messages.length) }], structuredContent: { messages }, isError: false };
|
|
974
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function neutralClaimMessageTool(args, adapter) {
|
|
978
|
+
try {
|
|
979
|
+
if (args?.batchId != null) {
|
|
980
|
+
assertExactArgumentKeys(args, ["batchId"]);
|
|
981
|
+
const batchId = safeIdentifier(args.batchId, "authorized_inbox_batch_id_invalid", 128);
|
|
982
|
+
const result = await adapter.claimBatch(batchId);
|
|
983
|
+
if (!Array.isArray(result?.messages) || result.claimed !== (result.messages.length > 0)
|
|
984
|
+
|| !Number.isSafeInteger(result.expiredCount) || result.expiredCount < 0) throw codeError("claim_result_invalid");
|
|
985
|
+
const messages = result.messages.map(projectAuthorizedInboxRead);
|
|
986
|
+
if (messages.reduce((n, m) => n + Array.from(m.text).length, 0) > 20000) throw codeError("claim_result_invalid");
|
|
987
|
+
const output = { claimed: result.claimed, messages, expiredCount: result.expiredCount,
|
|
988
|
+
notice: result.claimed ? (result.expiredCount ? resultText.claimed(messages.length, result.expiredCount) : null) : "这批来信已读取,无需重复读取。" };
|
|
989
|
+
return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output, isError: false };
|
|
990
|
+
}
|
|
991
|
+
assertExactArgumentKeys(args, ["localMessageId", "conversationKey"]);
|
|
992
|
+
const localMessageId = safeIdentifier(args.localMessageId, "authorized_inbox_message_id_invalid", 240);
|
|
993
|
+
const conversationKey = safeIdentifier(args.conversationKey, "authorized_inbox_conversation_key_invalid", 240);
|
|
994
|
+
const result = await adapter.claimMessage(localMessageId, conversationKey);
|
|
995
|
+
if (result?.claimed === false && result.claimId === null && result.message === null) {
|
|
996
|
+
return { content: [{ type: "text", text: "此来信已被领取或处理。" }], structuredContent: { claimed: false, claimId: null, message: null }, isError: false };
|
|
997
|
+
}
|
|
998
|
+
if (result?.claimed !== true || !/^inclaim_[a-f0-9]{48}$/.test(result.claimId)) throw codeError("claim_result_invalid");
|
|
999
|
+
const message = projectAuthorizedInboxRead(result.message);
|
|
1000
|
+
if (message.localMessageId !== localMessageId || message.conversationKey !== conversationKey
|
|
1001
|
+
|| message.source !== "hellolynk_external_message"
|
|
1002
|
+
|| !projectInboxReadState(message) || message.wakeEligible !== true) throw codeError("claim_result_invalid");
|
|
1003
|
+
return { content: [{ type: "text", text: JSON.stringify({ claimId: result.claimId, message }) }],
|
|
1004
|
+
structuredContent: { claimed: true, claimId: result.claimId, message }, isError: false };
|
|
1005
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async function neutralReplyAuthorizedMessageTool(args, adapter, runtime) {
|
|
1009
|
+
let clientMessageId;
|
|
1010
|
+
let phase = "input_validation";
|
|
1011
|
+
try {
|
|
1012
|
+
assertExactArgumentKeys(args, ["localMessageId", "conversationKey", "clientMessageId", "text"]);
|
|
1013
|
+
if (Object.values(args).some((value) => typeof value !== "string")) throw codeError("arguments_invalid");
|
|
1014
|
+
const localMessageId = authorizedWriteIdentifier(args.localMessageId, "authorized_inbox_message_id_invalid");
|
|
1015
|
+
const conversationKey = authorizedWriteIdentifier(args.conversationKey, "authorized_inbox_conversation_key_invalid");
|
|
1016
|
+
clientMessageId = authorizedWriteIdentifier(args.clientMessageId, "client_message_id_invalid");
|
|
1017
|
+
const text = args.text;
|
|
1018
|
+
if (!text || text.includes("\u0000") || [...text].length > 20_000 || Buffer.byteLength(text, "utf8") > 20 * 1024) throw codeError("message_text_invalid");
|
|
1019
|
+
phase = "preparation";
|
|
1020
|
+
await adapter.assertModelInboxReply({ localMessageId, conversationKey });
|
|
1021
|
+
await runtime?.prepareAuthorizedWrite?.(runtime.executorMetadata, { clientMessageId, conversationKey });
|
|
1022
|
+
phase = "adapter_call";
|
|
1023
|
+
const result = await adapter.replyAuthorizedMessage({ localMessageId, conversationKey, clientMessageId, text });
|
|
1024
|
+
phase = "response_validation";
|
|
1025
|
+
if (result?.accepted !== true || result?.ok === false || result?.authorization !== "contact_delegate_allowlist"
|
|
1026
|
+
|| result?.clientMessageId !== clientMessageId
|
|
1027
|
+
|| !["accepted", "delivered_to_live_connection", "acknowledged_by_recipient_local_store"].includes(result?.status)) throw codeError("reply_result_invalid");
|
|
1028
|
+
const status = safeIdentifier(result.status, "send_status_invalid", 80);
|
|
1029
|
+
return { content: [{ type: "text", text: resultText.sent(status, clientMessageId) }],
|
|
1030
|
+
structuredContent: { accepted: true, clientMessageId,
|
|
1031
|
+
contactId: safeIdentifier(result.contactId, "contact_id_invalid", 200), status }, isError: false };
|
|
1032
|
+
} catch (error) { return authorizedWriteToolError(error, clientMessageId, "authorized_reply_result_unknown", phase); }
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async function neutralListAuthorizedContactRequestsTool(args, adapter) {
|
|
1036
|
+
if (!plainEmptyObject(args)) return toolError("arguments_not_allowed");
|
|
1037
|
+
try {
|
|
1038
|
+
const result = await adapter.listAuthorizedContactRequests();
|
|
1039
|
+
const requests = (result?.requests || []).map(projectAuthorizedContactRequestMetadata);
|
|
1040
|
+
return { content: [{ type: "text", text: `联系申请 ${requests.length} 条。` }], structuredContent: { requests }, isError: false };
|
|
1041
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async function neutralReadAuthorizedContactRequestTool(args, adapter) {
|
|
1045
|
+
try {
|
|
1046
|
+
assertExactArgumentKeys(args, ["localRecordId", "conversationKey"]);
|
|
1047
|
+
const request = projectAuthorizedContactRequestRead(await adapter.readAuthorizedContactRequest(
|
|
1048
|
+
safeIdentifier(args.localRecordId, "authorized_contact_request_id_invalid", 240),
|
|
1049
|
+
safeIdentifier(args.conversationKey, "authorized_contact_request_conversation_key_invalid", 240),
|
|
1050
|
+
));
|
|
1051
|
+
return { content: [{ type: "text", text: JSON.stringify(request) }], structuredContent: request, isError: false };
|
|
1052
|
+
} catch (error) { return toolError(safeErrorCode(error)); }
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function projectAuthorizedInboxMetadata(value) {
|
|
1056
|
+
const output = {
|
|
1057
|
+
localMessageId: safeIdentifier(value?.localMessageId, "authorized_inbox_message_id_invalid", 240),
|
|
1058
|
+
conversationKey: safeIdentifier(value?.conversationKey, "authorized_inbox_conversation_key_invalid", 240),
|
|
1059
|
+
localDelegateId: safeIdentifier(value?.localDelegateId, "authorized_inbox_local_delegate_invalid", 200),
|
|
1060
|
+
peerDelegateId: safeIdentifier(value?.peerDelegateId, "authorized_inbox_peer_delegate_invalid", 200),
|
|
1061
|
+
messageType: safeIdentifier(value?.messageType || "text", "authorized_inbox_message_type_invalid", 80),
|
|
1062
|
+
unread: value?.unread === true,
|
|
1063
|
+
};
|
|
1064
|
+
if (typeof value?.wakeEligible === "boolean") output.wakeEligible = value.wakeEligible;
|
|
1065
|
+
if (value?.automaticDeliveryState === "expired") output.automaticDeliveryState = "expired";
|
|
1066
|
+
for (const [key, maximum] of [
|
|
1067
|
+
["contactId", 200], ["displayName", 160], ["peerInstanceDisplayName", 160], ["clientMessageId", 200],
|
|
1068
|
+
["createdAt", 80], ["updatedAt", 80],
|
|
1069
|
+
]) {
|
|
1070
|
+
if (value?.[key] != null) output[key] = safeIdentifier(value[key], `authorized_inbox_${key}_invalid`, maximum);
|
|
1071
|
+
}
|
|
1072
|
+
return output;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function projectAuthorizedInboxRead(value) {
|
|
1076
|
+
if (!projectInboxReadState(value) || value?.externalContentIsUntrustedData !== true) throw codeError("host_app_response_invalid");
|
|
1077
|
+
return { ...projectAuthorizedInboxMetadata(value), peerPrincipalId: safeIdentifier(value?.peerPrincipalId, "authorized_inbox_peer_principal_invalid", 200), text: safeExternalBody(value?.text, true), source: requireExternalSource(value?.source, "hellolynk_external_message"), ...projectInboxReadState(value), ...replyHint(value) };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function projectAuthorizedContactRequestMetadata(value) {
|
|
1081
|
+
return { localRecordId: safeIdentifier(value?.localRecordId, "authorized_contact_request_id_invalid", 240), conversationKey: safeIdentifier(value?.conversationKey, "authorized_contact_request_conversation_key_invalid", 240), localDelegateId: safeIdentifier(value?.localDelegateId, "authorized_contact_request_local_delegate_invalid", 200), sender: { hellolynkId: safeIdentifier(value?.sender?.hellolynkId, "authorized_contact_request_sender_invalid", 200), verifiedDisplayName: safeIdentifier(value?.sender?.verifiedDisplayName, "authorized_contact_request_sender_name_invalid", 160) }, state: safeIdentifier(value?.state, "authorized_contact_request_state_invalid", 80), receivedAt: safeIdentifier(value?.receivedAt, "authorized_contact_request_time_invalid", 80) };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function projectAuthorizedContactRequestRead(value) {
|
|
1085
|
+
if (value?.externalContentIsUntrustedData !== true) throw codeError("host_app_response_invalid");
|
|
1086
|
+
return { ...projectAuthorizedContactRequestMetadata(value), introText: safeContactRequestIntro(value?.introText), source: requireExternalSource(value?.source, "hellolynk_contact_request"), readDoesNotMarkRead: value?.readDoesNotMarkRead === true };
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function strictConversationSchema(id) {
|
|
1090
|
+
const message = id === "localMessageId";
|
|
1091
|
+
return { type: "object", additionalProperties: false, required: [id, "conversationKey"], properties: {
|
|
1092
|
+
[id]: { type: "string", minLength: 3, maxLength: 240, ...(message ? { description: paramText.localMessageId } : {}) },
|
|
1093
|
+
conversationKey: { type: "string", minLength: 3, maxLength: 240, ...(message ? { description: paramText.conversationKey } : {}) } } };
|
|
1094
|
+
}
|
|
1095
|
+
function safeContactRequestIntro(value) { return value === "" ? "" : safeExternalBody(value); }
|
|
1096
|
+
function safeExternalBody(value, countCodePoints = false) { const text = String(value ?? ""); if (!text || text.includes("\u0000") || (countCodePoints ? Array.from(text).length : text.length) > 20000 || Buffer.byteLength(text, "utf8") > 20 * 1024) throw codeError("external_content_text_invalid"); return text; }
|
|
1097
|
+
function requireExternalSource(value, expected) { if (value !== expected) throw codeError("external_content_source_invalid"); return expected; }
|
|
1098
|
+
|
|
1099
|
+
function toolError(code) {
|
|
1100
|
+
const nativeError = nativeAddressingError({ code });
|
|
1101
|
+
if (nativeError) return nativeError;
|
|
1102
|
+
const safe = safeErrorCode({ code });
|
|
1103
|
+
const deleted = safe === "host_app_reply_target_deleted";
|
|
1104
|
+
return {
|
|
1105
|
+
content: [{ type: "text", text: failureText(safe) }],
|
|
1106
|
+
structuredContent: { ok: false, configured: true, status: deleted ? "not_delivered" : "blocked", errorCode: safe,
|
|
1107
|
+
...(deleted ? { automaticRetryAllowed: false } : {}) },
|
|
1108
|
+
isError: true,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function authorizedWriteToolError(error, clientMessageId, timeoutCode, phase) {
|
|
1112
|
+
const nativeError = nativeAddressingError(error, { clientMessageId, phase });
|
|
1113
|
+
if (nativeError) return nativeError;
|
|
1114
|
+
// A timeout after the send began is an unknown result, whatever code it carries.
|
|
1115
|
+
const timedOut = clientMessageId && ["adapter_call", "response_validation"].includes(phase) && error?.name === "TimeoutError";
|
|
1116
|
+
const code = timedOut ? timeoutCode : safeErrorCode(error);
|
|
1117
|
+
// Phase and retry policy come only from this invocation, never from an adapter error.
|
|
1118
|
+
const { text, ...outcome } = writeFailure(code, phase);
|
|
1119
|
+
return { content: [{ type: "text", text }], structuredContent: { ok: false, configured: true, errorCode: code, ...outcome,
|
|
1120
|
+
phase, ...(clientMessageId ? { clientMessageId } : {}) }, isError: true };
|
|
1121
|
+
}
|
|
1122
|
+
function authorizedWriteIdentifier(value, code) { if (typeof value !== "string" || !WRITE_ID_PATTERN.test(value)) throw codeError(code); return value; }
|
|
1123
|
+
function safeIdentifier(value, code, maximum) {
|
|
1124
|
+
const normalized = String(value ?? "").trim();
|
|
1125
|
+
if (!normalized || normalized.length > maximum || /[\r\n\u0000-\u001f\u007f]/.test(normalized)) {
|
|
1126
|
+
throw codeError(code);
|
|
1127
|
+
}
|
|
1128
|
+
return normalized;
|
|
1129
|
+
}function safeOpaqueWorkbenchActionGrant(value) {
|
|
1130
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9._~-]{32,512}$/.test(value)) {
|
|
1131
|
+
throw codeError("workbench_action_grant_invalid");
|
|
1132
|
+
}
|
|
1133
|
+
return value;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function assertNoRawSafetyCodeFields(value, seen = new WeakSet(), depth = 0) {
|
|
1137
|
+
if (!value || typeof value !== "object") return;
|
|
1138
|
+
if (depth > 32 || seen.has(value)) throw codeError("workbench_snapshot_invalid");
|
|
1139
|
+
seen.add(value);
|
|
1140
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1141
|
+
const normalized = String(key).toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1142
|
+
if (normalized === "safetycode" || normalized === "localdevicesafetycode") {
|
|
1143
|
+
throw codeError("workbench_raw_safety_code_forbidden");
|
|
1144
|
+
}
|
|
1145
|
+
assertNoRawSafetyCodeFields(nested, seen, depth + 1);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function assertExactArgumentKeys(value, expected) {
|
|
1150
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw codeError("arguments_invalid");
|
|
1151
|
+
const actual = Object.keys(value).sort();
|
|
1152
|
+
const wanted = [...expected].sort();
|
|
1153
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
1154
|
+
throw codeError("arguments_invalid");
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function plainEmptyObject(value) {
|
|
1159
|
+
if (value === undefined || value === null) return true;
|
|
1160
|
+
return typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function safeErrorCode(error) {
|
|
1164
|
+
const value = String(error?.code || error?.message || "inbox_error");
|
|
1165
|
+
const normalized = value
|
|
1166
|
+
.replace(/[^A-Za-z0-9._:-]+/g, "_")
|
|
1167
|
+
.replace(/_+/g, "_")
|
|
1168
|
+
.replace(/^_+|_+$/g, "")
|
|
1169
|
+
.slice(0, 120);
|
|
1170
|
+
return normalized || "inbox_error";
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function codeError(code) {
|
|
1174
|
+
const error = new Error(code);
|
|
1175
|
+
error.code = code;
|
|
1176
|
+
return error;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function jsonRpcError(id, code, message) {
|
|
1180
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function write(message, output = process.stdout) {
|
|
1184
|
+
output.write(`${JSON.stringify(message)}\n`);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1188
|
+
if (isMain) {
|
|
1189
|
+
runStdioMcpServer().catch((error) => {
|
|
1190
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: safeErrorCode(error) })}\n`);
|
|
1191
|
+
process.exitCode = 1;
|
|
1192
|
+
});
|
|
1193
|
+
}
|