@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,826 @@
|
|
|
1
|
+
import { queryArgs, optionalTarget } from "../../../clients/core/hellolynk-native-addressing-tools.cjs";
|
|
2
|
+
import { captureHelloLynkModelNotices } from "../../../clients/core/hellolynk-model-notices.cjs";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { HELLOLYNK_HOST_CAPABILITY_CONTRACT } from "../../../clients/core/hellolynk-host-auth.mjs";
|
|
6
|
+
import {
|
|
7
|
+
createHostProtectedStore,
|
|
8
|
+
HELLOLYNK_OPENCLAW_NEUTRAL_EXACT_SCOPES,
|
|
9
|
+
} from "../../../clients/core/hellolynk-host-protected-store.mjs";
|
|
10
|
+
import { HELLOLYNK_HOST_PATHS } from "../../../clients/core/hellolynk-host.mjs";
|
|
11
|
+
import {
|
|
12
|
+
validateWorkbenchCommand,
|
|
13
|
+
validateWorkbenchCommandResult,
|
|
14
|
+
} from "../../../clients/workbench/contract.mjs";
|
|
15
|
+
|
|
16
|
+
export const HELLOLYNK_OPENCLAW_NEUTRAL_ADAPTER_CONTRACT = "hellolynk-openclaw-neutral-adapter-v3";
|
|
17
|
+
export const OPENCLAW_NEUTRAL_EXACT_SCOPES = Object.freeze([
|
|
18
|
+
...HELLOLYNK_OPENCLAW_NEUTRAL_EXACT_SCOPES,
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{2,198}$/;
|
|
22
|
+
const ACTION_GRANT = /^[A-Za-z0-9._~-]{32,512}$/;
|
|
23
|
+
const FROZEN_WORKBENCH_ACTION_GRANT_PATH = "/host/v1/app/workbench/action-grants";
|
|
24
|
+
const WORKBENCH_ACTION_GRANT_PATH = Object.hasOwn(HELLOLYNK_HOST_PATHS, "appWorkbenchActionGrant")
|
|
25
|
+
? HELLOLYNK_HOST_PATHS.appWorkbenchActionGrant
|
|
26
|
+
: FROZEN_WORKBENCH_ACTION_GRANT_PATH;
|
|
27
|
+
if (WORKBENCH_ACTION_GRANT_PATH !== FROZEN_WORKBENCH_ACTION_GRANT_PATH) {
|
|
28
|
+
throw codeError("neutral_host_workbench_action_grant_path_mismatch");
|
|
29
|
+
}
|
|
30
|
+
const BINDING_REF = "protected-store:openclaw";
|
|
31
|
+
const SAFE_DISCOVERY_ERRORS = new Set([
|
|
32
|
+
"ENOENT", "neutral_host_discovery_stale", "neutral_host_discovery_generation_mismatch",
|
|
33
|
+
]);
|
|
34
|
+
// Another client, or a newer release, is starting the Host: wait for it instead of failing.
|
|
35
|
+
const LAUNCH_YIELD_ERRORS = new Set(["neutral_host_already_running", "neutral_host_release_superseded"]);
|
|
36
|
+
// A release handover is on hold: wait for the newer Host like already_running, report "switching" only if none appears.
|
|
37
|
+
const RELEASE_SUPERSEDED = "neutral_host_release_superseded";
|
|
38
|
+
const TERMINAL_HOST_ERRORS = new Set([
|
|
39
|
+
"neutral_host_discovery_generation_mismatch", "neutral_host_discovery_stale",
|
|
40
|
+
"neutral_host_not_found", "host_not_running", "host_request_unauthorized",
|
|
41
|
+
"host_attachment_not_found", "host_attachment_expired", "ECONNREFUSED",
|
|
42
|
+
]);
|
|
43
|
+
export function isOpenClawNeutralHostUnavailable(error) {
|
|
44
|
+
return TERMINAL_HOST_ERRORS.has(error?.code) || TERMINAL_HOST_ERRORS.has(error?.cause?.code);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createOpenClawProtectedBindingProvider(options = {}) {
|
|
48
|
+
const store = options.protectedStore || createHostProtectedStore({
|
|
49
|
+
...(options.storeRoot ? { root: options.storeRoot } : {}),
|
|
50
|
+
...(options.installationStore ? { installationStore: options.installationStore } : {}),
|
|
51
|
+
...(options.helperPath ? { helperPath: options.helperPath } : {}),
|
|
52
|
+
});
|
|
53
|
+
return async function resolveOpenClawBinding() {
|
|
54
|
+
return store.resolveOpenClawNeutralBinding(options.bindingQuery || {});
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function probeOpenClawLegacyStoresReadOnly(paths = []) {
|
|
59
|
+
try {
|
|
60
|
+
const { probeLegacyStoresReadOnly } = await loadNeutralHostProcess();
|
|
61
|
+
return await probeLegacyStoresReadOnly(paths);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw codeError(error?.code || "neutral_host_legacy_probe_failed");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function createOpenClawNeutralHostAdapter(options = {}) {
|
|
68
|
+
if (options.bindingRef != null && options.bindingRef !== BINDING_REF) {
|
|
69
|
+
throw codeError("neutral_openclaw_binding_ref_invalid");
|
|
70
|
+
}
|
|
71
|
+
if (typeof options.capabilityProvider !== "function") {
|
|
72
|
+
throw codeError("neutral_capability_provider_required");
|
|
73
|
+
}
|
|
74
|
+
const hostRuntime = await loadNeutralHostProcess();
|
|
75
|
+
const binding = validateOpenClawProtectedBinding(await options.capabilityProvider());
|
|
76
|
+
const discover = options.discoverHost || hostRuntime.discoverHelloLynkNeutralHost;
|
|
77
|
+
const request = options.requestHost || hostRuntime.requestHelloLynkNeutralHost;
|
|
78
|
+
const requestHandover = options.requestHostHandover || hostRuntime.requestHelloLynkNeutralHostHandover;
|
|
79
|
+
const readTimeoutMs = integer(options.readTimeoutMs, 8_000, 100, 15_000);
|
|
80
|
+
const optionalReadTimeoutMs = integer(options.optionalReadTimeoutMs, 4_000, 100, 10_000);
|
|
81
|
+
const cacheTtlMs = integer(options.cacheTtlMs, 10_000, 100, 60_000);
|
|
82
|
+
const now = typeof options.clock === "function" ? options.clock : Date.now;
|
|
83
|
+
const discoverInput = publicBinding(binding);
|
|
84
|
+
let discovery = await safeDiscover(discover, discoverInput);
|
|
85
|
+
// A Host of an earlier release is asked to make room, and this client's release starts in its place.
|
|
86
|
+
if (discovery && typeof options.hostLauncher === "function"
|
|
87
|
+
&& await earlierHostLeft(requestHandover, binding, integer(options.handoverWaitMs, 5_000, 0, 30_000))) {
|
|
88
|
+
discovery = await safeDiscover(discover, discoverInput);
|
|
89
|
+
}
|
|
90
|
+
let startedHost = false;
|
|
91
|
+
if (!discovery) {
|
|
92
|
+
if (typeof options.hostLauncher !== "function") throw codeError("neutral_host_launcher_unavailable");
|
|
93
|
+
// Another client may have started the shared Host a moment ago: wait for it instead of failing.
|
|
94
|
+
let superseded = false;
|
|
95
|
+
try { await options.hostLauncher(launchBinding(binding)); startedHost = true; }
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!LAUNCH_YIELD_ERRORS.has(error?.code)) throw error;
|
|
98
|
+
superseded = error.code === RELEASE_SUPERSEDED;
|
|
99
|
+
}
|
|
100
|
+
discovery = await waitForDiscovery(discover, discoverInput, options);
|
|
101
|
+
if (!discovery && superseded) throw codeError(RELEASE_SUPERSEDED);
|
|
102
|
+
}
|
|
103
|
+
if (!discovery) throw codeError("neutral_host_not_found");
|
|
104
|
+
assertDiscovery(
|
|
105
|
+
discovery,
|
|
106
|
+
binding,
|
|
107
|
+
hostRuntime.HELLOLYNK_NEUTRAL_HOST_DISCOVERY_CONTRACT,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const adapterInstanceId = safeId(
|
|
111
|
+
options.adapterInstanceId || `openclaw-${process.pid}-${crypto.randomBytes(12).toString("hex")}`,
|
|
112
|
+
"neutral_host_adapter_instance_invalid",
|
|
113
|
+
);
|
|
114
|
+
const callRaw = async (method, pathname, body = null, requestOptions = {}) => captureHelloLynkModelNotices(await request({
|
|
115
|
+
discovery,
|
|
116
|
+
capability: binding.capability,
|
|
117
|
+
method,
|
|
118
|
+
pathname,
|
|
119
|
+
body,
|
|
120
|
+
timeoutMs: readTimeoutMs,
|
|
121
|
+
...requestOptions,
|
|
122
|
+
}));
|
|
123
|
+
const attached = requireOk(await callRaw("POST", HELLOLYNK_HOST_PATHS.attachmentAttach, { adapterInstanceId }));
|
|
124
|
+
|
|
125
|
+
let closed = false;
|
|
126
|
+
let deadCode = null;
|
|
127
|
+
let leaseExpiresAt = Number(attached.leaseExpiresAt) || Date.now() + 30_000;
|
|
128
|
+
let finish;
|
|
129
|
+
const done = new Promise((resolve) => { finish = resolve; });
|
|
130
|
+
let heartbeatBusy = false;
|
|
131
|
+
const markDead = (error) => {
|
|
132
|
+
deadCode ||= String(error?.code || error?.cause?.code || "host_attachment_expired");
|
|
133
|
+
clearInterval(heartbeat);
|
|
134
|
+
finish(deadCode);
|
|
135
|
+
};
|
|
136
|
+
function assertLive() {
|
|
137
|
+
if (closed) throw codeError("neutral_host_adapter_closed");
|
|
138
|
+
if (deadCode) throw codeError(deadCode);
|
|
139
|
+
}
|
|
140
|
+
// A Host without the native inbox routes is of an earlier release: ask it to make room, once; the error is unchanged.
|
|
141
|
+
let handoverAsked = false;
|
|
142
|
+
async function call(method, pathname, body = null, requestOptions = {}) {
|
|
143
|
+
assertLive();
|
|
144
|
+
try {
|
|
145
|
+
const result = await callRaw(method, pathname, body, requestOptions);
|
|
146
|
+
if (isOpenClawNeutralHostUnavailable({ code: result?.body?.error })) {
|
|
147
|
+
throw codeError(result.body.error);
|
|
148
|
+
}
|
|
149
|
+
if (!handoverAsked && isMissingNativeInboxRoute(pathname, result)) {
|
|
150
|
+
handoverAsked = true;
|
|
151
|
+
try {
|
|
152
|
+
await requestHandover({ runtimeRoot: binding.runtimeRoot, hostInstallationId: binding.hostInstallationId,
|
|
153
|
+
publishedAt: new Date().toISOString(), waitMs: 0 });
|
|
154
|
+
} catch {}
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (isOpenClawNeutralHostUnavailable(error)) markDead(error);
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const heartbeatMs = integer(options.heartbeatMs, 10_000, 100, 30_000);
|
|
163
|
+
const heartbeat = setInterval(async () => {
|
|
164
|
+
if (closed || deadCode || heartbeatBusy) return;
|
|
165
|
+
heartbeatBusy = true;
|
|
166
|
+
try {
|
|
167
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.attachmentHeartbeat, { adapterInstanceId }));
|
|
168
|
+
leaseExpiresAt = Number(result.leaseExpiresAt) || Date.now() + 30_000;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (isOpenClawNeutralHostUnavailable(error)) markDead(error);
|
|
171
|
+
else if (Date.now() >= leaseExpiresAt) markDead(codeError("host_attachment_expired"));
|
|
172
|
+
} finally {
|
|
173
|
+
heartbeatBusy = false;
|
|
174
|
+
}
|
|
175
|
+
}, heartbeatMs);
|
|
176
|
+
heartbeat.unref?.();
|
|
177
|
+
|
|
178
|
+
const resourceCache = new Map([
|
|
179
|
+
["inbox", { value: null, expiresAt: 0, flight: null, generation: 0 }],
|
|
180
|
+
["contacts", { value: null, expiresAt: 0, flight: null, generation: 0 }],
|
|
181
|
+
["contactRequests", { value: null, expiresAt: 0, flight: null, generation: 0 }],
|
|
182
|
+
["outbox", { value: null, expiresAt: 0, flight: null, generation: 0 }],
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
function invalidateResources(...names) {
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
const entry = resourceCache.get(name);
|
|
188
|
+
if (!entry) continue;
|
|
189
|
+
entry.value = null;
|
|
190
|
+
entry.expiresAt = 0;
|
|
191
|
+
entry.flight = null;
|
|
192
|
+
entry.generation += 1;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function refreshResource(name, loader) {
|
|
197
|
+
const entry = resourceCache.get(name);
|
|
198
|
+
if (entry.flight) return entry.flight;
|
|
199
|
+
const generation = entry.generation;
|
|
200
|
+
const flight = Promise.resolve()
|
|
201
|
+
.then(loader)
|
|
202
|
+
.then((value) => {
|
|
203
|
+
if (entry.generation === generation) {
|
|
204
|
+
entry.value = value;
|
|
205
|
+
entry.expiresAt = now() + cacheTtlMs;
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
});
|
|
209
|
+
entry.flight = flight;
|
|
210
|
+
void flight.then(
|
|
211
|
+
() => { if (entry.flight === flight) entry.flight = null; },
|
|
212
|
+
() => { if (entry.flight === flight) entry.flight = null; },
|
|
213
|
+
);
|
|
214
|
+
return flight;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function readResource(name, loader, forceRefresh) {
|
|
218
|
+
const entry = resourceCache.get(name);
|
|
219
|
+
const current = now();
|
|
220
|
+
if (!forceRefresh && entry.value && entry.expiresAt > current) return entry.value;
|
|
221
|
+
if (!forceRefresh && entry.value) {
|
|
222
|
+
void refreshResource(name, loader).catch(() => {});
|
|
223
|
+
return entry.value;
|
|
224
|
+
}
|
|
225
|
+
return refreshResource(name, loader);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function readBundle(input = {}) {
|
|
229
|
+
assertLive();
|
|
230
|
+
const includeContacts = input.includeContacts === true;
|
|
231
|
+
const includeContactRequests = input.includeContactRequests === true;
|
|
232
|
+
const includeOutbox = input.includeOutbox === true;
|
|
233
|
+
const forceRefresh = input.refresh === true;
|
|
234
|
+
const [inbox, contacts, contactRequests, outbox] = await Promise.all([
|
|
235
|
+
readResource("inbox", () => readRequired(HELLOLYNK_HOST_PATHS.appInboxMessages), forceRefresh),
|
|
236
|
+
includeContacts
|
|
237
|
+
? readResource(
|
|
238
|
+
"contacts",
|
|
239
|
+
() => readOptional(HELLOLYNK_HOST_PATHS.appContacts, { contacts: [] }),
|
|
240
|
+
forceRefresh,
|
|
241
|
+
)
|
|
242
|
+
: { available: false, contacts: [] },
|
|
243
|
+
includeContactRequests
|
|
244
|
+
? readResource(
|
|
245
|
+
"contactRequests",
|
|
246
|
+
() => readOptional(HELLOLYNK_HOST_PATHS.appContactRequests, { requests: [] }),
|
|
247
|
+
forceRefresh,
|
|
248
|
+
)
|
|
249
|
+
: { available: false, requests: [] },
|
|
250
|
+
includeOutbox
|
|
251
|
+
? readResource(
|
|
252
|
+
"outbox",
|
|
253
|
+
() => readOptional(HELLOLYNK_HOST_PATHS.appOutboxMessages, { messages: [] }),
|
|
254
|
+
forceRefresh,
|
|
255
|
+
)
|
|
256
|
+
: { available: false, messages: [] },
|
|
257
|
+
]);
|
|
258
|
+
assertLive();
|
|
259
|
+
const messages = Object.freeze([...(inbox.messages || [])]);
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
source: "neutral_host",
|
|
262
|
+
inbox: Object.freeze({
|
|
263
|
+
messages,
|
|
264
|
+
unreadCount: messages.filter((message) => message?.unread !== false).length,
|
|
265
|
+
}),
|
|
266
|
+
contacts: Object.freeze({
|
|
267
|
+
available: contacts.available,
|
|
268
|
+
contacts: Object.freeze((contacts.contacts || []).map(projectSecurityStateOnly)),
|
|
269
|
+
}),
|
|
270
|
+
contactRequests: Object.freeze({
|
|
271
|
+
available: contactRequests.available,
|
|
272
|
+
requests: Object.freeze([...(contactRequests.requests || [])]),
|
|
273
|
+
}),
|
|
274
|
+
outbox: Object.freeze({
|
|
275
|
+
available: outbox.available,
|
|
276
|
+
messages: Object.freeze([...(outbox.messages || [])]),
|
|
277
|
+
}),
|
|
278
|
+
changes: Object.freeze({ available: false, cursor: null, modelVisibility: "none" }),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function readRequired(pathname) {
|
|
283
|
+
return withReadTimeout(
|
|
284
|
+
call("GET", pathname, null, { timeoutMs: readTimeoutMs }),
|
|
285
|
+
readTimeoutMs,
|
|
286
|
+
"neutral_host_read_timeout",
|
|
287
|
+
).then(requireOk);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function readOptional(pathname, fallback) {
|
|
291
|
+
try {
|
|
292
|
+
const value = requireOk(await withReadTimeout(
|
|
293
|
+
call("GET", pathname, null, { timeoutMs: optionalReadTimeoutMs }),
|
|
294
|
+
optionalReadTimeoutMs,
|
|
295
|
+
"neutral_host_optional_read_timeout",
|
|
296
|
+
));
|
|
297
|
+
return Object.freeze({ ...value, available: true });
|
|
298
|
+
} catch {
|
|
299
|
+
return Object.freeze({ ...fallback, available: false });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function close() {
|
|
304
|
+
if (closed) return;
|
|
305
|
+
closed = true;
|
|
306
|
+
clearInterval(heartbeat);
|
|
307
|
+
finish(null);
|
|
308
|
+
try {
|
|
309
|
+
if (!deadCode) await callRaw("POST", HELLOLYNK_HOST_PATHS.attachmentDetach, { adapterInstanceId }).catch(() => {});
|
|
310
|
+
} finally {
|
|
311
|
+
invalidateResources(...resourceCache.keys());
|
|
312
|
+
binding.capability.secretBuffer.fill(0);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return Object.freeze({
|
|
317
|
+
contract: HELLOLYNK_OPENCLAW_NEUTRAL_ADAPTER_CONTRACT,
|
|
318
|
+
mode: "neutral",
|
|
319
|
+
get state() { return closed ? "closed" : deadCode ? "dead" : "attached"; },
|
|
320
|
+
done,
|
|
321
|
+
startedHost,
|
|
322
|
+
binding: publicBinding(binding),
|
|
323
|
+
discovery: publicDiscovery(discovery),
|
|
324
|
+
adapterInstanceId,
|
|
325
|
+
readBundle,
|
|
326
|
+
async modelInbox(input) { return requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appModelInbox, input, { timeoutMs: 15_000 })); },
|
|
327
|
+
async readWorkbenchChanges({ afterCursor = 0 } = {}) {
|
|
328
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appChangesSubscribe, { afterCursor }));
|
|
329
|
+
return { ...result, hostGeneration: discovery.hostGeneration };
|
|
330
|
+
},
|
|
331
|
+
async readWorkbenchSnapshot(input = {}) {
|
|
332
|
+
try {
|
|
333
|
+
// Workbench reads can wait behind an earlier read for this delegate.
|
|
334
|
+
// Keep the deadline below the page's 30 seconds; other calls stay unchanged.
|
|
335
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appWorkbenchSnapshot, input, { timeoutMs: 25_000 }));
|
|
336
|
+
return result.snapshot;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (error?.name === "TimeoutError") throw codeError("workbench_snapshot_timeout");
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
async issueWorkbenchActionGrant(command) {
|
|
343
|
+
const validated = validateWorkbenchCommand(command);
|
|
344
|
+
return requireWorkbenchActionGrant(requireOk(await call(
|
|
345
|
+
"POST",
|
|
346
|
+
WORKBENCH_ACTION_GRANT_PATH,
|
|
347
|
+
{ adapterInstanceId, request: validated },
|
|
348
|
+
)));
|
|
349
|
+
},
|
|
350
|
+
async executeWorkbenchCommand(command, context) {
|
|
351
|
+
// The Host is the sole grant issuer and writer. OpenClaw forwards each
|
|
352
|
+
// exact envelope once and never interprets, stores, retries, or changes it.
|
|
353
|
+
const validated = validateWorkbenchCommand(command);
|
|
354
|
+
const statusRead = validated.command === "message.send-status";
|
|
355
|
+
const actionGrant = statusRead ? noActionGrantContext(context) : requireActionGrantContext(context);
|
|
356
|
+
return requireWorkbenchCommandResult(await call(
|
|
357
|
+
"POST",
|
|
358
|
+
HELLOLYNK_HOST_PATHS.appWorkbenchCommand,
|
|
359
|
+
{ adapterInstanceId, request: validated, ...(statusRead ? {} : { actionGrant: actionGrant.actionGrant }) },
|
|
360
|
+
{},
|
|
361
|
+
), validated.command);
|
|
362
|
+
},
|
|
363
|
+
async markRead(localMessageId) {
|
|
364
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appInboxMarkRead, {
|
|
365
|
+
localMessageId: safeId(localMessageId, "host_app_local_message_id_invalid"),
|
|
366
|
+
}));
|
|
367
|
+
invalidateResources("inbox");
|
|
368
|
+
return result;
|
|
369
|
+
},
|
|
370
|
+
async deleteRead(localMessageIds) {
|
|
371
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appInboxDeleteRead, {
|
|
372
|
+
localMessageIds: localMessageIds.map((value) => safeId(value, "host_app_local_message_id_invalid")),
|
|
373
|
+
}));
|
|
374
|
+
invalidateResources("inbox");
|
|
375
|
+
return result;
|
|
376
|
+
},
|
|
377
|
+
async saveContact(input) {
|
|
378
|
+
const result = requireOk(await call(
|
|
379
|
+
"POST",
|
|
380
|
+
HELLOLYNK_HOST_PATHS.appContactsUpsert,
|
|
381
|
+
exactLegacyContactWrite(input),
|
|
382
|
+
));
|
|
383
|
+
invalidateResources("contacts");
|
|
384
|
+
return projectSecurityStateOnly(result);
|
|
385
|
+
},
|
|
386
|
+
async deleteContact(contactId) {
|
|
387
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appContactsRemove, {
|
|
388
|
+
contactId: safeId(contactId, "host_app_contact_id_invalid"),
|
|
389
|
+
}));
|
|
390
|
+
invalidateResources("contacts");
|
|
391
|
+
return result;
|
|
392
|
+
},
|
|
393
|
+
async setAutonomousSendAuthorization(contactId, recipientDelegateId, enabled) {
|
|
394
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appContactAutonomousSendAuthorization, {
|
|
395
|
+
contactId: safeId(contactId, "host_app_contact_id_invalid"),
|
|
396
|
+
recipientDelegateId: safeId(recipientDelegateId, "recipient_delegate_id_invalid"),
|
|
397
|
+
enabled: enabled === true,
|
|
398
|
+
}));
|
|
399
|
+
invalidateResources("contacts");
|
|
400
|
+
return result;
|
|
401
|
+
},
|
|
402
|
+
async listAuthorizedContacts(input = {}) {
|
|
403
|
+
const query = queryArgs(input);
|
|
404
|
+
const result = await requireOk(await call(Object.keys(query).length ? "POST" : "GET", HELLOLYNK_HOST_PATHS.appAuthorizedContacts, Object.keys(query).length ? query : undefined, { timeoutMs: 25_000 }));
|
|
405
|
+
return Object.freeze({
|
|
406
|
+
...(result.nativeConversations ? { nativeConversations: result.nativeConversations } : {}), contacts: Object.freeze((result.contacts || []).map(projectSecurityStateOnly)),
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
async listAuthorizedInboxMessages({ before } = {}) {
|
|
410
|
+
const result = await requireOk(await (before === undefined ? call("GET", HELLOLYNK_HOST_PATHS.appAuthorizedInboxMessages)
|
|
411
|
+
: call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxMessages, { before: safeId(before, "host_app_authorized_inbox_message_id_invalid") })));
|
|
412
|
+
return Object.freeze({ messages: Object.freeze([...(result.messages || [])]) });
|
|
413
|
+
},
|
|
414
|
+
async listPendingMessages(query = null) {
|
|
415
|
+
const result = await requireOk(await call(query == null ? "GET" : "POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxPending, query));
|
|
416
|
+
return Object.freeze({ messages: Object.freeze([...(result.messages || [])]), ...(result.nextCursor == null ? {} : { nextCursor: result.nextCursor }) });
|
|
417
|
+
},
|
|
418
|
+
async claimMessage(localMessageId, conversationKey) {
|
|
419
|
+
return requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxClaim, {
|
|
420
|
+
localMessageId: safeId(localMessageId, "host_app_authorized_inbox_message_id_invalid"),
|
|
421
|
+
conversationKey: safeId(conversationKey, "host_app_authorized_inbox_conversation_key_invalid"),
|
|
422
|
+
}));
|
|
423
|
+
},
|
|
424
|
+
async claimBatch(batchId) {
|
|
425
|
+
return requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxClaim, { batchId }));
|
|
426
|
+
},
|
|
427
|
+
async inboxBatch(action, input = {}) {
|
|
428
|
+
if (!["reserve", "open", "attempt", "confirm", "cancel", "expire", "closed"].includes(action)) throw codeError("native_batch_action_invalid");
|
|
429
|
+
return requireOk(await call("POST", `/host/v1/app/authorized-inbox/batch/${action}`, input));
|
|
430
|
+
},
|
|
431
|
+
async activateAuthorizedInbox() {
|
|
432
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxActivate));
|
|
433
|
+
if (result.active !== true) throw codeError("neutral_authorized_inbox_activation_invalid");
|
|
434
|
+
return Object.freeze({ ok: true, active: true });
|
|
435
|
+
},
|
|
436
|
+
async readAuthorizedInboxMessage(localMessageId, conversationKey) {
|
|
437
|
+
const result = await requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedInboxRead, {
|
|
438
|
+
localMessageId: safeId(localMessageId, "host_app_authorized_inbox_message_id_invalid"),
|
|
439
|
+
conversationKey: safeId(conversationKey, "host_app_authorized_inbox_conversation_key_invalid"),
|
|
440
|
+
}));
|
|
441
|
+
return result.message;
|
|
442
|
+
},
|
|
443
|
+
async listAuthorizedContactRequests() {
|
|
444
|
+
const result = await requireOk(await call("GET", "/host/v1/app/authorized-contact-requests"));
|
|
445
|
+
return Object.freeze({ requests: Object.freeze([...(result.requests || [])]) });
|
|
446
|
+
},
|
|
447
|
+
async readAuthorizedContactRequest(localRecordId, conversationKey) {
|
|
448
|
+
const result = await requireOk(await call("POST", "/host/v1/app/authorized-contact-requests/read", {
|
|
449
|
+
localRecordId: safeId(localRecordId, "host_app_authorized_contact_request_id_invalid"),
|
|
450
|
+
conversationKey: safeId(conversationKey, "host_app_authorized_contact_request_conversation_key_invalid"),
|
|
451
|
+
}));
|
|
452
|
+
return result.request;
|
|
453
|
+
},
|
|
454
|
+
async sendAuthorizedMessage(input) {
|
|
455
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedSend, authorizedSendInput(input),
|
|
456
|
+
input.targetConversationTitle ? { timeoutMs: 55_000 } : input.targetConversationId ? { timeoutMs: 30_000 } : {}));
|
|
457
|
+
invalidateResources("outbox");
|
|
458
|
+
return result;
|
|
459
|
+
},
|
|
460
|
+
async replyAuthorizedMessage(input) {
|
|
461
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appAuthorizedReply, authorizedReplyInput(input)));
|
|
462
|
+
invalidateResources("outbox");
|
|
463
|
+
return Object.freeze({
|
|
464
|
+
accepted: result.accepted,
|
|
465
|
+
authorization: result.authorization,
|
|
466
|
+
contactId: result.contactId,
|
|
467
|
+
clientMessageId: result.clientMessageId,
|
|
468
|
+
status: result.status,
|
|
469
|
+
});
|
|
470
|
+
},
|
|
471
|
+
async issueRetryGrant(clientMessageId) {
|
|
472
|
+
return requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appOutboxRetryGrant, {
|
|
473
|
+
adapterInstanceId,
|
|
474
|
+
clientMessageId: safeId(clientMessageId, "host_app_client_message_id_invalid"),
|
|
475
|
+
}));
|
|
476
|
+
},
|
|
477
|
+
async retryOutgoing(clientMessageId, retryGrant) {
|
|
478
|
+
const result = requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appOutboxRetry, {
|
|
479
|
+
adapterInstanceId,
|
|
480
|
+
clientMessageId: safeId(clientMessageId, "host_app_client_message_id_invalid"),
|
|
481
|
+
retryGrant: String(retryGrant || ""),
|
|
482
|
+
}));
|
|
483
|
+
invalidateResources("outbox");
|
|
484
|
+
return result;
|
|
485
|
+
},
|
|
486
|
+
async deleteOutgoing() {
|
|
487
|
+
throw codeError("neutral_outbox_delete_route_unavailable");
|
|
488
|
+
},
|
|
489
|
+
async requestHumanSend(input) {
|
|
490
|
+
const result = requireAccepted(await call("POST", HELLOLYNK_HOST_PATHS.appHumanSendRequests, {
|
|
491
|
+
adapterInstanceId,
|
|
492
|
+
request: input,
|
|
493
|
+
}));
|
|
494
|
+
invalidateResources("outbox");
|
|
495
|
+
return result;
|
|
496
|
+
},
|
|
497
|
+
async getHumanSendStatus(actionId) {
|
|
498
|
+
return requireOk(await call("POST", HELLOLYNK_HOST_PATHS.appHumanSendRequestStatus, {
|
|
499
|
+
adapterInstanceId,
|
|
500
|
+
request: { actionId: safeId(actionId, "host_app_action_id_invalid") },
|
|
501
|
+
}));
|
|
502
|
+
},
|
|
503
|
+
close,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function validateOpenClawProtectedBinding(value) {
|
|
508
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
509
|
+
throw codeError("neutral_capability_binding_invalid");
|
|
510
|
+
}
|
|
511
|
+
const capability = value.capability;
|
|
512
|
+
if (!capability || typeof capability !== "object" || Array.isArray(capability)) {
|
|
513
|
+
throw codeError("neutral_capability_invalid");
|
|
514
|
+
}
|
|
515
|
+
const binding = {
|
|
516
|
+
region: safeId(value.region, "neutral_region_invalid"),
|
|
517
|
+
principalId: safeId(value.principalId, "neutral_principal_invalid"),
|
|
518
|
+
delegateId: safeId(value.delegateId, "neutral_delegate_invalid"),
|
|
519
|
+
hostInstallationId: safeId(value.hostInstallationId, "neutral_host_installation_invalid"),
|
|
520
|
+
profileId: safeId(value.profileId, "neutral_profile_invalid"),
|
|
521
|
+
adapterId: safeId(value.adapterId, "neutral_adapter_invalid"),
|
|
522
|
+
runtimeRoot: String(value.runtimeRoot || ""),
|
|
523
|
+
releaseRoot: String(value.releaseRoot || ""),
|
|
524
|
+
releaseManifestSha256: String(value.releaseManifestSha256 || ""),
|
|
525
|
+
runnerRelativePath: String(value.runnerRelativePath || ""),
|
|
526
|
+
profileManifestPath: String(value.profileManifestPath || ""),
|
|
527
|
+
};
|
|
528
|
+
if (binding.adapterId !== "openclaw") throw codeError("neutral_adapter_mismatch");
|
|
529
|
+
if (!binding.runtimeRoot || !path.isAbsolute(binding.runtimeRoot)) throw codeError("neutral_runtime_root_invalid");
|
|
530
|
+
if (!binding.releaseRoot || !path.isAbsolute(binding.releaseRoot)
|
|
531
|
+
|| !binding.profileManifestPath || !path.isAbsolute(binding.profileManifestPath)
|
|
532
|
+
|| !/^[a-f0-9]{64}$/.test(binding.releaseManifestSha256)
|
|
533
|
+
|| binding.runnerRelativePath !== "runtime/neutral-host-bootstrap.mjs") {
|
|
534
|
+
throw codeError("neutral_openclaw_launch_configuration_invalid");
|
|
535
|
+
}
|
|
536
|
+
if (capability.region !== binding.region || capability.principalId !== binding.principalId
|
|
537
|
+
|| capability.delegateId !== binding.delegateId
|
|
538
|
+
|| capability.hostInstallationId !== binding.hostInstallationId
|
|
539
|
+
|| capability.adapterId !== binding.adapterId) {
|
|
540
|
+
throw codeError("neutral_capability_owner_mismatch");
|
|
541
|
+
}
|
|
542
|
+
const scopes = [...new Set((capability.scopes || []).map(String))].sort();
|
|
543
|
+
if (!sameStrings(scopes, OPENCLAW_NEUTRAL_EXACT_SCOPES)) {
|
|
544
|
+
throw codeError("neutral_capability_scopes_mismatch");
|
|
545
|
+
}
|
|
546
|
+
const secretBuffer = Buffer.isBuffer(capability.secretBuffer)
|
|
547
|
+
? Buffer.from(capability.secretBuffer)
|
|
548
|
+
: capability.secretBuffer instanceof Uint8Array ? Buffer.from(capability.secretBuffer) : null;
|
|
549
|
+
if (!secretBuffer || secretBuffer.length < 32 || secretBuffer.length > 128) {
|
|
550
|
+
throw codeError("neutral_capability_secret_invalid");
|
|
551
|
+
}
|
|
552
|
+
if (Number(capability.version) !== 2) throw codeError("neutral_capability_version_mismatch");
|
|
553
|
+
if (capability.state !== "active") throw codeError("neutral_capability_inactive");
|
|
554
|
+
if (capability.contract !== HELLOLYNK_HOST_CAPABILITY_CONTRACT) throw codeError("neutral_capability_binding_invalid");
|
|
555
|
+
const capabilityId = safeId(capability.capabilityId, "neutral_capability_id_invalid");
|
|
556
|
+
const issuedAt = Number(capability.issuedAt);
|
|
557
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt < 1 || capability.revokedAt != null) {
|
|
558
|
+
throw codeError("neutral_capability_binding_invalid");
|
|
559
|
+
}
|
|
560
|
+
return Object.freeze({
|
|
561
|
+
...binding,
|
|
562
|
+
capability: Object.freeze({
|
|
563
|
+
contract: HELLOLYNK_HOST_CAPABILITY_CONTRACT,
|
|
564
|
+
capabilityId,
|
|
565
|
+
region: binding.region,
|
|
566
|
+
principalId: binding.principalId,
|
|
567
|
+
delegateId: binding.delegateId,
|
|
568
|
+
hostInstallationId: binding.hostInstallationId,
|
|
569
|
+
adapterId: "openclaw",
|
|
570
|
+
version: 2,
|
|
571
|
+
state: "active",
|
|
572
|
+
issuedAt,
|
|
573
|
+
revokedAt: null,
|
|
574
|
+
scopes: Object.freeze(scopes),
|
|
575
|
+
secretBuffer,
|
|
576
|
+
}),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function assertDiscovery(discovery, binding, discoveryContract) {
|
|
581
|
+
if (!discovery || discovery.contractId !== discoveryContract
|
|
582
|
+
|| discovery.region !== binding.region
|
|
583
|
+
|| discovery.principalId !== binding.principalId
|
|
584
|
+
|| discovery.hostInstallationId !== binding.hostInstallationId
|
|
585
|
+
|| discovery.host !== "127.0.0.1"
|
|
586
|
+
|| !Number.isSafeInteger(discovery.port) || discovery.port < 1 || discovery.port > 65_535
|
|
587
|
+
|| !/^[a-f0-9]{48}$/.test(String(discovery.hostGeneration || ""))) {
|
|
588
|
+
throw codeError("neutral_host_discovery_owner_mismatch");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function isMissingNativeInboxRoute(pathname, result) {
|
|
593
|
+
return (pathname === HELLOLYNK_HOST_PATHS.appAuthorizedInboxPending || pathname === HELLOLYNK_HOST_PATHS.appAuthorizedInboxClaim
|
|
594
|
+
|| pathname.startsWith("/host/v1/app/authorized-inbox/batch/"))
|
|
595
|
+
&& (result?.body?.error === "host_route_not_found" || (result?.status === 404 && !result?.body?.error));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function requireOk(result) {
|
|
599
|
+
if (!result || result.status !== 200 || result.body?.ok !== true) {
|
|
600
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
601
|
+
}
|
|
602
|
+
return result.body;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function requireAccepted(result) {
|
|
606
|
+
if (!result || result.status !== 202 || result.body?.ok !== true) {
|
|
607
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
608
|
+
}
|
|
609
|
+
return result.body;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function requireWorkbenchCommandResult(result, expectedCommand) {
|
|
613
|
+
if (!result || result.status !== 200) {
|
|
614
|
+
throw codeError(String(result?.body?.error || `neutral_host_http_${result?.status || 0}`));
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
return validateWorkbenchCommandResult(result.body, expectedCommand);
|
|
618
|
+
} catch {
|
|
619
|
+
throw codeError("neutral_host_workbench_command_response_invalid");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function requireWorkbenchActionGrant(value) {
|
|
624
|
+
if (!isPlainRecord(value)
|
|
625
|
+
|| Object.keys(value).sort().join(",") !== "actionGrant,expiresAtMs,ok"
|
|
626
|
+
|| value.ok !== true
|
|
627
|
+
|| typeof value.actionGrant !== "string"
|
|
628
|
+
|| !ACTION_GRANT.test(value.actionGrant)
|
|
629
|
+
|| !Number.isSafeInteger(value.expiresAtMs)
|
|
630
|
+
|| value.expiresAtMs <= Date.now()) {
|
|
631
|
+
throw codeError("neutral_host_workbench_action_grant_response_invalid");
|
|
632
|
+
}
|
|
633
|
+
return Object.freeze({ actionGrant: value.actionGrant });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function requireActionGrantContext(value) {
|
|
637
|
+
if (!isPlainRecord(value)
|
|
638
|
+
|| Object.keys(value).length !== 1
|
|
639
|
+
|| !Object.hasOwn(value, "actionGrant")
|
|
640
|
+
|| typeof value.actionGrant !== "string"
|
|
641
|
+
|| !ACTION_GRANT.test(value.actionGrant)) {
|
|
642
|
+
throw codeError("neutral_host_workbench_action_grant_required");
|
|
643
|
+
}
|
|
644
|
+
return Object.freeze({ actionGrant: value.actionGrant });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function noActionGrantContext(value) {
|
|
648
|
+
if (value != null && (!isPlainRecord(value) || Object.keys(value).length !== 0)) {
|
|
649
|
+
throw codeError("neutral_host_workbench_action_grant_forbidden");
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function isPlainRecord(value) {
|
|
655
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
656
|
+
const prototype = Object.getPrototypeOf(value);
|
|
657
|
+
return prototype === Object.prototype || prototype === null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function projectSecurityStateOnly(value) {
|
|
661
|
+
if (Array.isArray(value)) return Object.freeze(value.map(projectSecurityStateOnly));
|
|
662
|
+
if (!isPlainRecord(value)) return value;
|
|
663
|
+
const projected = {};
|
|
664
|
+
for (const key of Object.keys(value)) {
|
|
665
|
+
const normalizedKey = key.replace(/[-_]/g, "").toLowerCase();
|
|
666
|
+
if (normalizedKey === "safetycode" || normalizedKey === "localdevicesafetycode") {
|
|
667
|
+
throw codeError("neutral_host_raw_safety_code_rejected");
|
|
668
|
+
}
|
|
669
|
+
projected[key] = projectSecurityStateOnly(value[key]);
|
|
670
|
+
}
|
|
671
|
+
return Object.freeze(projected);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function exactLegacyContactWrite(value) {
|
|
675
|
+
if (!isPlainRecord(value)) throw codeError("neutral_host_contact_fields_invalid");
|
|
676
|
+
const actual = Object.keys(value).sort().join(",");
|
|
677
|
+
if (actual !== "displayName,hellolynkId" && actual !== "contactId,displayName,hellolynkId") {
|
|
678
|
+
throw codeError("neutral_host_contact_fields_invalid");
|
|
679
|
+
}
|
|
680
|
+
return Object.freeze({
|
|
681
|
+
...(Object.hasOwn(value, "contactId") ? { contactId: value.contactId } : {}),
|
|
682
|
+
displayName: value.displayName,
|
|
683
|
+
hellolynkId: value.hellolynkId,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function publicBinding(value) {
|
|
688
|
+
return Object.freeze({
|
|
689
|
+
region: value.region,
|
|
690
|
+
principalId: value.principalId,
|
|
691
|
+
delegateId: value.delegateId,
|
|
692
|
+
hostInstallationId: value.hostInstallationId,
|
|
693
|
+
profileId: value.profileId,
|
|
694
|
+
adapterId: "openclaw",
|
|
695
|
+
capabilityVersion: 2,
|
|
696
|
+
scopes: Object.freeze([...value.capability.scopes]),
|
|
697
|
+
runtimeRoot: value.runtimeRoot,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function launchBinding(value) {
|
|
702
|
+
return Object.freeze({
|
|
703
|
+
...publicBinding(value),
|
|
704
|
+
delegateId: value.delegateId,
|
|
705
|
+
profileId: value.profileId,
|
|
706
|
+
adapterId: "openclaw",
|
|
707
|
+
version: 2,
|
|
708
|
+
scopes: Object.freeze([...value.capability.scopes]),
|
|
709
|
+
releaseRoot: value.releaseRoot,
|
|
710
|
+
releaseManifestSha256: value.releaseManifestSha256,
|
|
711
|
+
runnerRelativePath: value.runnerRelativePath,
|
|
712
|
+
profileManifestPath: value.profileManifestPath,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// The same or a higher release answers "current" and nothing is written. An earlier one finishes
|
|
717
|
+
// its admitted work and leaves; while it is busy it keeps serving and this client attaches to it.
|
|
718
|
+
async function earlierHostLeft(requestHandover, binding, waitMs) {
|
|
719
|
+
try {
|
|
720
|
+
const { status } = await requestHandover({ runtimeRoot: binding.runtimeRoot,
|
|
721
|
+
hostInstallationId: binding.hostInstallationId, publishedAt: new Date().toISOString(), waitMs, untilStopped: true });
|
|
722
|
+
return ["stopped", "restarted", "no_host"].includes(status);
|
|
723
|
+
} catch { return false; }
|
|
724
|
+
}
|
|
725
|
+
async function safeDiscover(discover, input) {
|
|
726
|
+
try { return await discover(input); }
|
|
727
|
+
catch (error) { if (SAFE_DISCOVERY_ERRORS.has(error?.code)) return null; throw error; }
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function waitForDiscovery(discover, input, options) {
|
|
731
|
+
const wait = options.wait || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
732
|
+
const timeoutMs = integer(options.discoveryWaitMs, 75_000, 10, 120_000);
|
|
733
|
+
const deadline = Date.now() + timeoutMs;
|
|
734
|
+
while (Date.now() < deadline) {
|
|
735
|
+
const value = await safeDiscover(discover, input);
|
|
736
|
+
if (value) return value;
|
|
737
|
+
await wait(Math.min(50, Math.max(1, deadline - Date.now())));
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function publicDiscovery(value) {
|
|
743
|
+
return Object.freeze({
|
|
744
|
+
region: value.region,
|
|
745
|
+
principalId: value.principalId,
|
|
746
|
+
hostInstallationId: value.hostInstallationId,
|
|
747
|
+
host: value.host,
|
|
748
|
+
port: value.port,
|
|
749
|
+
hostGeneration: value.hostGeneration,
|
|
750
|
+
startedAt: value.startedAt || null,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function safeId(value, code) {
|
|
755
|
+
const text = String(value || "");
|
|
756
|
+
if (!SAFE_ID.test(text)) throw codeError(code);
|
|
757
|
+
return text;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function authorizedSendInput(value) {
|
|
761
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
762
|
+
|| Object.keys(value).filter(key => key !== "targetConversationId" && key !== "targetConversationTitle").sort().join("|") !== "clientMessageId|contactId|recipientDelegateId|text") {
|
|
763
|
+
throw codeError("authorized_send_input_invalid");
|
|
764
|
+
}
|
|
765
|
+
const text = String(value.text ?? "");
|
|
766
|
+
if (!text || text.includes("\u0000") || Buffer.byteLength(text, "utf8") > 20 * 1024) {
|
|
767
|
+
throw codeError("message_text_invalid");
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
...optionalTarget(value),
|
|
771
|
+
clientMessageId: safeId(value.clientMessageId, "host_app_client_message_id_invalid"),
|
|
772
|
+
contactId: safeId(value.contactId, "host_app_contact_id_invalid"),
|
|
773
|
+
recipientDelegateId: safeId(value.recipientDelegateId, "recipient_delegate_id_invalid"),
|
|
774
|
+
text,
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function authorizedReplyInput(value) {
|
|
779
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
780
|
+
|| Object.keys(value).sort().join("|") !== "clientMessageId|conversationKey|localMessageId|text") {
|
|
781
|
+
throw codeError("authorized_reply_input_invalid");
|
|
782
|
+
}
|
|
783
|
+
if (typeof value.text !== "string" || !value.text || value.text.includes("\u0000")
|
|
784
|
+
|| Buffer.byteLength(value.text, "utf8") > 20 * 1024) {
|
|
785
|
+
throw codeError("message_text_invalid");
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
localMessageId: safeId(value.localMessageId, "host_app_local_message_id_invalid"),
|
|
789
|
+
conversationKey: safeId(value.conversationKey, "host_app_conversation_key_invalid"),
|
|
790
|
+
clientMessageId: safeId(value.clientMessageId, "host_app_client_message_id_invalid"),
|
|
791
|
+
text: value.text,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function integer(value, fallback, minimum, maximum) {
|
|
796
|
+
const number = Number(value ?? fallback);
|
|
797
|
+
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
798
|
+
throw codeError("neutral_limit_invalid");
|
|
799
|
+
}
|
|
800
|
+
return number;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function withReadTimeout(operation, timeoutMs, code) {
|
|
804
|
+
let timeout;
|
|
805
|
+
const expiry = new Promise((_, reject) => {
|
|
806
|
+
timeout = setTimeout(() => reject(codeError(code)), timeoutMs);
|
|
807
|
+
timeout.unref?.();
|
|
808
|
+
});
|
|
809
|
+
return Promise.race([operation, expiry]).finally(() => clearTimeout(timeout));
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function sameStrings(left, right) {
|
|
813
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function codeError(code) {
|
|
817
|
+
const error = new Error(code);
|
|
818
|
+
error.code = code;
|
|
819
|
+
return error;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function loadNeutralHostProcess() {
|
|
823
|
+
// OpenClaw captures plugin dependencies before running channel callbacks.
|
|
824
|
+
// Keep the lazy import literal so that capture can include the shared core.
|
|
825
|
+
return import("../../../clients/core/hellolynk-neutral-host-process.mjs");
|
|
826
|
+
}
|