@hellolynk-ai/openclaw-channel 0.0.0-stage → 0.2.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +92 -0
- package/README.md +86 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/clients/core/hellolynk-agent-configurator.mjs +701 -0
- package/clients/core/hellolynk-client-account.mjs +180 -0
- package/clients/core/hellolynk-client-core.mjs +770 -0
- package/clients/core/hellolynk-client-profile.mjs +191 -0
- package/clients/core/hellolynk-client-update-entry.mjs +31 -0
- package/clients/core/hellolynk-client-update-runtime.mjs +600 -0
- package/clients/core/hellolynk-client-update.mjs +453 -0
- package/clients/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/clients/core/hellolynk-device-enrollment.mjs +435 -0
- package/clients/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/clients/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/clients/core/hellolynk-host-auth.mjs +347 -0
- package/clients/core/hellolynk-host-protected-store.mjs +2721 -0
- package/clients/core/hellolynk-host-transport-client.mjs +946 -0
- package/clients/core/hellolynk-host.mjs +2374 -0
- package/clients/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/clients/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/clients/core/hellolynk-inbox-read-state.mjs +11 -0
- package/clients/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/clients/core/hellolynk-instance-conversation.mjs +185 -0
- package/clients/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/clients/core/hellolynk-local-contacts.mjs +1228 -0
- package/clients/core/hellolynk-local-data-migration.mjs +766 -0
- package/clients/core/hellolynk-local-data-protection.mjs +747 -0
- package/clients/core/hellolynk-local-identity-trust.mjs +274 -0
- package/clients/core/hellolynk-local-messaging.mjs +3043 -0
- package/clients/core/hellolynk-local-peer-transport.mjs +103 -0
- package/clients/core/hellolynk-local-process-lock.mjs +157 -0
- package/clients/core/hellolynk-model-inbox.mjs +230 -0
- package/clients/core/hellolynk-model-notices.cjs +60 -0
- package/clients/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/clients/core/hellolynk-native-conversations.mjs +280 -0
- package/clients/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/clients/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/clients/core/hellolynk-native-notice.mjs +50 -0
- package/clients/core/hellolynk-native-reply-routes.mjs +96 -0
- package/clients/core/hellolynk-native-worker-lease.mjs +84 -0
- package/clients/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/clients/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/clients/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/clients/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/clients/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/clients/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/clients/core/hellolynk-platform-credential-store.mjs +773 -0
- package/clients/core/hellolynk-relay-client-cli.mjs +232 -0
- package/clients/core/hellolynk-release-retention.mjs +160 -0
- package/clients/core/hellolynk-runtime-safety.cjs +471 -0
- package/clients/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/clients/core/hellolynk-workbench-connection.mjs +234 -0
- package/clients/platform/windows/configure-agent.ps1 +281 -0
- package/clients/platform/windows/credential-store.ps1 +844 -0
- package/clients/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/clients/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/clients/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/clients/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/clients/workbench/contract.mjs +1314 -0
- package/clients/workbench/service.mjs +485 -0
- package/hellolynk-client-pack-manifest.json +854 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/integrations/openclaw-channel/dist/control-ui/index.js +63 -0
- package/integrations/openclaw-channel/index.mjs +5 -0
- package/integrations/openclaw-channel/openclaw.plugin.json +57 -0
- package/integrations/openclaw-channel/package.json +50 -0
- package/integrations/openclaw-channel/setup-entry.mjs +4 -0
- package/integrations/openclaw-channel/skills/hellolynk-messaging/SKILL.md +9 -0
- package/integrations/openclaw-channel/src/authorized-messaging-tools.mjs +423 -0
- package/integrations/openclaw-channel/src/channel-dispatch.mjs +657 -0
- package/integrations/openclaw-channel/src/channel.mjs +368 -0
- package/integrations/openclaw-channel/src/config-schema.mjs +33 -0
- package/integrations/openclaw-channel/src/control-ui.mjs +411 -0
- package/integrations/openclaw-channel/src/inbound-policy.mjs +44 -0
- package/integrations/openclaw-channel/src/native-origins.mjs +53 -0
- package/integrations/openclaw-channel/src/neutral-host-adapter.mjs +826 -0
- package/integrations/openclaw-channel/src/neutral-read-runtime.mjs +469 -0
- package/integrations/openclaw-channel/src/runtime-primitives.mjs +12 -0
- package/integrations/openclaw-channel/src/transcript-attribution.mjs +140 -0
- package/package.json +69 -4
- package/runtime/connect-activate.mjs +837 -0
- package/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/runtime/core/hellolynk-client-account.mjs +180 -0
- package/runtime/core/hellolynk-client-core.mjs +770 -0
- package/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/runtime/core/hellolynk-client-update.mjs +453 -0
- package/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/runtime/core/hellolynk-host.mjs +2374 -0
- package/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/runtime/core/hellolynk-release-retention.mjs +160 -0
- package/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/runtime/neutral-host-bootstrap.mjs +57 -0
- package/runtime/neutral-host-release-entry.mjs +81 -0
- package/runtime/neutral-host-release-loader.mjs +156 -0
- package/runtime/neutral-host-release-manifest.json +302 -0
- package/runtime/neutral-host-release-pin.json +4 -0
- package/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/runtime/platform/windows/credential-store.ps1 +844 -0
- package/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/runtime/workbench/contract.mjs +1314 -0
- package/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/runtime/workbench/host-capabilities.mjs +80 -0
- package/runtime/workbench/i18n.mjs +1127 -0
- package/runtime/workbench/loopback-surface.mjs +740 -0
- package/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/runtime/workbench/service.mjs +485 -0
- package/runtime/workbench/state.mjs +421 -0
- package/runtime/workbench/transports.mjs +171 -0
- package/runtime/workbench/ui/app.mjs +6120 -0
- package/runtime/workbench/ui/connection-form.mjs +155 -0
- package/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/runtime/workbench/ui/http-transport.mjs +247 -0
- package/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/runtime/workbench/ui/icons/close.svg +3 -0
- package/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/runtime/workbench/ui/icons/search.svg +3 -0
- package/runtime/workbench/ui/icons/security.svg +3 -0
- package/runtime/workbench/ui/icons/settings.svg +3 -0
- package/runtime/workbench/ui/index.html +139 -0
- package/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/runtime/workbench/ui/styles.css +686 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { workbenchConnectionError } from "../../../clients/core/hellolynk-workbench-connection.mjs";
|
|
3
|
+
import {
|
|
4
|
+
HELLOLYNK_WORKBENCH_COMMAND_CONTRACT,
|
|
5
|
+
HELLOLYNK_WORKBENCH_RESULT_CONTRACT,
|
|
6
|
+
HELLOLYNK_WORKBENCH_SCHEMA_VERSION,
|
|
7
|
+
HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT,
|
|
8
|
+
HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION,
|
|
9
|
+
validateWorkbenchCommand,
|
|
10
|
+
validateWorkbenchCommandResult,
|
|
11
|
+
validateWorkbenchSnapshot,
|
|
12
|
+
} from "../../../clients/workbench/contract.mjs";
|
|
13
|
+
|
|
14
|
+
const PANEL_BASE_PATH = "/plugins/hellolynk/inbox";
|
|
15
|
+
const WORKBENCH_CANDIDATE_PATH = "/plugins/hellolynk/workbench-candidate";
|
|
16
|
+
const ACTION_GRANT = /^[A-Za-z0-9._~-]{32,512}$/;
|
|
17
|
+
const F3_SHARED_CONTRACT_AVAILABLE = HELLOLYNK_WORKBENCH_SNAPSHOT_CONTRACT === "hellolynk-workbench-snapshot-v12"
|
|
18
|
+
&& HELLOLYNK_WORKBENCH_SNAPSHOT_SCHEMA_VERSION === 12
|
|
19
|
+
&& HELLOLYNK_WORKBENCH_COMMAND_CONTRACT === "hellolynk-workbench-command-v6"
|
|
20
|
+
&& HELLOLYNK_WORKBENCH_RESULT_CONTRACT === "hellolynk-workbench-command-result-v6"
|
|
21
|
+
&& HELLOLYNK_WORKBENCH_SCHEMA_VERSION === 6;
|
|
22
|
+
|
|
23
|
+
export function registerHelloLynkWorkbenchControlUi(api, options = {}) {
|
|
24
|
+
if (api?.registrationMode !== "full") return false;
|
|
25
|
+
const workbenchProvider = options.workbenchProvider;
|
|
26
|
+
if (typeof workbenchProvider !== "function") {
|
|
27
|
+
throw codeError("openclaw_workbench_provider_required");
|
|
28
|
+
}
|
|
29
|
+
const controls = api?.session?.controls;
|
|
30
|
+
if (typeof controls?.registerControlUiDescriptor !== "function") {
|
|
31
|
+
throw codeError("openclaw_control_ui_descriptor_api_unavailable");
|
|
32
|
+
}
|
|
33
|
+
if (typeof api.registerHttpRoute !== "function") {
|
|
34
|
+
throw codeError("openclaw_http_route_api_unavailable");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
controls.registerControlUiDescriptor({
|
|
38
|
+
surface: "tab",
|
|
39
|
+
id: "hellolynk-inbox",
|
|
40
|
+
label: "HelloLynk 通讯",
|
|
41
|
+
description: "本地消息与联系人界面;共享 Host 权限、安全与设置合同",
|
|
42
|
+
icon: "message-circle",
|
|
43
|
+
path: PANEL_BASE_PATH,
|
|
44
|
+
group: "agent",
|
|
45
|
+
order: 86,
|
|
46
|
+
requiredScopes: ["operator.read"],
|
|
47
|
+
});
|
|
48
|
+
api.registerHttpRoute({
|
|
49
|
+
path: PANEL_BASE_PATH,
|
|
50
|
+
auth: "gateway",
|
|
51
|
+
match: "prefix",
|
|
52
|
+
handler: createWorkbenchCandidateRedirectHandler(
|
|
53
|
+
workbenchProvider,
|
|
54
|
+
PANEL_BASE_PATH,
|
|
55
|
+
"openclaw_workbench_unavailable",
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
// Native browser pages use the authenticated host bridge. The generic
|
|
59
|
+
// external-tab sandbox has an opaque origin and cannot run our cookie-based
|
|
60
|
+
// loopback UI. Do not relax its CORS or copy Gateway credentials into it.
|
|
61
|
+
if (typeof api.registerGatewayMethod === "function") {
|
|
62
|
+
api.registerGatewayMethod("hellolynk.workbench.open", async ({ params, client, respond }) => {
|
|
63
|
+
try {
|
|
64
|
+
if (!params || Object.keys(params).length !== 0) throw codeError("openclaw_workbench_input_invalid");
|
|
65
|
+
const browser = client?.browserOrigin;
|
|
66
|
+
if (browser?.isLocalClient !== true || !browser.origin || !browser.requestHost) {
|
|
67
|
+
throw codeError("openclaw_workbench_local_browser_required");
|
|
68
|
+
}
|
|
69
|
+
const parent = assertGatewayOrigin(browser.origin);
|
|
70
|
+
if (parent.host !== browser.requestHost) throw codeError("openclaw_workbench_gateway_origin_invalid");
|
|
71
|
+
const candidate = validateWorkbenchCandidate(await workbenchProvider({ gatewayOrigin: parent.origin }));
|
|
72
|
+
const armed = await candidate.armNavigation();
|
|
73
|
+
if (armed?.cleanUrl !== candidate.cleanUrl || !Number.isSafeInteger(armed.expiresAtMs)
|
|
74
|
+
|| armed.expiresAtMs <= Date.now()) throw codeError("openclaw_workbench_candidate_rearm_invalid");
|
|
75
|
+
respond(true, { url: armed.cleanUrl }, undefined);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
respond(false, undefined, { code: "UNAVAILABLE", message: "HelloLynk page unavailable",
|
|
78
|
+
details: { reason: /^openclaw_workbench_[a-z_]+$/.test(error?.code || "")
|
|
79
|
+
? error.code : "openclaw_workbench_unavailable" } });
|
|
80
|
+
}
|
|
81
|
+
}, { scope: "operator.write" });
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createOpenClawWorkbenchAdapterFacade(adapter) {
|
|
87
|
+
if (!adapter || typeof adapter.readWorkbenchSnapshot !== "function") {
|
|
88
|
+
throw codeError("openclaw_workbench_host_adapter_invalid");
|
|
89
|
+
}
|
|
90
|
+
const hostOwnsActionGrantPair = F3_SHARED_CONTRACT_AVAILABLE
|
|
91
|
+
&& typeof adapter.issueWorkbenchActionGrant === "function"
|
|
92
|
+
&& adapter.issueWorkbenchActionGrant.length >= 1
|
|
93
|
+
&& typeof adapter.executeWorkbenchCommand === "function"
|
|
94
|
+
&& adapter.executeWorkbenchCommand.length >= 2;
|
|
95
|
+
const facade = {
|
|
96
|
+
async readSnapshot(input = {}) {
|
|
97
|
+
try { return validateWorkbenchSnapshot(await adapter.readWorkbenchSnapshot(input)); }
|
|
98
|
+
catch (error) { throw workbenchConnectionError(error); }
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
if (typeof adapter.readWorkbenchChanges === "function") {
|
|
102
|
+
facade.readChanges = (input) => adapter.readWorkbenchChanges(input);
|
|
103
|
+
}
|
|
104
|
+
if (hostOwnsActionGrantPair) {
|
|
105
|
+
facade.issueActionGrant = async (command) => {
|
|
106
|
+
const validated = validateWorkbenchCommand(command);
|
|
107
|
+
return exactActionGrantEnvelope(await adapter.issueWorkbenchActionGrant(validated));
|
|
108
|
+
};
|
|
109
|
+
facade.executeCommand = async (command, context) => {
|
|
110
|
+
const validated = validateWorkbenchCommand(command);
|
|
111
|
+
const normalizedContext = validated.command === "message.send-status"
|
|
112
|
+
? exactEmptyContext(context)
|
|
113
|
+
: exactActionGrantContext(context);
|
|
114
|
+
return validateWorkbenchCommandResult(
|
|
115
|
+
await adapter.executeWorkbenchCommand(validated, normalizedContext),
|
|
116
|
+
validated.command,
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze(facade);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function exactActionGrantEnvelope(value) {
|
|
124
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
125
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
126
|
+
|| Object.keys(value).length !== 1 || !Object.hasOwn(value, "actionGrant")
|
|
127
|
+
|| typeof value.actionGrant !== "string" || !ACTION_GRANT.test(value.actionGrant)) {
|
|
128
|
+
throw codeError("openclaw_workbench_action_grant_invalid");
|
|
129
|
+
}
|
|
130
|
+
return Object.freeze({ actionGrant: value.actionGrant });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function exactActionGrantContext(value) {
|
|
134
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
135
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
136
|
+
|| Object.keys(value).length !== 1 || !Object.hasOwn(value, "actionGrant")
|
|
137
|
+
|| typeof value.actionGrant !== "string" || !ACTION_GRANT.test(value.actionGrant)) {
|
|
138
|
+
throw codeError("openclaw_workbench_action_grant_required");
|
|
139
|
+
}
|
|
140
|
+
return Object.freeze({ actionGrant: value.actionGrant });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function exactEmptyContext(value) {
|
|
144
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
145
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))
|
|
146
|
+
|| Object.keys(value).length !== 0) {
|
|
147
|
+
throw codeError("openclaw_workbench_action_grant_invalid");
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze({});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createOpenClawWorkbenchRuntime(options = {}) {
|
|
153
|
+
return createOpenClawWorkbenchRuntimeInternal(
|
|
154
|
+
options,
|
|
155
|
+
"hellolynk-openclaw-workbench",
|
|
156
|
+
"openclaw_workbench_not_started",
|
|
157
|
+
true,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resolveOpenClawWorkbenchParentOrigin(config) {
|
|
162
|
+
const gateway = config?.gateway;
|
|
163
|
+
// Saved configuration is a fallback for callers outside the HTTP route.
|
|
164
|
+
// The route derives the actual origin from its authenticated Gateway request.
|
|
165
|
+
if (gateway?.port === undefined || (gateway.bind !== undefined && gateway.bind !== "loopback")
|
|
166
|
+
|| gateway.tls?.enabled === true) return null;
|
|
167
|
+
if (!Number.isSafeInteger(gateway.port) || gateway.port < 1 || gateway.port > 65_535) {
|
|
168
|
+
throw codeError("openclaw_workbench_gateway_port_invalid");
|
|
169
|
+
}
|
|
170
|
+
return new URL(`http://localhost:${gateway.port}`).origin;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function createOpenClawWorkbenchCandidateRuntime(options = {}) {
|
|
174
|
+
return createOpenClawWorkbenchRuntimeInternal(
|
|
175
|
+
options,
|
|
176
|
+
"hellolynk-openclaw-workbench-candidate",
|
|
177
|
+
"openclaw_workbench_candidate_not_started",
|
|
178
|
+
false,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createOpenClawWorkbenchRuntimeInternal(
|
|
183
|
+
options,
|
|
184
|
+
serviceId,
|
|
185
|
+
notStartedCode,
|
|
186
|
+
bootstrapOnProvider,
|
|
187
|
+
) {
|
|
188
|
+
if (typeof options.adapterProvider !== "function") {
|
|
189
|
+
throw codeError("openclaw_workbench_adapter_provider_required");
|
|
190
|
+
}
|
|
191
|
+
let adapter = null;
|
|
192
|
+
const candidates = new Map();
|
|
193
|
+
const bootstrapPromises = new Map();
|
|
194
|
+
let adapterPromise = null;
|
|
195
|
+
let serviceStarted = false;
|
|
196
|
+
|
|
197
|
+
async function bootstrap(parentOrigin = options.trustedParentOrigin ?? null) {
|
|
198
|
+
const key = parentOrigin || "standalone";
|
|
199
|
+
if (candidates.has(key)) return candidates.get(key);
|
|
200
|
+
if (!bootstrapPromises.has(key)) {
|
|
201
|
+
const pending = (async () => {
|
|
202
|
+
adapterPromise ||= Promise.resolve().then(options.adapterProvider);
|
|
203
|
+
try { adapter = await adapterPromise; }
|
|
204
|
+
catch (error) { adapterPromise = null; throw error; }
|
|
205
|
+
const { bootstrapWorkbenchCandidate, workbenchRoot } = await loadWorkbenchCandidateBootstrap(options.releaseRoot);
|
|
206
|
+
const started = await bootstrapWorkbenchCandidate("openclaw", {
|
|
207
|
+
adapter: createOpenClawWorkbenchAdapterFacade(adapter),
|
|
208
|
+
workbenchRoot,
|
|
209
|
+
connectAccount: options.connectAccount,
|
|
210
|
+
...(options.port === undefined ? {} : { port: options.port }),
|
|
211
|
+
...(options.pagePath === undefined ? {} : { pagePath: options.pagePath }),
|
|
212
|
+
allowSameSiteTopLevelNavigation: true,
|
|
213
|
+
trustedParentOrigin: parentOrigin,
|
|
214
|
+
});
|
|
215
|
+
candidates.set(key, started);
|
|
216
|
+
return started;
|
|
217
|
+
})();
|
|
218
|
+
bootstrapPromises.set(key, pending);
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
return await bootstrapPromises.get(key);
|
|
222
|
+
} finally {
|
|
223
|
+
bootstrapPromises.delete(key);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function start() {
|
|
228
|
+
serviceStarted = true;
|
|
229
|
+
if (bootstrapOnProvider) return undefined;
|
|
230
|
+
try {
|
|
231
|
+
return await bootstrap();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
serviceStarted = false;
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function stop() {
|
|
239
|
+
serviceStarted = false;
|
|
240
|
+
await Promise.allSettled([...bootstrapPromises.values()]);
|
|
241
|
+
const current = [...candidates.values()];
|
|
242
|
+
candidates.clear();
|
|
243
|
+
bootstrapPromises.clear();
|
|
244
|
+
const currentAdapter = adapter;
|
|
245
|
+
adapter = null;
|
|
246
|
+
adapterPromise = null;
|
|
247
|
+
try {
|
|
248
|
+
await Promise.all(current.map((candidate) => candidate.close()));
|
|
249
|
+
} finally {
|
|
250
|
+
if (currentAdapter && typeof currentAdapter.close === "function") await currentAdapter.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const workbenchProvider = async (context = {}) => {
|
|
255
|
+
if (!serviceStarted) throw codeError(notStartedCode);
|
|
256
|
+
// This hint comes only from our authenticated Gateway HTTP route. It uses
|
|
257
|
+
// the actual listener port and request hostname, independent of the page
|
|
258
|
+
// path, query, default port, or a CLI override of the saved Gateway port.
|
|
259
|
+
const parentOrigin = context.gatewayOrigin ?? options.trustedParentOrigin ?? null;
|
|
260
|
+
if (parentOrigin != null) assertGatewayOrigin(parentOrigin);
|
|
261
|
+
if (candidates.size + bootstrapPromises.size >= 3
|
|
262
|
+
&& !candidates.has(parentOrigin || "standalone") && !bootstrapPromises.has(parentOrigin || "standalone")) {
|
|
263
|
+
throw codeError("openclaw_workbench_gateway_origin_invalid");
|
|
264
|
+
}
|
|
265
|
+
const current = bootstrapOnProvider || context.gatewayOrigin
|
|
266
|
+
? await bootstrap(parentOrigin)
|
|
267
|
+
: candidates.get(parentOrigin || "standalone");
|
|
268
|
+
if (!serviceStarted || !current) throw codeError(notStartedCode);
|
|
269
|
+
return Object.freeze({
|
|
270
|
+
cleanUrl: current.cleanUrl,
|
|
271
|
+
armNavigation: () => current.surface.armNavigation(),
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
workbenchProvider,
|
|
276
|
+
candidateProvider: workbenchProvider,
|
|
277
|
+
service: Object.freeze({
|
|
278
|
+
id: serviceId,
|
|
279
|
+
start,
|
|
280
|
+
stop,
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function loadWorkbenchCandidateBootstrap(releaseRoot) {
|
|
286
|
+
for (const [specifier, relativeRoot] of [
|
|
287
|
+
["../../../clients/workbench/candidate-bootstrap.mjs", "clients/workbench"],
|
|
288
|
+
["../../../runtime/workbench/candidate-bootstrap.mjs", "runtime/workbench"],
|
|
289
|
+
]) {
|
|
290
|
+
try {
|
|
291
|
+
const loaded = await import(specifier);
|
|
292
|
+
if (typeof loaded.bootstrapWorkbenchCandidate === "function") {
|
|
293
|
+
return {
|
|
294
|
+
bootstrapWorkbenchCandidate: loaded.bootstrapWorkbenchCandidate,
|
|
295
|
+
workbenchRoot: releaseRoot ? path.join(releaseRoot, relativeRoot) : undefined,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error?.code !== "ERR_MODULE_NOT_FOUND") throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw codeError("openclaw_workbench_runtime_unavailable");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function codeError(code) {
|
|
306
|
+
const error = new Error(code);
|
|
307
|
+
error.code = code;
|
|
308
|
+
return error;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function validateWorkbenchCandidate(value) {
|
|
312
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
313
|
+
|| Object.keys(value).sort().join(",") !== "armNavigation,cleanUrl"
|
|
314
|
+
|| typeof value.armNavigation !== "function") {
|
|
315
|
+
throw new Error("openclaw_workbench_candidate_invalid");
|
|
316
|
+
}
|
|
317
|
+
const url = new URL(String(value.cleanUrl || ""));
|
|
318
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(url.hostname) || Number(url.port || 80) < 1
|
|
319
|
+
|| url.pathname !== "/workbench" || url.search || url.hash || url.username || url.password) {
|
|
320
|
+
throw new Error("openclaw_workbench_candidate_url_invalid");
|
|
321
|
+
}
|
|
322
|
+
return Object.freeze({
|
|
323
|
+
cleanUrl: url.href.replace(/\/$/, ""),
|
|
324
|
+
armNavigation: value.armNavigation,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function createWorkbenchCandidateRedirectHandler(
|
|
329
|
+
candidateProvider,
|
|
330
|
+
routePath = WORKBENCH_CANDIDATE_PATH,
|
|
331
|
+
unavailableCode = "openclaw_workbench_candidate_unavailable",
|
|
332
|
+
) {
|
|
333
|
+
return async (req, res) => {
|
|
334
|
+
const url = new URL(req.url || WORKBENCH_CANDIDATE_PATH, "http://127.0.0.1");
|
|
335
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
336
|
+
if (String(req.method || "GET").toUpperCase() !== "GET" || pathname !== routePath) {
|
|
337
|
+
res.statusCode = 404;
|
|
338
|
+
res.setHeader("cache-control", "no-store");
|
|
339
|
+
res.end();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
if (req.headers?.["sec-fetch-dest"] === "iframe") {
|
|
343
|
+
// The host's default external-tab sandbox omits allow-same-origin.
|
|
344
|
+
// Explain the official native-page prerequisite instead of consuming a
|
|
345
|
+
// loopback navigation and leaving a broken frame behind.
|
|
346
|
+
send(res, 200, "text/html; charset=utf-8", "<!doctype html><html lang=\"zh-CN\"><meta charset=\"utf-8\"><title>HelloLynk</title><body><h2>HelloLynk 通讯</h2><p>要在这里显示 HelloLynk,请打开 设置 → Labs → Custom plugin UI,开启后重新点左侧“HelloLynk 通讯”。也可以直接对 AI 说「打开 HelloLynk」。</p><p>To show HelloLynk here, enable Settings → Labs → Custom plugin UI, then click “HelloLynk 通讯” in the left sidebar again. You can also ask your AI to open HelloLynk.</p></body></html>");
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
let candidate;
|
|
350
|
+
try {
|
|
351
|
+
candidate = validateWorkbenchCandidate(await candidateProvider(gatewayRequestContext(req)));
|
|
352
|
+
} catch {
|
|
353
|
+
sendJson(res, 503, { ok: false, error: unavailableCode });
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
let armed;
|
|
357
|
+
try {
|
|
358
|
+
armed = await candidate.armNavigation();
|
|
359
|
+
} catch {
|
|
360
|
+
sendJson(res, 503, { ok: false, error: unavailableCode });
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
if (!armed || typeof armed !== "object" || Array.isArray(armed)
|
|
364
|
+
|| armed.cleanUrl !== candidate.cleanUrl
|
|
365
|
+
|| !Number.isSafeInteger(armed.expiresAtMs) || armed.expiresAtMs <= Date.now()) {
|
|
366
|
+
throw new Error("openclaw_workbench_candidate_rearm_invalid");
|
|
367
|
+
}
|
|
368
|
+
res.statusCode = 302;
|
|
369
|
+
res.setHeader("location", armed.cleanUrl);
|
|
370
|
+
res.setHeader("cache-control", "no-store");
|
|
371
|
+
res.setHeader("content-security-policy", "default-src 'none'; frame-ancestors 'self'; base-uri 'none'");
|
|
372
|
+
res.setHeader("referrer-policy", req.headers?.["sec-fetch-dest"] === "iframe" ? "origin" : "no-referrer");
|
|
373
|
+
res.end();
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function assertGatewayOrigin(value) {
|
|
379
|
+
let url;
|
|
380
|
+
try { url = new URL(value); } catch { throw codeError("openclaw_workbench_gateway_origin_invalid"); }
|
|
381
|
+
if (url.protocol !== "http:" || !["localhost", "127.0.0.1"].includes(url.hostname)
|
|
382
|
+
|| url.username || url.password || url.origin !== value) {
|
|
383
|
+
throw codeError("openclaw_workbench_gateway_origin_invalid");
|
|
384
|
+
}
|
|
385
|
+
return url;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function gatewayRequestContext(req) {
|
|
389
|
+
if (!req.headers?.host) return {};
|
|
390
|
+
const url = new URL(`http://${req.headers.host}`);
|
|
391
|
+
assertGatewayOrigin(url.origin);
|
|
392
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash
|
|
393
|
+
|| (req.socket?.localPort != null && Number(url.port || 80) !== req.socket.localPort)
|
|
394
|
+
|| req.socket?.encrypted === true) {
|
|
395
|
+
throw codeError("openclaw_workbench_gateway_origin_invalid");
|
|
396
|
+
}
|
|
397
|
+
return { gatewayOrigin: url.origin };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function send(res, statusCode, contentType, body) {
|
|
401
|
+
res.statusCode = statusCode;
|
|
402
|
+
res.setHeader("content-type", contentType);
|
|
403
|
+
res.setHeader("cache-control", "no-store");
|
|
404
|
+
res.setHeader("x-content-type-options", "nosniff");
|
|
405
|
+
res.setHeader("content-security-policy", "default-src 'none'; style-src 'self'; form-action 'self'; frame-ancestors 'self'; base-uri 'none'");
|
|
406
|
+
res.end(body);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function sendJson(res, statusCode, value) {
|
|
410
|
+
send(res, statusCode, "application/json; charset=utf-8", JSON.stringify(value));
|
|
411
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const DEV_TRUSTED_IDENTITY_PROVENANCE_STATES = new Set([
|
|
2
|
+
"legacy_compat_identity_projection"
|
|
3
|
+
]);
|
|
4
|
+
|
|
5
|
+
export function classifyDevInboundPolicy(identity, trustedPrincipalIds = []) {
|
|
6
|
+
const principalId = normalizeIdentityId(identity?.senderPrincipalId);
|
|
7
|
+
const delegateId = normalizeIdentityId(identity?.senderDelegateId);
|
|
8
|
+
const provenance = normalizeProtocolValue(identity?.identityProvenanceState);
|
|
9
|
+
if (!principalId) return pendingPolicy("sender_principal_missing");
|
|
10
|
+
if (!delegateId) return pendingPolicy("sender_delegate_missing");
|
|
11
|
+
if (principalId === delegateId) return pendingPolicy("sender_identity_collapsed");
|
|
12
|
+
if (!DEV_TRUSTED_IDENTITY_PROVENANCE_STATES.has(provenance)) {
|
|
13
|
+
return pendingPolicy("sender_identity_provenance_untrusted");
|
|
14
|
+
}
|
|
15
|
+
const trusted = new Set(normalizeDevTrustedSenderPrincipalIds(trustedPrincipalIds));
|
|
16
|
+
if (!trusted.has(principalId)) return pendingPolicy("sender_principal_not_dev_trusted");
|
|
17
|
+
// Trust makes a sender eligible for a later, explicit local authorization.
|
|
18
|
+
// It never turns an ordinary remote chat message into model input by itself.
|
|
19
|
+
return pendingPolicy("sender_principal_dev_trusted_requires_human");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeDevTrustedSenderPrincipalIds(value) {
|
|
23
|
+
if (!Array.isArray(value)) throw new Error("devTrustedSenderPrincipalIds must be an array");
|
|
24
|
+
const normalized = value.map((item) => {
|
|
25
|
+
if (typeof item !== "string" || !item.trim()) {
|
|
26
|
+
throw new Error("devTrustedSenderPrincipalIds must contain non-empty strings");
|
|
27
|
+
}
|
|
28
|
+
return item.trim();
|
|
29
|
+
});
|
|
30
|
+
return [...new Set(normalized)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function pendingPolicy(reason) {
|
|
34
|
+
return { decision: "pending_human_confirmation", reason };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeIdentityId(value) {
|
|
38
|
+
if (value === undefined || value === null) return "";
|
|
39
|
+
return String(value).trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeProtocolValue(value) {
|
|
43
|
+
return typeof value === "string" ? value : "";
|
|
44
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { rememberNativeReplyRoute, readNativeReplyRoute } from "../../../clients/core/hellolynk-native-reply-routes.mjs";
|
|
2
|
+
|
|
3
|
+
const AGENT = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
4
|
+
const SESSION = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
5
|
+
|
|
6
|
+
export function openClawModelInboxCaller(context) {
|
|
7
|
+
if (!AGENT.test(context?.agentId || "") || !SESSION.test(context?.sessionId || "")
|
|
8
|
+
|| typeof context.sessionKey !== "string" || !context.sessionKey.startsWith(`agent:${context.agentId}:`)
|
|
9
|
+
|| /:subagent:/.test(context.sessionKey)) return undefined;
|
|
10
|
+
context.assertInvocationCurrent?.();
|
|
11
|
+
return { threadId: `${context.agentId}:${context.sessionId}` };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// sessionKey can be reused after /new or reset. Bind to the native session UUID,
|
|
15
|
+
// supplied by the host tool factory, so a later conversation cannot inherit mail.
|
|
16
|
+
export async function rememberOpenClawOrigin(adapter, context, message) {
|
|
17
|
+
if (!AGENT.test(context?.agentId || "") || !SESSION.test(context?.sessionId || "")) return false;
|
|
18
|
+
if (typeof context.sessionKey !== "string" || !context.sessionKey.startsWith(`agent:${context.agentId}:`)
|
|
19
|
+
|| /:subagent:/.test(context.sessionKey)) return false;
|
|
20
|
+
try {
|
|
21
|
+
context.assertInvocationCurrent?.();
|
|
22
|
+
await rememberNativeReplyRoute({ ...adapter.binding, adapterId: "openclaw" }, {
|
|
23
|
+
clientMessageId: message.clientMessageId, threadId: `${context.agentId}:${context.sessionId}`,
|
|
24
|
+
peerDelegateId: message.recipientDelegateId || message.peerDelegateId,
|
|
25
|
+
...(message.conversationKey ? { conversationKey: message.conversationKey } : {}),
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch { return false; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function resolveOpenClawOrigin(ctx, adapter, message) {
|
|
32
|
+
if (!message.replyToClientMessageId) return null;
|
|
33
|
+
const route = await readNativeReplyRoute({ ...adapter.binding, adapterId: "openclaw" }, message);
|
|
34
|
+
const [agentId, sessionId, extra] = (route?.threadId || "").split(":");
|
|
35
|
+
if (extra || !AGENT.test(agentId || "") || !SESSION.test(sessionId || "")) throw unavailable();
|
|
36
|
+
if (typeof ctx.hellolynkResolveSessionKeyById !== "function" || typeof ctx.hellolynkGetSessionEntry !== "function") throw unavailable();
|
|
37
|
+
const storePath = ctx.channelRuntime.session.resolveStorePath(ctx.cfg?.session?.store, { agentId });
|
|
38
|
+
const sessionKey = await ctx.hellolynkResolveSessionKeyById({ agentId, sessionId, storePath });
|
|
39
|
+
if (typeof sessionKey !== "string" || !sessionKey.startsWith(`agent:${agentId}:`) || /:subagent:/.test(sessionKey)) throw unavailable();
|
|
40
|
+
const origin = { threadId: route.threadId, agentId, sessionId, sessionKey, storePath };
|
|
41
|
+
await assertOpenClawOriginCurrent(ctx, origin);
|
|
42
|
+
if (typeof ctx.hellolynkActiveSessionRun !== "function" || ctx.hellolynkActiveSessionRun(sessionKey)) throw unavailable();
|
|
43
|
+
return origin;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function assertOpenClawOriginCurrent(ctx, origin) {
|
|
47
|
+
const entry = await ctx.hellolynkGetSessionEntry({ agentId: origin.agentId, sessionKey: origin.sessionKey,
|
|
48
|
+
storePath: origin.storePath, readConsistency: "latest", hydrateSkillPromptRefs: false });
|
|
49
|
+
if (!entry || entry.sessionId !== origin.sessionId || entry.archivedAt != null) throw unavailable();
|
|
50
|
+
return entry;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unavailable() { return Object.assign(new Error("OpenClaw reply origin is unavailable"), { code: "openclaw_reply_origin_unavailable" }); }
|