@hellolynk-ai/workbuddy-mcp 0.0.0-stage → 0.3.96
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 +88 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -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-task-index.cjs +305 -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/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 +884 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/integrations/workbuddy/package.json +15 -0
- package/integrations/workbuddy/server/host-lifecycle.mjs +249 -0
- package/integrations/workbuddy/server/mcp-proxy.mjs +258 -0
- package/integrations/workbuddy/server/mcp-server.mjs +357 -0
- package/integrations/workbuddy/server/model-call-origin.mjs +87 -0
- package/integrations/workbuddy/server/native-task-cdp.mjs +175 -0
- package/integrations/workbuddy/server/native-task-debug-startup.mjs +117 -0
- package/integrations/workbuddy/server/native-task-debug-startup.ps1 +40 -0
- package/integrations/workbuddy/server/native-task-mcp-registration.mjs +30 -0
- package/integrations/workbuddy/server/native-task-service-registration.mjs +115 -0
- package/integrations/workbuddy/server/native-task-worker.mjs +127 -0
- package/integrations/workbuddy/server/neutral-host-adapter.mjs +639 -0
- package/integrations/workbuddy/server/origin-hook.mjs +111 -0
- package/integrations/workbuddy/server/origin-routes.mjs +211 -0
- package/integrations/workbuddy/server/package-bootstrap.mjs +111 -0
- package/integrations/workbuddy/server/workbench-candidate.mjs +202 -0
- package/integrations/workbuddy/server/workbench-hidden-launcher.vbs +46 -0
- package/integrations/workbuddy/server/workbench-process.mjs +373 -0
- package/integrations/workbuddy/server/workbench-task.ps1 +196 -0
- package/integrations/workbuddy/skills/hellolynk-inbox/SKILL.md +9 -0
- package/integrations/workbuddy/skills/hellolynk-inbox/workbuddy.json +6 -0
- package/package.json +65 -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
- package/tools/hellolynk-install-client-skills.mjs +1211 -0
- package/workbuddy-package-manifest.json +1 -0
- package/workbuddy-package-pin.json +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Option Explicit
|
|
2
|
+
|
|
3
|
+
If WScript.Arguments.Count <> 5 And WScript.Arguments.Count <> 7 Then WScript.Quit 2
|
|
4
|
+
|
|
5
|
+
Dim shell, fso, command, exitCode, nodePath, entryPath, rootPath, manifest, storeRoot, expectedLauncher, expectedEntry, serviceMode, debugEndpoint, endpointPattern
|
|
6
|
+
Set shell = CreateObject("WScript.Shell")
|
|
7
|
+
Set fso = CreateObject("Scripting.FileSystemObject")
|
|
8
|
+
|
|
9
|
+
Function QuoteArgument(value)
|
|
10
|
+
QuoteArgument = Chr(34) & Replace(value, Chr(34), Chr(34) & Chr(34)) & Chr(34)
|
|
11
|
+
End Function
|
|
12
|
+
|
|
13
|
+
Function IsSha256(value)
|
|
14
|
+
Dim index, valueChar
|
|
15
|
+
IsSha256 = Len(value) = 64
|
|
16
|
+
For index = 1 To Len(value)
|
|
17
|
+
valueChar = Mid(value, index, 1)
|
|
18
|
+
If InStr("0123456789abcdef", valueChar) = 0 Then IsSha256 = False
|
|
19
|
+
Next
|
|
20
|
+
End Function
|
|
21
|
+
|
|
22
|
+
nodePath = WScript.Arguments(0)
|
|
23
|
+
entryPath = WScript.Arguments(1)
|
|
24
|
+
rootPath = WScript.Arguments(2)
|
|
25
|
+
manifest = WScript.Arguments(3)
|
|
26
|
+
storeRoot = WScript.Arguments(4)
|
|
27
|
+
If StrComp(fso.GetAbsolutePathName(rootPath), rootPath, 1) <> 0 Then WScript.Quit 3
|
|
28
|
+
If StrComp(fso.GetAbsolutePathName(storeRoot), storeRoot, 1) <> 0 Or LCase(fso.GetFileName(storeRoot)) <> "host" Then WScript.Quit 3
|
|
29
|
+
expectedLauncher = fso.BuildPath(rootPath, "integrations\workbuddy\server\workbench-hidden-launcher.vbs")
|
|
30
|
+
expectedEntry = fso.BuildPath(rootPath, "integrations\workbuddy\server\package-bootstrap.mjs")
|
|
31
|
+
If StrComp(fso.GetAbsolutePathName(WScript.ScriptFullName), expectedLauncher, 1) <> 0 Then WScript.Quit 3
|
|
32
|
+
If StrComp(fso.GetAbsolutePathName(nodePath), nodePath, 1) <> 0 Or StrComp(fso.GetAbsolutePathName(entryPath), expectedEntry, 1) <> 0 Or Not fso.FileExists(entryPath) Or Not fso.FileExists(nodePath) Or Not IsSha256(manifest) Then WScript.Quit 3
|
|
33
|
+
shell.Environment("Process")("HELLOLYNK_NEUTRAL_STORE_ROOT") = storeRoot
|
|
34
|
+
serviceMode = "workbench-service"
|
|
35
|
+
If WScript.Arguments.Count = 7 Then
|
|
36
|
+
serviceMode = WScript.Arguments(5)
|
|
37
|
+
debugEndpoint = WScript.Arguments(6)
|
|
38
|
+
Set endpointPattern = New RegExp
|
|
39
|
+
endpointPattern.Pattern = "^http://127\.0\.0\.1:[0-9]{1,5}$"
|
|
40
|
+
If serviceMode <> "native-task-service" Or Not endpointPattern.Test(debugEndpoint) Then WScript.Quit 3
|
|
41
|
+
shell.Environment("Process")("HELLOLYNK_WORKBUDDY_DEBUG_ENDPOINT") = debugEndpoint
|
|
42
|
+
End If
|
|
43
|
+
command = QuoteArgument(nodePath) & " " & QuoteArgument(entryPath) & " " & serviceMode & " --root " & QuoteArgument(rootPath) & _
|
|
44
|
+
" --expected-manifest-sha256 " & manifest
|
|
45
|
+
exitCode = shell.Run(command, 0, True)
|
|
46
|
+
WScript.Quit exitCode
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import runtimeSafety from "../../../clients/core/hellolynk-runtime-safety.cjs";
|
|
9
|
+
import { defaultNeutralHostStoreRoot } from "../../../clients/core/hellolynk-neutral-host-installation-store.mjs";
|
|
10
|
+
import { bootstrapWorkBuddyWorkbenchCandidate } from "./workbench-candidate.mjs";
|
|
11
|
+
import { registerWorkBuddyNativeMcp } from "./native-task-mcp-registration.mjs";
|
|
12
|
+
import { configureWorkBuddyDebugStartup, transferWorkBuddyDebugStartupOwner } from "./native-task-debug-startup.mjs";
|
|
13
|
+
import { withWorkBuddyNativeServiceRecord } from "./native-task-service-registration.mjs";
|
|
14
|
+
import { removeWorkBuddyOriginHook, workBuddyOriginHookCommand, workBuddyOriginInbox } from "./origin-routes.mjs";
|
|
15
|
+
import { loadPinnedWorkBuddyProxy } from "./package-bootstrap.mjs";
|
|
16
|
+
|
|
17
|
+
const exec = promisify(execFile);
|
|
18
|
+
const STARTUP_TIMEOUT_MS = 90_000;
|
|
19
|
+
|
|
20
|
+
// WorkBuddy may replace its conversation worker after a normal turn. Windows
|
|
21
|
+
// uses the installed on-demand page service; other platforms use a detached
|
|
22
|
+
// handoff. Both run only the pinned package.
|
|
23
|
+
export async function launchDetachedWorkBuddyWorkbench(options) {
|
|
24
|
+
const args = bootstrapArgs("handoff-workbench", options);
|
|
25
|
+
if ((options.platform || process.platform) === "win32") return launchScheduledWorkbench(options);
|
|
26
|
+
const executable = await runtimeSafety.resolveHelloLynkStandaloneNode({ preferredExecutable: options.nodePath });
|
|
27
|
+
let stdout;
|
|
28
|
+
try {
|
|
29
|
+
({ stdout } = await (options.exec || exec)(executable, args, {
|
|
30
|
+
cwd: options.root,
|
|
31
|
+
env: runtimeSafety.standaloneHelloLynkNodeEnvironment(process.env),
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
timeout: STARTUP_TIMEOUT_MS + 5_000,
|
|
34
|
+
maxBuffer: 4096,
|
|
35
|
+
}));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
// Only bounded, known diagnostic codes cross the subprocess boundary.
|
|
38
|
+
// Never return the command line, stderr or native exception to the client.
|
|
39
|
+
if (error?.killed === true && error?.signal === "SIGTERM"
|
|
40
|
+
&& error?.code !== "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
41
|
+
throw failure("workbuddy_local_web_launch_timeout");
|
|
42
|
+
}
|
|
43
|
+
let reported;
|
|
44
|
+
try { reported = JSON.parse(String(error?.stderr || "").slice(0, 4096)); } catch {}
|
|
45
|
+
const known = ["workbuddy_local_web_launch_timeout", "workbuddy_local_web_launch_failed",
|
|
46
|
+
"workbuddy_local_web_launch_invalid", "workbuddy_local_web_launch_handoff_missing"];
|
|
47
|
+
throw failure(reported?.ok === false && known.includes(reported.error)
|
|
48
|
+
? reported.error : "workbuddy_local_web_launch_failed");
|
|
49
|
+
}
|
|
50
|
+
try { return validateReady(JSON.parse(stdout)); }
|
|
51
|
+
catch { throw failure("workbuddy_local_web_launch_invalid"); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Installed once by setup. The on-demand task owns the page, so a client's
|
|
55
|
+
// Windows Job cleanup cannot terminate an active browser session.
|
|
56
|
+
export async function configureWorkBuddyWorkbenchTask(options, operation = "Install") {
|
|
57
|
+
bootstrapArgs("workbench-service", options);
|
|
58
|
+
if (!["Install", "Verify", "Start", "Uninstall"].includes(operation)) throw failure("workbuddy_page_task_operation_invalid");
|
|
59
|
+
try {
|
|
60
|
+
const driverRoot = operation === "Verify" ? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..") : options.root;
|
|
61
|
+
const { stdout } = await exec("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File",
|
|
62
|
+
path.join(driverRoot, "integrations/workbuddy/server/workbench-task.ps1"),
|
|
63
|
+
"-Command", operation, "-Root", options.root, "-Manifest", options.expectedManifestSha256,
|
|
64
|
+
"-DriverRoot", driverRoot,
|
|
65
|
+
"-NodePath", options.nodePath || process.execPath,
|
|
66
|
+
"-StoreRoot", options.storeRoot || defaultNeutralHostStoreRoot()], {
|
|
67
|
+
windowsHide: true, timeout: 30_000, maxBuffer: 4096,
|
|
68
|
+
env: runtimeSafety.sanitizeHelloLynkChildEnvironment(process.env),
|
|
69
|
+
});
|
|
70
|
+
const result = JSON.parse(stdout);
|
|
71
|
+
if (result?.ok !== true) throw failure("workbuddy_page_task_failed");
|
|
72
|
+
return result;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const code = String(error?.stderr || "").trim();
|
|
75
|
+
throw failure(/^workbuddy_[a-z_]+$/.test(code) ? code : "workbuddy_page_task_failed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Uses the same installed, hidden launcher and pinned verifier as the page
|
|
80
|
+
// service, with a separate task identity and explicit loopback CDP opt-in.
|
|
81
|
+
export async function configureWorkBuddyNativeTaskService(options, operation = "Install") {
|
|
82
|
+
if (!["Install", "Uninstall"].includes(operation)) return configureNativeTaskService(options, operation);
|
|
83
|
+
bootstrapArgs("native-task-service", options);
|
|
84
|
+
const endpoint = new URL(options.debugEndpoint || "");
|
|
85
|
+
if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.username || endpoint.password) throw failure("workbuddy_native_debug_endpoint_invalid");
|
|
86
|
+
// MCP connection can initialize a new proxy which reads the service record.
|
|
87
|
+
// Complete that handshake before taking the record/Start lock.
|
|
88
|
+
const debugStartup = operation === "Install" ? await prepareNativeTaskServiceInstall(options, endpoint) : undefined;
|
|
89
|
+
return withWorkBuddyNativeServiceRecord(options.storeRoot, async registry => {
|
|
90
|
+
if (!registry && operation === "Install") throw failure("workbuddy_native_service_store_missing");
|
|
91
|
+
// Withdraw before stopping, under the same lock used by MCP Verify/Start.
|
|
92
|
+
if (operation === "Uninstall") await registry?.removeOwned(options);
|
|
93
|
+
const result = await configureNativeTaskService(options, operation, debugStartup);
|
|
94
|
+
if (operation === "Install") await registry.save(options, result.taskName);
|
|
95
|
+
// Without the service nobody imports hook records; leave WorkBuddy's
|
|
96
|
+
// other hooks and settings untouched.
|
|
97
|
+
if (operation === "Uninstall") result.originHook = await removeWorkBuddyOriginHook({ settingsPath: options.workBuddySettingsPath,
|
|
98
|
+
command: workBuddyOriginHookCommand({ nodePath: options.nodePath || process.execPath,
|
|
99
|
+
hookPath: path.join(options.root, "integrations/workbuddy/server/origin-hook.mjs"),
|
|
100
|
+
inbox: workBuddyOriginInbox(options.storeRoot || defaultNeutralHostStoreRoot()) }) })
|
|
101
|
+
.catch(() => ({ changed: false, status: "unavailable" }));
|
|
102
|
+
return result;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// An ordinary package upgrade must carry forward an existing listener even
|
|
107
|
+
// while WorkBuddy is closed. First customer install explicitly enables it. The
|
|
108
|
+
// new release's MCP command needs WorkBuddy's trust again: leave one request,
|
|
109
|
+
// which that release's own service approves once WorkBuddy is open.
|
|
110
|
+
export async function updateRecordedWorkBuddyNativeTaskService(options, operation = "Install") {
|
|
111
|
+
if (!["Install", "Verify"].includes(operation)) throw failure("workbuddy_page_task_operation_invalid");
|
|
112
|
+
let pendingService = null;
|
|
113
|
+
const updated = await withWorkBuddyNativeServiceRecord(options.storeRoot, async registry => {
|
|
114
|
+
const record = await registry?.read();
|
|
115
|
+
if (!record) {
|
|
116
|
+
if (operation !== "Install" || options.installIfMissing !== true) return { configured: false, status: "not_installed" };
|
|
117
|
+
if (!registry) throw failure("workbuddy_native_service_store_missing");
|
|
118
|
+
const target = { ...options, nodePath: options.nodePath || process.execPath };
|
|
119
|
+
await loadPinnedWorkBuddyProxy(target);
|
|
120
|
+
const startupOptions = { storeRoot: registry.storeRoot, debugEndpoint: target.debugEndpoint,
|
|
121
|
+
installationId: nativeDebugStartupOwner(target), enableDebugStartup: true,
|
|
122
|
+
...(options.debugStartupEnvironment ? { environment: options.debugStartupEnvironment } : {}) };
|
|
123
|
+
const debugStartup = await configureWorkBuddyDebugStartup(startupOptions, "Install");
|
|
124
|
+
target.debugEndpoint = debugStartup.debugEndpoint;
|
|
125
|
+
try {
|
|
126
|
+
const result = await configureNativeTaskService(target, "Install", debugStartup);
|
|
127
|
+
// Trust is completed by this receiver after the client reopens. Setup
|
|
128
|
+
// does not require or attach to a running WorkBuddy window.
|
|
129
|
+
await registry.save({ ...target, pendingMcpTrust: { command: target.nodePath } }, result.taskName);
|
|
130
|
+
pendingService = target;
|
|
131
|
+
return { ...result, configured: true, restartRequired: true };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
let rollbackError;
|
|
134
|
+
// Each cleanup is independent: a Scheduler failure must not leave our
|
|
135
|
+
// newly enabled startup setting behind. Preserve any user change.
|
|
136
|
+
for (const undo of [() => registry.removeOwned(target),
|
|
137
|
+
() => configureNativeTaskService({ ...target, debugStartupEnvironment: options.debugStartupEnvironment }, "Uninstall"),
|
|
138
|
+
() => configureWorkBuddyDebugStartup(startupOptions, "Uninstall")]) {
|
|
139
|
+
try { await undo(); } catch (cause) { rollbackError ||= cause; }
|
|
140
|
+
}
|
|
141
|
+
if (rollbackError) {
|
|
142
|
+
throw Object.assign(failure("local_configuration_outcome_uncertain"), { cause: rollbackError, configurationCause: error });
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const target = { ...options, nodePath: record.nodePath, debugEndpoint: record.debugEndpoint };
|
|
148
|
+
const sameRelease = path.resolve(record.root).toLowerCase() === path.resolve(target.root).toLowerCase()
|
|
149
|
+
&& record.expectedManifestSha256 === target.expectedManifestSha256;
|
|
150
|
+
if (operation === "Verify" && !sameRelease) throw failure("workbuddy_native_service_record_mismatch");
|
|
151
|
+
await loadPinnedWorkBuddyProxy(target);
|
|
152
|
+
const result = await configureNativeTaskService(target, operation);
|
|
153
|
+
if (operation === "Install") {
|
|
154
|
+
await transferWorkBuddyDebugStartupOwner({ storeRoot: registry.storeRoot,
|
|
155
|
+
previousInstallationId: nativeDebugStartupOwner(record), installationId: nativeDebugStartupOwner(target) });
|
|
156
|
+
// The installer wrote mcp.json with this process's node.
|
|
157
|
+
const pendingMcpTrust = sameRelease ? record.pendingMcpTrust : { command: process.execPath };
|
|
158
|
+
await registry.save({ ...target, pendingMcpTrust }, result.taskName);
|
|
159
|
+
if (pendingMcpTrust) pendingService = target;
|
|
160
|
+
}
|
|
161
|
+
return { ...result, configured: true };
|
|
162
|
+
});
|
|
163
|
+
// After the lock: the MCP that approval connects takes it to Verify/Start.
|
|
164
|
+
if (pendingService) updated.serviceStarted = await (options.startNativeService || (target => configureNativeTaskService(target, "Start")))(pendingService)
|
|
165
|
+
.then(() => true, () => false);
|
|
166
|
+
return updated;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function prepareNativeTaskServiceInstall(options, endpoint) {
|
|
170
|
+
// Explicit opt-in only. Persist ownership before configuring the task so a
|
|
171
|
+
// later repair/uninstall can recover a partially completed installation.
|
|
172
|
+
const debugStartup = await configureWorkBuddyDebugStartup({
|
|
173
|
+
storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(), debugEndpoint: endpoint.origin,
|
|
174
|
+
installationId: nativeDebugStartupOwner(options),
|
|
175
|
+
enableDebugStartup: options.enableDebugStartup === true,
|
|
176
|
+
}, "Install");
|
|
177
|
+
await registerWorkBuddyNativeMcp({
|
|
178
|
+
debugEndpoint: endpoint.origin,
|
|
179
|
+
command: options.nodePath || process.execPath,
|
|
180
|
+
releaseRoot: options.mcpRegistration?.releaseRoot || options.root,
|
|
181
|
+
expectedManifestSha256: options.mcpRegistration?.expectedManifestSha256 || options.expectedManifestSha256,
|
|
182
|
+
storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(),
|
|
183
|
+
});
|
|
184
|
+
return debugStartup;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function configureNativeTaskService(options, operation, debugStartup) {
|
|
188
|
+
bootstrapArgs("native-task-service", options);
|
|
189
|
+
const endpoint = new URL(options.debugEndpoint || "");
|
|
190
|
+
if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.username || endpoint.password) throw failure("workbuddy_native_debug_endpoint_invalid");
|
|
191
|
+
if (!["Install", "Verify", "Start", "Uninstall"].includes(operation)) throw failure("workbuddy_page_task_operation_invalid");
|
|
192
|
+
try {
|
|
193
|
+
const { stdout } = await exec("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File",
|
|
194
|
+
path.join(options.root, "integrations/workbuddy/server/workbench-task.ps1"), "-Command", operation,
|
|
195
|
+
"-Root", options.root, "-Manifest", options.expectedManifestSha256, "-NodePath", options.nodePath || process.execPath,
|
|
196
|
+
"-StoreRoot", options.storeRoot || defaultNeutralHostStoreRoot(), "-Mode", "native-task-service", "-DebugEndpoint", endpoint.origin], {
|
|
197
|
+
windowsHide: true, timeout: 30_000, maxBuffer: 4096, env: runtimeSafety.sanitizeHelloLynkChildEnvironment(process.env),
|
|
198
|
+
});
|
|
199
|
+
const result = JSON.parse(stdout); if (result?.ok !== true) throw failure("workbuddy_page_task_failed");
|
|
200
|
+
if (operation === "Uninstall") result.debugStartup = await configureWorkBuddyDebugStartup({
|
|
201
|
+
storeRoot: options.storeRoot || defaultNeutralHostStoreRoot(), debugEndpoint: endpoint.origin,
|
|
202
|
+
installationId: nativeDebugStartupOwner(options),
|
|
203
|
+
...(options.debugStartupEnvironment ? { environment: options.debugStartupEnvironment } : {}),
|
|
204
|
+
}, operation);
|
|
205
|
+
else if (debugStartup) result.debugStartup = debugStartup;
|
|
206
|
+
return result;
|
|
207
|
+
} catch { throw failure("workbuddy_native_service_configuration_failed"); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function nativeDebugStartupOwner(options) {
|
|
211
|
+
return crypto.createHash("sha256").update(`${path.resolve(options.root).toLowerCase()}|${options.expectedManifestSha256}`).digest("hex");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function controlPipe(options) {
|
|
215
|
+
const key = crypto.createHash("sha256").update(`${options.root.toLowerCase()}|${options.expectedManifestSha256}`).digest("hex");
|
|
216
|
+
return `\\\\.\\pipe\\hellolynk-workbuddy-${key}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function requestExistingWorkbench(options) {
|
|
220
|
+
return new Promise((resolve, reject) => {
|
|
221
|
+
const socket = net.connect(controlPipe(options));
|
|
222
|
+
let text = "";
|
|
223
|
+
socket.setTimeout(3000, () => socket.destroy(failure("workbuddy_page_service_timeout")));
|
|
224
|
+
socket.on("error", reject);
|
|
225
|
+
socket.on("connect", () => socket.write("open\n"));
|
|
226
|
+
socket.on("data", (chunk) => {
|
|
227
|
+
text += chunk.toString("utf8");
|
|
228
|
+
if (text.length > 4096) socket.destroy(failure("workbuddy_local_web_launch_invalid"));
|
|
229
|
+
});
|
|
230
|
+
socket.on("end", () => {
|
|
231
|
+
try { resolve(validateReady(JSON.parse(text))); }
|
|
232
|
+
catch { reject(failure("workbuddy_local_web_launch_invalid")); }
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function launchScheduledWorkbench(options) {
|
|
238
|
+
try { return await requestExistingWorkbench(options); }
|
|
239
|
+
catch (error) { if (!["ENOENT", "ECONNREFUSED"].includes(error?.code)) throw error; }
|
|
240
|
+
await configureWorkBuddyWorkbenchTask(options, "Start");
|
|
241
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
242
|
+
do {
|
|
243
|
+
await delay(200);
|
|
244
|
+
try { return await requestExistingWorkbench(options); }
|
|
245
|
+
catch (error) { if (!["ENOENT", "ECONNREFUSED"].includes(error?.code)) throw error; }
|
|
246
|
+
} while (Date.now() < deadline);
|
|
247
|
+
throw failure("workbuddy_local_web_launch_timeout");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function serveWorkBuddyWorkbenchService(options) {
|
|
251
|
+
let server;
|
|
252
|
+
const sockets = new Set();
|
|
253
|
+
try {
|
|
254
|
+
await serveWorkBuddyWorkbench({ ...options, async publishReady(value, surface) {
|
|
255
|
+
server = net.createServer((socket) => {
|
|
256
|
+
sockets.add(socket);
|
|
257
|
+
socket.on("close", () => sockets.delete(socket));
|
|
258
|
+
socket.on("error", () => {});
|
|
259
|
+
socket.setTimeout(3000, () => socket.destroy());
|
|
260
|
+
let text = "";
|
|
261
|
+
socket.on("data", (chunk) => {
|
|
262
|
+
text += chunk.toString("utf8");
|
|
263
|
+
if (text === "open\n") { surface.armNavigation(); socket.end(`${JSON.stringify(validateReady(value))}\n`); }
|
|
264
|
+
else if (text.length >= 5) socket.destroy();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
await new Promise((resolve, reject) => {
|
|
268
|
+
server.once("error", reject);
|
|
269
|
+
server.listen(controlPipe(options), () => { server.off("error", reject); resolve(); });
|
|
270
|
+
});
|
|
271
|
+
} });
|
|
272
|
+
} finally {
|
|
273
|
+
for (const socket of sockets) socket.destroy();
|
|
274
|
+
if (server?.listening) await new Promise((resolve) => server.close(resolve));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function handoffWorkBuddyWorkbench(options) {
|
|
279
|
+
const executable = await runtimeSafety.resolveHelloLynkStandaloneNode();
|
|
280
|
+
const child = spawn(executable, bootstrapArgs("workbench", options), {
|
|
281
|
+
cwd: options.root,
|
|
282
|
+
env: runtimeSafety.standaloneHelloLynkNodeEnvironment(process.env),
|
|
283
|
+
detached: true,
|
|
284
|
+
windowsHide: true,
|
|
285
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
286
|
+
});
|
|
287
|
+
try {
|
|
288
|
+
const ready = await new Promise((resolve, reject) => {
|
|
289
|
+
const timer = setTimeout(() => done(failure("workbuddy_local_web_launch_timeout")), STARTUP_TIMEOUT_MS);
|
|
290
|
+
const failed = () => done(failure("workbuddy_local_web_launch_failed"));
|
|
291
|
+
const message = (value) => {
|
|
292
|
+
try { done(null, validateReady(value)); }
|
|
293
|
+
catch { failed(); }
|
|
294
|
+
};
|
|
295
|
+
const done = (error, value) => {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
child.off("message", message);
|
|
298
|
+
child.off("error", failed);
|
|
299
|
+
child.off("exit", failed);
|
|
300
|
+
if (error) reject(error); else resolve(value);
|
|
301
|
+
};
|
|
302
|
+
child.once("error", failed);
|
|
303
|
+
child.once("exit", failed);
|
|
304
|
+
child.once("message", message);
|
|
305
|
+
});
|
|
306
|
+
child.disconnect();
|
|
307
|
+
child.unref();
|
|
308
|
+
return ready;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
child.kill();
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export async function serveWorkBuddyWorkbench(options) {
|
|
316
|
+
const { adapter, signal, publishReady = sendReady } = options;
|
|
317
|
+
let page;
|
|
318
|
+
try {
|
|
319
|
+
page = await bootstrapWorkBuddyWorkbenchCandidate({
|
|
320
|
+
adapter, pagePath: "/inbox",
|
|
321
|
+
...(options.bootstrapWorkbenchCandidate ? {
|
|
322
|
+
bootstrapWorkbenchCandidate: options.bootstrapWorkbenchCandidate,
|
|
323
|
+
workbenchContract: options.workbenchContract,
|
|
324
|
+
} : {}),
|
|
325
|
+
});
|
|
326
|
+
if (signal?.aborted) return;
|
|
327
|
+
await publishReady({ cleanUrl: page.cleanUrl }, page.surface);
|
|
328
|
+
while (!signal?.aborted) {
|
|
329
|
+
const state = page.surface.status();
|
|
330
|
+
// Reuse the existing navigation and session expiry rules. Keeping a
|
|
331
|
+
// browser page alive does not grant any extra Host or message access.
|
|
332
|
+
if (!state.navigationArmed && state.liveSessionCount === 0) return;
|
|
333
|
+
try { await delay(options.checkIntervalMs ?? 1000, undefined, { signal }); }
|
|
334
|
+
catch (error) { if (!signal?.aborted) throw error; }
|
|
335
|
+
}
|
|
336
|
+
} finally {
|
|
337
|
+
try { await page?.close(); }
|
|
338
|
+
finally { await adapter.close(); }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function bootstrapArgs(mode, options) {
|
|
343
|
+
if (!path.isAbsolute(options?.root || "")
|
|
344
|
+
|| path.resolve(options.root) !== options.root
|
|
345
|
+
|| !/^[a-f0-9]{64}$/.test(options?.expectedManifestSha256 || "")) {
|
|
346
|
+
throw failure("workbuddy_local_web_launch_configuration_invalid");
|
|
347
|
+
}
|
|
348
|
+
return [path.join(options.root, "integrations/workbuddy/server/package-bootstrap.mjs"),
|
|
349
|
+
mode, "--root", options.root, "--expected-manifest-sha256", options.expectedManifestSha256];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function validateReady(value) {
|
|
353
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
354
|
+
|| Object.keys(value).length !== 1 || typeof value.cleanUrl !== "string"
|
|
355
|
+
|| !/^http:\/\/127\.0\.0\.1:\d{1,5}\/inbox$/.test(value.cleanUrl)) {
|
|
356
|
+
throw failure("workbuddy_local_web_launch_invalid");
|
|
357
|
+
}
|
|
358
|
+
const port = Number(new URL(value.cleanUrl).port);
|
|
359
|
+
if (port < 1 || port > 65535) throw failure("workbuddy_local_web_launch_invalid");
|
|
360
|
+
return Object.freeze({ cleanUrl: value.cleanUrl });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function sendReady(value) {
|
|
364
|
+
return new Promise((resolve, reject) => {
|
|
365
|
+
if (!process.connected || typeof process.send !== "function") {
|
|
366
|
+
reject(failure("workbuddy_local_web_launch_handoff_missing"));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
process.send(validateReady(value), (error) => error ? reject(error) : resolve());
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function failure(code) { return Object.assign(new Error(code), { code }); }
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory=$true)][ValidateSet('Install','Verify','Start','Run','Uninstall')][string]$Command,
|
|
3
|
+
[Parameter(Mandatory=$true)][string]$Root,
|
|
4
|
+
[Parameter(Mandatory=$true)][ValidatePattern('^[a-f0-9]{64}$')][string]$Manifest,
|
|
5
|
+
[Parameter(Mandatory=$true)][string]$NodePath,
|
|
6
|
+
[Parameter(Mandatory=$true)][string]$StoreRoot,
|
|
7
|
+
[string]$DriverRoot = '',
|
|
8
|
+
[ValidateSet('workbench-service','native-task-service')][string]$Mode = 'workbench-service',
|
|
9
|
+
[string]$DebugEndpoint = ''
|
|
10
|
+
)
|
|
11
|
+
$ErrorActionPreference = 'Stop'
|
|
12
|
+
try {
|
|
13
|
+
$nativeService = $Mode -eq 'native-task-service'
|
|
14
|
+
if (-not $DriverRoot) { $DriverRoot = $Root }
|
|
15
|
+
# A new, verified installer may check an older immutable release with its
|
|
16
|
+
# current task driver. It may not mutate that release through this path.
|
|
17
|
+
if ($DriverRoot -ne $Root -and $Command -ne 'Verify') { throw 'workbuddy_page_task_path_invalid' }
|
|
18
|
+
if ($nativeService -and ($DebugEndpoint -notmatch '^http://127\.0\.0\.1:[0-9]{1,5}$' -or ([uri]$DebugEndpoint).Port -lt 1)) { throw 'workbuddy_native_debug_endpoint_invalid' }
|
|
19
|
+
if (-not $nativeService -and $DebugEndpoint) { throw 'workbuddy_native_debug_endpoint_invalid' }
|
|
20
|
+
if (-not [IO.Path]::IsPathRooted($Root) -or $Root -match '["\r\n]' -or
|
|
21
|
+
-not [IO.Path]::IsPathRooted($NodePath) -or $NodePath -match '["\r\n]' -or
|
|
22
|
+
-not [IO.Path]::IsPathRooted($StoreRoot) -or $StoreRoot -match '["\r\n]' -or
|
|
23
|
+
[IO.Path]::GetFileName($StoreRoot) -ne 'host') { throw 'workbuddy_page_task_path_invalid' }
|
|
24
|
+
$entry = Join-Path $Root 'integrations\workbuddy\server\package-bootstrap.mjs'
|
|
25
|
+
$script = Join-Path $DriverRoot 'integrations\workbuddy\server\workbench-task.ps1'
|
|
26
|
+
$launcher = Join-Path $Root 'integrations\workbuddy\server\workbench-hidden-launcher.vbs'
|
|
27
|
+
$wscript = Join-Path $env:SystemRoot 'System32\wscript.exe'
|
|
28
|
+
if (-not (Test-Path -LiteralPath $entry -PathType Leaf) -or -not (Test-Path -LiteralPath $launcher -PathType Leaf) -or
|
|
29
|
+
-not (Test-Path -LiteralPath $wscript -PathType Leaf) -or -not (Test-Path -LiteralPath $NodePath -PathType Leaf) -or
|
|
30
|
+
[IO.Path]::GetFullPath($PSCommandPath) -ne [IO.Path]::GetFullPath($script)) { throw 'workbuddy_page_task_entry_invalid' }
|
|
31
|
+
if ($Command -eq 'Run') {
|
|
32
|
+
$arguments = '//B //NoLogo "{0}" "{1}" "{2}" "{3}" {4} "{5}"' -f $launcher,$NodePath,$entry,$Root,$Manifest,$StoreRoot
|
|
33
|
+
if ($nativeService) { $arguments += ' native-task-service "{0}"' -f $DebugEndpoint }
|
|
34
|
+
$worker = Start-Process -FilePath $wscript -ArgumentList $arguments -WindowStyle Hidden -PassThru -Wait
|
|
35
|
+
exit $worker.ExitCode
|
|
36
|
+
}
|
|
37
|
+
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
38
|
+
$namespaceRoot = [IO.Path]::GetDirectoryName($Root)
|
|
39
|
+
if ([IO.Path]::GetFileName($namespaceRoot) -ne 'releases') { $namespaceRoot = $Root }
|
|
40
|
+
$namespace = $sid + '|' + [IO.Path]::GetFullPath($namespaceRoot).ToLowerInvariant()
|
|
41
|
+
$hash = [Security.Cryptography.SHA256]::Create()
|
|
42
|
+
try {
|
|
43
|
+
$suffix = ([BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($namespace)))).Replace('-','').Substring(0,20)
|
|
44
|
+
$releaseIdentity = [IO.Path]::GetFullPath($Root).ToLowerInvariant() + '|' + $Manifest
|
|
45
|
+
$releaseSuffix = ([BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($releaseIdentity)))).Replace('-','').Substring(0,16)
|
|
46
|
+
}
|
|
47
|
+
finally { $hash.Dispose() }
|
|
48
|
+
$taskBase = "HelloLynk WorkBuddy - $suffix"
|
|
49
|
+
if ($nativeService) { $taskBase = "HelloLynk WorkBuddy Native - $suffix" }
|
|
50
|
+
$taskName = "$taskBase - $releaseSuffix"
|
|
51
|
+
$description = 'HelloLynk WorkBuddy page service; starts on demand and stops when browser sessions expire.'
|
|
52
|
+
if ($nativeService) { $description = 'HelloLynk WorkBuddy native inbox listener; explicit loopback desktop bridge; no model polling.' }
|
|
53
|
+
$executable = $wscript
|
|
54
|
+
$arguments = '//B //NoLogo "{0}" "{1}" "{2}" "{3}" {4} "{5}"' -f $launcher,$NodePath,$entry,$Root,$Manifest,$StoreRoot
|
|
55
|
+
if ($nativeService) { $arguments += ' native-task-service "{0}"' -f $DebugEndpoint }
|
|
56
|
+
# The native listener also resumes at this user's logon with normal rights,
|
|
57
|
+
# so an update's trust request survives a restart before WorkBuddy opens.
|
|
58
|
+
# One instance per release. The page task stays on demand.
|
|
59
|
+
$instances = 0; $installedTrigger = 'none'
|
|
60
|
+
if ($nativeService) { $instances = 2; $installedTrigger = 'logon' }
|
|
61
|
+
# Use the Scheduler API directly; its CIM wrapper can time out while the
|
|
62
|
+
# registered task and on-demand service are both healthy.
|
|
63
|
+
$scheduler = New-Object -ComObject 'Schedule.Service'; $scheduler.Connect()
|
|
64
|
+
$folder = $scheduler.GetFolder('\')
|
|
65
|
+
function Read-Task($registered) {
|
|
66
|
+
$d = $registered.Definition
|
|
67
|
+
[pscustomobject]@{ TaskName=$registered.Name; Description=$d.RegistrationInfo.Description; Principal=$d.Principal; Settings=$d.Settings
|
|
68
|
+
Triggers=@($d.Triggers); Actions=@($d.Actions | ForEach-Object { [pscustomobject]@{Execute=$_.Path; Arguments=$_.Arguments; WorkingDirectory=$_.WorkingDirectory; Type=$_.Type} })
|
|
69
|
+
State=$registered.State; Registered=$registered }
|
|
70
|
+
}
|
|
71
|
+
# 'none', 'logon' (this user's logon trigger), 'logon_disabled' or 'other'.
|
|
72
|
+
function Get-TriggerState($triggers) {
|
|
73
|
+
$list = @($triggers | Where-Object { $null -ne $_ })
|
|
74
|
+
if ($list.Count -eq 0) { return 'none' }
|
|
75
|
+
if (-not $nativeService -or $list.Count -ne 1 -or $list[0].Type -ne 9) { return 'other' }
|
|
76
|
+
$user = [string]$list[0].UserId
|
|
77
|
+
if ($user -notmatch '^S-1-') { $user = (New-Object Security.Principal.NTAccount($user)).Translate([Security.Principal.SecurityIdentifier]).Value }
|
|
78
|
+
if ($user -ne $sid) { return 'other' }
|
|
79
|
+
if ($list[0].Enabled) { return 'logon' }
|
|
80
|
+
return 'logon_disabled'
|
|
81
|
+
}
|
|
82
|
+
$task = $null
|
|
83
|
+
try { $task = Read-Task ($folder.GetTask($taskName)) }
|
|
84
|
+
catch { if ($_.Exception.HResult -ne -2147024894) { throw } }
|
|
85
|
+
if ($task) {
|
|
86
|
+
$taskSid = [string]$task.Principal.UserId
|
|
87
|
+
if ($taskSid -notmatch '^S-1-') { $taskSid = (New-Object Security.Principal.NTAccount($taskSid)).Translate([Security.Principal.SecurityIdentifier]).Value }
|
|
88
|
+
if ($task.Description -ne $description -or $taskSid -ne $sid) { throw 'workbuddy_page_task_conflict' }
|
|
89
|
+
}
|
|
90
|
+
$namespaceTasks = @($folder.GetTasks(1) | Where-Object { $_.Name -eq $taskBase -or $_.Name.StartsWith($taskBase + ' - ') } | ForEach-Object { Read-Task $_ })
|
|
91
|
+
foreach ($candidate in $namespaceTasks) {
|
|
92
|
+
$candidateSid = [string]$candidate.Principal.UserId
|
|
93
|
+
if ($candidateSid -notmatch '^S-1-') { $candidateSid = (New-Object Security.Principal.NTAccount($candidateSid)).Translate([Security.Principal.SecurityIdentifier]).Value }
|
|
94
|
+
if ($candidate.Description -ne $description -or $candidateSid -ne $sid) { throw 'workbuddy_page_task_conflict' }
|
|
95
|
+
}
|
|
96
|
+
function Stop-VerifiedNativeTaskProcesses($candidate) {
|
|
97
|
+
# Task Scheduler can stop wscript while leaving its shell.Run child alive.
|
|
98
|
+
# Match the entire verified native command, never a generic node process.
|
|
99
|
+
if (@($candidate.Actions).Count -ne 1 -or $candidate.Actions[0].Execute -ne $wscript) { throw 'workbuddy_native_service_identity_invalid' }
|
|
100
|
+
$pattern = '^//B //NoLogo "([^"\r\n]+)" "([^"\r\n]+)" "([^"\r\n]+)" "([^"\r\n]+)" ([a-f0-9]{64}) "([^"\r\n]+)" native-task-service "(http://127\.0\.0\.1:[0-9]{1,5})"$'
|
|
101
|
+
$match = [regex]::Match([string]$candidate.Actions[0].Arguments, $pattern)
|
|
102
|
+
if (-not $match.Success) { throw 'workbuddy_native_service_identity_invalid' }
|
|
103
|
+
$oldLauncher = $match.Groups[1].Value; $oldNode = $match.Groups[2].Value
|
|
104
|
+
$oldEntry = $match.Groups[3].Value; $oldRoot = $match.Groups[4].Value
|
|
105
|
+
$oldManifest = $match.Groups[5].Value; $oldStore = $match.Groups[6].Value
|
|
106
|
+
if (-not [IO.Path]::IsPathRooted($oldRoot) -or $oldNode -ne $NodePath -or $oldStore -ne $StoreRoot -or
|
|
107
|
+
$oldLauncher -ne (Join-Path $oldRoot 'integrations\workbuddy\server\workbench-hidden-launcher.vbs') -or
|
|
108
|
+
$oldEntry -ne (Join-Path $oldRoot 'integrations\workbuddy\server\package-bootstrap.mjs')) { throw 'workbuddy_native_service_identity_invalid' }
|
|
109
|
+
$oldNamespace = [IO.Path]::GetDirectoryName($oldRoot)
|
|
110
|
+
if ([IO.Path]::GetFileName($oldNamespace) -ne 'releases') { $oldNamespace = $oldRoot }
|
|
111
|
+
if ([IO.Path]::GetFullPath($oldNamespace) -ne [IO.Path]::GetFullPath($namespaceRoot)) { throw 'workbuddy_native_service_identity_invalid' }
|
|
112
|
+
$identityHash = [Security.Cryptography.SHA256]::Create()
|
|
113
|
+
try { $oldSuffix = ([BitConverter]::ToString($identityHash.ComputeHash([Text.Encoding]::UTF8.GetBytes([IO.Path]::GetFullPath($oldRoot).ToLowerInvariant() + '|' + $oldManifest)))).Replace('-','').Substring(0,16) }
|
|
114
|
+
finally { $identityHash.Dispose() }
|
|
115
|
+
if ($candidate.TaskName -ne "$taskBase - $oldSuffix") { throw 'workbuddy_native_service_identity_invalid' }
|
|
116
|
+
$nodeTail = '"{0}" native-task-service --root "{1}" --expected-manifest-sha256 {2}' -f $oldEntry,$oldRoot,$oldManifest
|
|
117
|
+
# WScript.Shell.Run can add a second space after the executable. Only
|
|
118
|
+
# that argv separator varies; all paths, mode and manifest remain exact.
|
|
119
|
+
$nodeCommandPattern = '^(?:"' + [regex]::Escape($oldNode) + '"|' + [regex]::Escape($oldNode) + ')[ \t]+' + [regex]::Escape($nodeTail) + '$'
|
|
120
|
+
$helperCommandPattern = '^(?:"' + [regex]::Escape($wscript) + '"|' + [regex]::Escape($wscript) + ')[ \t]+' + [regex]::Escape([string]$candidate.Actions[0].Arguments) + '$'
|
|
121
|
+
foreach ($observed in @(Get-CimInstance Win32_Process -Filter "Name = 'node.exe' OR Name = 'wscript.exe'")) {
|
|
122
|
+
$observedCommand = ([string]$observed.CommandLine).Trim()
|
|
123
|
+
if (-not (($observed.ExecutablePath -eq $oldNode -and $observedCommand -match $nodeCommandPattern) -or
|
|
124
|
+
($observed.ExecutablePath -eq $wscript -and $observedCommand -match $helperCommandPattern))) { continue }
|
|
125
|
+
if ((Invoke-CimMethod -InputObject $observed -MethodName GetOwnerSid).Sid -ne $sid) { continue }
|
|
126
|
+
$process = $null
|
|
127
|
+
try {
|
|
128
|
+
$process = Get-Process -Id $observed.ProcessId -ErrorAction SilentlyContinue
|
|
129
|
+
if (-not $process) { continue }
|
|
130
|
+
$openedHandle = $process.Handle
|
|
131
|
+
$current = Get-CimInstance Win32_Process -Filter "ProcessId = $($observed.ProcessId)"
|
|
132
|
+
if (-not $current -or $current.CreationDate -ne $observed.CreationDate -or $current.CommandLine -ne $observed.CommandLine -or $current.ExecutablePath -ne $observed.ExecutablePath) { continue }
|
|
133
|
+
$process.Kill()
|
|
134
|
+
if (-not $process.WaitForExit(5000)) { throw 'workbuddy_native_service_stop_timeout' }
|
|
135
|
+
} finally { if ($process) { $process.Dispose() } }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if ($Command -eq 'Uninstall') {
|
|
139
|
+
foreach ($candidate in $namespaceTasks) {
|
|
140
|
+
# An obsolete release may be uninstalled after its replacement is active.
|
|
141
|
+
# Its manifest/root identity only owns this exact task, not the namespace.
|
|
142
|
+
if ($candidate.TaskName -ne $taskName) { continue }
|
|
143
|
+
if ($nativeService) { Stop-VerifiedNativeTaskProcesses $candidate }
|
|
144
|
+
$candidate.Registered.Stop(0)
|
|
145
|
+
$folder.DeleteTask($candidate.TaskName, 0)
|
|
146
|
+
}
|
|
147
|
+
} elseif ($Command -eq 'Install') {
|
|
148
|
+
if ($task -and @($task.Actions).Count -eq 1 -and $task.Actions[0].Type -eq 0 -and $task.Actions[0].Execute -eq $executable -and $task.Actions[0].Arguments -eq $arguments -and $task.Actions[0].WorkingDirectory -eq $Root -and
|
|
149
|
+
(Get-TriggerState $task.Triggers) -eq $installedTrigger -and $task.Principal.RunLevel -eq 0 -and $task.Principal.LogonType -eq 3 -and
|
|
150
|
+
$task.Settings.MultipleInstances -eq $instances -and $task.Settings.ExecutionTimeLimit -eq 'PT0S' -and $task.Settings.AllowDemandStart) {
|
|
151
|
+
# The exact current package is already registered; leave its page alone.
|
|
152
|
+
} else {
|
|
153
|
+
# Registering a new immutable release must not terminate a page that a
|
|
154
|
+
# user already has open. Different releases use different control pipes;
|
|
155
|
+
# Parallel lets the new release start while the old page drains. Calls
|
|
156
|
+
# for the same release still reuse its existing pipe before task start.
|
|
157
|
+
$definition = $scheduler.NewTask(0); $definition.RegistrationInfo.Description = $description
|
|
158
|
+
$definition.Principal.UserId = $sid; $definition.Principal.LogonType = 3; $definition.Principal.RunLevel = 0
|
|
159
|
+
$definition.Settings.MultipleInstances = $instances; $definition.Settings.ExecutionTimeLimit = 'PT0S'; $definition.Settings.AllowDemandStart = $true
|
|
160
|
+
$definition.Settings.DisallowStartIfOnBatteries = $false; $definition.Settings.StopIfGoingOnBatteries = $false
|
|
161
|
+
if ($nativeService) { $logon = $definition.Triggers.Create(9); $logon.UserId = $sid }
|
|
162
|
+
$action = $definition.Actions.Create(0); $action.Path = $executable; $action.Arguments = $arguments; $action.WorkingDirectory = $Root
|
|
163
|
+
$null = $folder.RegisterTaskDefinition($taskName, $definition, 6, $sid, $null, 3)
|
|
164
|
+
}
|
|
165
|
+
foreach ($candidate in $namespaceTasks) {
|
|
166
|
+
if ($nativeService -and $candidate.TaskName -ne $taskName) {
|
|
167
|
+
Stop-VerifiedNativeTaskProcesses $candidate
|
|
168
|
+
$candidate.Registered.Stop(0)
|
|
169
|
+
}
|
|
170
|
+
if ($candidate.TaskName -ne $taskName -and ($nativeService -or $candidate.State -ne 4)) {
|
|
171
|
+
$folder.DeleteTask($candidate.TaskName, 0)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
if (-not $task) { throw 'workbuddy_page_task_missing' }
|
|
176
|
+
# A logon trigger the user removed or disabled still leaves on-demand start.
|
|
177
|
+
if (@($task.Actions).Count -ne 1 -or $task.Actions[0].Type -ne 0 -or $task.Actions[0].Execute -ne $executable -or $task.Actions[0].Arguments -ne $arguments -or $task.Actions[0].WorkingDirectory -ne $Root -or
|
|
178
|
+
(Get-TriggerState $task.Triggers) -eq 'other' -or $task.Principal.RunLevel -ne 0 -or $task.Principal.LogonType -ne 3 -or
|
|
179
|
+
$task.Settings.MultipleInstances -ne $instances -or $task.Settings.ExecutionTimeLimit -ne 'PT0S' -or -not $task.Settings.AllowDemandStart) { throw 'workbuddy_page_task_configuration_invalid' }
|
|
180
|
+
if ($Command -eq 'Start' -and $task.State -ne 4) {
|
|
181
|
+
for ($attempt = 0; $attempt -lt 3; $attempt += 1) {
|
|
182
|
+
try { $null = $task.Registered.Run($null); break }
|
|
183
|
+
catch {
|
|
184
|
+
if ($attempt -eq 2) { throw }
|
|
185
|
+
Start-Sleep -Milliseconds (100 * ($attempt + 1))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
[pscustomobject]@{ok=$true;operation=$Command;taskName=$taskName} | ConvertTo-Json -Compress
|
|
191
|
+
} catch {
|
|
192
|
+
$code = [string]$_.Exception.Message
|
|
193
|
+
if ($code -notmatch '^workbuddy_[a-z_]+$') { $code = 'workbuddy_page_task_failed' }
|
|
194
|
+
[Console]::Error.WriteLine($code)
|
|
195
|
+
exit 1
|
|
196
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hellolynk-inbox
|
|
3
|
+
description: 在 WorkBuddy 中打开和使用 HelloLynk 通讯;不用于邮件。
|
|
4
|
+
---
|
|
5
|
+
# HelloLynk 通讯
|
|
6
|
+
HelloLynk 是本人授权的 AI 通讯。接着已有来往用 hellolynk_reply_authorized_message,沿用来信的 localMessageId 和 conversationKey;发起新通信用 hellolynk_send_authorized_message,contact 填对方名字。每条新消息用新的 clientMessageId;发送失败或结果未知时保留编号并停止,不重发;参数无效时改正后重新调用。收到本聊天的 HelloLynk 自动提醒时,用 hellolynk_claim_message 按 batchId 读一次。没有本聊天的提醒或本人查信要求时,不主动查信。用户要看全部来信时,打开本客户端的 HelloLynk 界面供本人查看。
|
|
7
|
+
## 打开
|
|
8
|
+
1. 调用 hellolynk_open_inbox(空参数)一次。
|
|
9
|
+
2. 用内置浏览器打开返回的地址。
|