@hellolynk-ai/codex-plugin 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/.agents/plugins/marketplace.json +20 -0
- package/LICENSE +92 -0
- package/README.md +101 -2
- package/bin/hellolynk-client-setup.mjs +2748 -0
- package/bin/hellolynk-mcp-config-shape.mjs +23 -0
- package/hellolynk-client-pack-manifest.json +619 -0
- package/hellolynk-client-pack-pin.json +4 -0
- package/package.json +58 -4
- package/plugins/hellolynk-codex/.app.json +3 -0
- package/plugins/hellolynk-codex/.codex-plugin/plugin.json +38 -0
- package/plugins/hellolynk-codex/.mcp.json +19 -0
- package/plugins/hellolynk-codex/assets/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/package.json +11 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/bootstrap/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/connect-activate.mjs +837 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-agent-configurator.mjs +701 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-account.mjs +180 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-core.mjs +770 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-profile.mjs +191 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-entry.mjs +31 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update-runtime.mjs +600 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-client-update.mjs +453 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-contact-request-e2ee.mjs +867 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-device-enrollment.mjs +435 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee-client.mjs +1964 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-direct-text-e2ee.mjs +1207 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-auth.mjs +347 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-protected-store.mjs +2721 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host-transport-client.mjs +946 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-host.mjs +2374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-confirmation-ledger.mjs +256 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-human-relationship-controller.mjs +1234 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-inbox-read-state.mjs +11 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-indexed-message-store.mjs +1558 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-instance-conversation.mjs +185 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contact-requests.mjs +1733 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-contacts.mjs +1228 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-migration.mjs +766 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-data-protection.mjs +747 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-identity-trust.mjs +274 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-messaging.mjs +3043 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-peer-transport.mjs +103 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-local-process-lock.mjs +157 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-inbox.mjs +230 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-model-notices.cjs +60 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-addressing-tools.cjs +231 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-conversations.mjs +280 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-human-confirmation-broker.mjs +1036 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-inbox-runtime.mjs +553 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-notice.mjs +50 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-reply-routes.mjs +96 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-native-worker-lease.mjs +84 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-assembly.mjs +955 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-installation-store.mjs +765 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-launcher.mjs +665 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-process.mjs +4374 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-store-adapters.mjs +548 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-neutral-host-workbench-read-model.mjs +1365 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-platform-credential-store.mjs +773 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-relay-client-cli.mjs +232 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-release-retention.mjs +277 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-runtime-safety.cjs +471 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-action-grants.mjs +457 -0
- package/plugins/hellolynk-codex/runtime/core/hellolynk-workbench-connection.mjs +234 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-bootstrap.mjs +64 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-entry.mjs +70 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-loader.mjs +156 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-manifest.json +358 -0
- package/plugins/hellolynk-codex/runtime/neutral-host-release-pin.json +4 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/configure-agent.ps1 +281 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/credential-store.ps1 +844 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-host-protected-store.ps1 +2866 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-human-confirmation-ledger.ps1 +155 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-local-data-key.ps1 +300 -0
- package/plugins/hellolynk-codex/runtime/platform/windows/hellolynk-native-human-confirmation-broker.ps1 +216 -0
- package/plugins/hellolynk-codex/runtime/workbench/candidate-bootstrap.mjs +54 -0
- package/plugins/hellolynk-codex/runtime/workbench/contract.mjs +1314 -0
- package/plugins/hellolynk-codex/runtime/workbench/hellolynk-workbench-build.json +215 -0
- package/plugins/hellolynk-codex/runtime/workbench/host-capabilities.mjs +80 -0
- package/plugins/hellolynk-codex/runtime/workbench/i18n.mjs +1127 -0
- package/plugins/hellolynk-codex/runtime/workbench/loopback-surface.mjs +740 -0
- package/plugins/hellolynk-codex/runtime/workbench/mcp-app-bundle.mjs +97 -0
- package/plugins/hellolynk-codex/runtime/workbench/service.mjs +485 -0
- package/plugins/hellolynk-codex/runtime/workbench/state.mjs +421 -0
- package/plugins/hellolynk-codex/runtime/workbench/transports.mjs +171 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/app.mjs +6120 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/connection-form.mjs +155 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/hellolynk-mark.png +0 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/http-transport.mjs +247 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/arrow-left.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/close.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/contacts.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/inbox.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/refresh.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/search.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/security.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/icons/settings.svg +3 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/index.html +139 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/loopback-entry.mjs +26 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-client.mjs +151 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/mcp-app-entry.mjs +452 -0
- package/plugins/hellolynk-codex/runtime/workbench/ui/styles.css +686 -0
- package/plugins/hellolynk-codex/server/codex-neutral-production.mjs +520 -0
- package/plugins/hellolynk-codex/server/mcp-proxy.mjs +306 -0
- package/plugins/hellolynk-codex/server/mcp-server.mjs +1193 -0
- package/plugins/hellolynk-codex/server/native-conversation-directory.mjs +33 -0
- package/plugins/hellolynk-codex/server/native-reply-routes.mjs +32 -0
- package/plugins/hellolynk-codex/server/native-task-client.mjs +382 -0
- package/plugins/hellolynk-codex/server/native-task-runtime.mjs +8 -0
- package/plugins/hellolynk-codex/server/neutral-host-adapter.mjs +649 -0
- package/plugins/hellolynk-codex/server/workbench-runtime.mjs +180 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/SKILL.md +11 -0
- package/plugins/hellolynk-codex/skills/hellolynk-open/agents/openai.yaml +4 -0
- package/tools/hellolynk-activate-neutral-host-clients.mjs +837 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createHostProtectedStore } from "./hellolynk-host-protected-store.mjs";
|
|
8
|
+
import { defaultNeutralHostStoreRoot } from "./hellolynk-neutral-host-installation-store.mjs";
|
|
9
|
+
import runtimeSafety from "./hellolynk-runtime-safety.cjs";
|
|
10
|
+
|
|
11
|
+
const { containsHelloLynkCredentialMaterial, sanitizeHelloLynkChildEnvironment } = runtimeSafety;
|
|
12
|
+
|
|
13
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
14
|
+
const configurationRollbackSnapshots = new WeakMap();
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const [client, ...rest] = argv;
|
|
18
|
+
const options = {};
|
|
19
|
+
for (let index = 0; index < rest.length; index += 2) {
|
|
20
|
+
if (!rest[index]?.startsWith("--") || index + 1 >= rest.length) throw new Error("agent_setup_arguments_invalid");
|
|
21
|
+
options[rest[index].slice(2)] = rest[index + 1];
|
|
22
|
+
}
|
|
23
|
+
return { client, options };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function required(options, key) {
|
|
27
|
+
const value = String(options[key] || "").trim();
|
|
28
|
+
if (!value) throw new Error(`agent_setup_${key}_required`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Desktop clients can move their whole profile through a user-owned junction.
|
|
33
|
+
// Pin the existing parent to its physical location; the config file itself
|
|
34
|
+
// must still be regular, and the Windows writer keeps its no-reparse checks.
|
|
35
|
+
function resolveConfigPath(options) {
|
|
36
|
+
const logical = path.resolve(options.configPath);
|
|
37
|
+
let parent = path.dirname(logical);
|
|
38
|
+
const missing = [];
|
|
39
|
+
while (!fs.existsSync(parent)) {
|
|
40
|
+
missing.unshift(path.basename(parent));
|
|
41
|
+
const next = path.dirname(parent);
|
|
42
|
+
if (next === parent) throw new Error("agent_setup_config_path_invalid");
|
|
43
|
+
parent = next;
|
|
44
|
+
}
|
|
45
|
+
const resolved = path.join(fs.realpathSync(parent), ...missing, path.basename(logical));
|
|
46
|
+
if (options.expectedConfigPath != null && !samePath(resolved, options.expectedConfigPath)) {
|
|
47
|
+
throw new Error("agent_setup_config_path_changed");
|
|
48
|
+
}
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJsonConfig(configPath, options = {}) {
|
|
53
|
+
let metadata;
|
|
54
|
+
try {
|
|
55
|
+
metadata = fs.lstatSync(configPath);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (options.allowMissing === true && error?.code === "ENOENT") {
|
|
58
|
+
return { exists: false, value: {}, bytes: null, sha256: null };
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > MAX_CONFIG_BYTES) {
|
|
63
|
+
throw new Error("agent_setup_config_invalid");
|
|
64
|
+
}
|
|
65
|
+
const bytes = fs.readFileSync(configPath);
|
|
66
|
+
let body;
|
|
67
|
+
try { body = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
|
|
68
|
+
catch { throw new Error("agent_setup_config_encoding_invalid"); }
|
|
69
|
+
if (containsHelloLynkCredentialMaterial(body)) throw new Error("agent_setup_config_contains_secret");
|
|
70
|
+
const value = options.jsonc ? JSON.parse(jsoncWithoutComments(body).replace(/("(?:\\.|[^"\\])*")|,(?=\s*[}\]])/g, (match, quoted) => quoted || "")) : JSON.parse(body);
|
|
71
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_setup_config_invalid");
|
|
72
|
+
return { exists: true, value, bytes, jsonc: options.jsonc === true, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Keep strings and offsets intact: argv.json permits comments and trailing
|
|
76
|
+
// commas. JSON.parse still validates all remaining syntax before any write.
|
|
77
|
+
function jsoncWithoutComments(body) {
|
|
78
|
+
return body.replace(/^\uFEFF/, " ").replace(/("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g,
|
|
79
|
+
(match, quoted) => quoted || match.replace(/[^\r\n]/g, " "));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function windowsAtomicReplace(configPath, temporary, expectedSha256) {
|
|
83
|
+
const helper = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../platform/windows/configure-agent.ps1");
|
|
84
|
+
const result = spawnSync("pwsh", [
|
|
85
|
+
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", helper, "-AtomicReplace",
|
|
86
|
+
], {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
windowsHide: true,
|
|
89
|
+
env: sanitizeHelloLynkChildEnvironment(process.env),
|
|
90
|
+
input: JSON.stringify({
|
|
91
|
+
configPath,
|
|
92
|
+
temporary,
|
|
93
|
+
expectedSha256,
|
|
94
|
+
replacementSha256: createHash("sha256").update(fs.readFileSync(temporary)).digest("hex"),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
const code = String(result.stdout || "").trim();
|
|
98
|
+
if (result.status !== 0 || code !== "agent_setup_config_replace_ok") {
|
|
99
|
+
throw new Error(/^[a-z][a-z0-9_]{0,127}$/.test(code) ? code : "agent_setup_config_replace_failed");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function serializedJsonBytes(value) {
|
|
104
|
+
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
105
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_CONFIG_BYTES
|
|
106
|
+
|| containsHelloLynkCredentialMaterial(serialized)) {
|
|
107
|
+
throw new Error("agent_setup_config_contains_secret");
|
|
108
|
+
}
|
|
109
|
+
return Buffer.from(serialized, "utf8");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function atomicWriteBytes(configPath, bytes, expectedSha256) {
|
|
113
|
+
if (!Buffer.isBuffer(bytes) || bytes.length > MAX_CONFIG_BYTES
|
|
114
|
+
|| containsHelloLynkCredentialMaterial(bytes.toString("utf8"))) {
|
|
115
|
+
throw new Error("agent_setup_config_contains_secret");
|
|
116
|
+
}
|
|
117
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
118
|
+
const temporary = path.join(path.dirname(configPath), `.${path.basename(configPath)}.${randomUUID()}.tmp`);
|
|
119
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
120
|
+
try {
|
|
121
|
+
fs.writeFileSync(fd, bytes);
|
|
122
|
+
fs.fsyncSync(fd);
|
|
123
|
+
} finally {
|
|
124
|
+
fs.closeSync(fd);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
if (expectedSha256 == null) {
|
|
128
|
+
// Linking the fully written temporary file publishes a missing config without
|
|
129
|
+
// overwriting a concurrently-created path on either Windows or POSIX.
|
|
130
|
+
fs.linkSync(temporary, configPath);
|
|
131
|
+
// The config is committed now. Removing its temporary name must not hide
|
|
132
|
+
// that success or prevent finishConfiguration from retaining its receipt.
|
|
133
|
+
try { fs.unlinkSync(temporary); } catch {}
|
|
134
|
+
} else if (process.platform === "win32") {
|
|
135
|
+
windowsAtomicReplace(configPath, temporary, expectedSha256);
|
|
136
|
+
} else {
|
|
137
|
+
const current = readJsonConfig(configPath, { jsonc: true });
|
|
138
|
+
if (current.sha256 !== expectedSha256) throw new Error("agent_setup_config_changed");
|
|
139
|
+
fs.renameSync(temporary, configPath);
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
try { fs.rmSync(temporary, { force: true }); } catch {}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function assertPreparedConfig(snapshot, replacementBytes, options) {
|
|
148
|
+
if (Object.hasOwn(options, "expectedConfigSha256")
|
|
149
|
+
&& options.expectedConfigSha256 !== snapshot.sha256) {
|
|
150
|
+
throw new Error("agent_setup_config_changed");
|
|
151
|
+
}
|
|
152
|
+
const replacementSha256 = createHash("sha256").update(replacementBytes).digest("hex");
|
|
153
|
+
if (Object.hasOwn(options, "expectedReplacementSha256")
|
|
154
|
+
&& options.expectedReplacementSha256 !== replacementSha256) {
|
|
155
|
+
throw new Error("agent_setup_config_plan_changed");
|
|
156
|
+
}
|
|
157
|
+
return replacementSha256;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function finishConfiguration({ result, snapshot, replacementBytes, replacementSha256, apply }) {
|
|
161
|
+
const changed = snapshot.sha256 !== replacementSha256;
|
|
162
|
+
const publicResult = Object.freeze({
|
|
163
|
+
...result,
|
|
164
|
+
planned: apply !== true,
|
|
165
|
+
changed,
|
|
166
|
+
expectedConfigSha256: snapshot.sha256,
|
|
167
|
+
replacementSha256,
|
|
168
|
+
});
|
|
169
|
+
if (apply !== true || !changed) return publicResult;
|
|
170
|
+
atomicWriteBytes(result.configPath, replacementBytes, snapshot.sha256);
|
|
171
|
+
configurationRollbackSnapshots.set(publicResult, Object.freeze({
|
|
172
|
+
configPath: result.configPath,
|
|
173
|
+
priorExists: snapshot.exists,
|
|
174
|
+
priorBytes: snapshot.bytes,
|
|
175
|
+
replacementSha256,
|
|
176
|
+
jsonc: snapshot.jsonc === true,
|
|
177
|
+
}));
|
|
178
|
+
return publicResult;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function rollbackAgentConfiguration(configured) {
|
|
182
|
+
const snapshot = configurationRollbackSnapshots.get(configured);
|
|
183
|
+
if (!snapshot) return Object.freeze({ rolledBack: false, reason: "unchanged" });
|
|
184
|
+
const current = readJsonConfig(snapshot.configPath, { jsonc: snapshot.jsonc });
|
|
185
|
+
if (current.sha256 !== snapshot.replacementSha256) {
|
|
186
|
+
throw new Error("agent_setup_config_rollback_conflict");
|
|
187
|
+
}
|
|
188
|
+
if (snapshot.priorExists) {
|
|
189
|
+
atomicWriteBytes(snapshot.configPath, snapshot.priorBytes, snapshot.replacementSha256);
|
|
190
|
+
} else {
|
|
191
|
+
const quarantine = path.join(
|
|
192
|
+
path.dirname(snapshot.configPath),
|
|
193
|
+
`.${path.basename(snapshot.configPath)}.${randomUUID()}.rollback`,
|
|
194
|
+
);
|
|
195
|
+
fs.renameSync(snapshot.configPath, quarantine);
|
|
196
|
+
try {
|
|
197
|
+
const quarantined = readJsonConfig(quarantine, { jsonc: snapshot.jsonc });
|
|
198
|
+
if (quarantined.sha256 !== snapshot.replacementSha256) {
|
|
199
|
+
throw new Error("agent_setup_config_rollback_conflict");
|
|
200
|
+
}
|
|
201
|
+
fs.unlinkSync(quarantine);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (!fs.existsSync(snapshot.configPath) && fs.existsSync(quarantine)) {
|
|
204
|
+
fs.renameSync(quarantine, snapshot.configPath);
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
configurationRollbackSnapshots.delete(configured);
|
|
210
|
+
return Object.freeze({ rolledBack: true });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function runAgentConfigurationTransaction(options = {}) {
|
|
214
|
+
const plans = Array.isArray(options.plans) ? options.plans : null;
|
|
215
|
+
if (!plans || typeof options.apply !== "function" || typeof options.rollback !== "function"
|
|
216
|
+
|| (options.finalize != null && typeof options.finalize !== "function")) {
|
|
217
|
+
throw new Error("agent_setup_transaction_invalid");
|
|
218
|
+
}
|
|
219
|
+
const committed = [];
|
|
220
|
+
try {
|
|
221
|
+
for (const plan of plans) committed.push(await options.apply(plan));
|
|
222
|
+
if (options.finalize) await options.finalize();
|
|
223
|
+
return Object.freeze({ committed: Object.freeze(committed) });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const rollbackFailures = [];
|
|
226
|
+
for (const item of [...committed].reverse()) {
|
|
227
|
+
try { await options.rollback(item); }
|
|
228
|
+
catch (rollbackError) { rollbackFailures.push(rollbackError); }
|
|
229
|
+
}
|
|
230
|
+
if (rollbackFailures.length > 0 || localConfigurationMayBeUncertain(error)) {
|
|
231
|
+
const failure = new Error("local_configuration_outcome_uncertain");
|
|
232
|
+
failure.localConfigurationOutcomeUncertain = true;
|
|
233
|
+
failure.cause = rollbackFailures[0] || error;
|
|
234
|
+
failure.configurationCause = error;
|
|
235
|
+
if (rollbackFailures.length > 1) failure.rollbackFailures = rollbackFailures;
|
|
236
|
+
throw failure;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function localConfigurationMayBeUncertain(error) {
|
|
243
|
+
const code = String(error?.code || error?.message || "");
|
|
244
|
+
return error?.localConfigurationOutcomeUncertain === true
|
|
245
|
+
|| code === "local_configuration_outcome_uncertain"
|
|
246
|
+
|| /^(?:agent_setup_config|client_skill)(?:_[a-z0-9]+)*_rollback_failed(?::|$)/u.test(code);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function requirePluginDirectory(pluginPath) {
|
|
250
|
+
const absolute = path.resolve(String(pluginPath || ""));
|
|
251
|
+
const metadata = fs.lstatSync(absolute);
|
|
252
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) throw new Error("agent_setup_plugin_invalid");
|
|
253
|
+
const manifestPath = path.join(absolute, "openclaw.plugin.json");
|
|
254
|
+
const manifest = fs.lstatSync(manifestPath);
|
|
255
|
+
if (!manifest.isFile() || manifest.isSymbolicLink()) throw new Error("agent_setup_plugin_invalid");
|
|
256
|
+
return absolute;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pluginIdAt(pluginPath) {
|
|
260
|
+
try {
|
|
261
|
+
const manifestPath = path.join(path.resolve(String(pluginPath || "")), "openclaw.plugin.json");
|
|
262
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
263
|
+
return typeof manifest?.id === "string" ? manifest.id.trim() : "";
|
|
264
|
+
} catch {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function buildOpenClawChannelConfig(nativeModel) {
|
|
270
|
+
// Match the current OpenClaw channel's optional native model contract.
|
|
271
|
+
if (nativeModel !== undefined && (!plainObject(nativeModel)
|
|
272
|
+
|| Object.keys(nativeModel).some((key) => !["provider", "model", "thinkingLevel"].includes(key))
|
|
273
|
+
|| typeof nativeModel.provider !== "string" || !/^[a-z0-9][a-z0-9._-]{0,79}$/.test(nativeModel.provider)
|
|
274
|
+
|| typeof nativeModel.model !== "string" || nativeModel.model.length > 240
|
|
275
|
+
|| !/^[^\s\u0000-\u001f\u007f]+$/.test(nativeModel.model)
|
|
276
|
+
|| (nativeModel.thinkingLevel !== undefined && (typeof nativeModel.thinkingLevel !== "string"
|
|
277
|
+
|| !/^[a-z][a-z0-9_-]{0,31}$/.test(nativeModel.thinkingLevel))))) {
|
|
278
|
+
throw new Error("neutral_openclaw_native_model_invalid");
|
|
279
|
+
}
|
|
280
|
+
return Object.freeze({
|
|
281
|
+
enabled: true,
|
|
282
|
+
hostMode: "neutral",
|
|
283
|
+
bindingRef: "protected-store:openclaw",
|
|
284
|
+
...(nativeModel !== undefined ? { nativeModel: Object.freeze({ ...nativeModel }) } : {}),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function configureOpenClaw(options) {
|
|
289
|
+
const configPath = resolveConfigPath(options);
|
|
290
|
+
const pluginPath = requirePluginDirectory(options.pluginPath);
|
|
291
|
+
const configSnapshot = readJsonConfig(configPath, { allowMissing: options.allowMissing === true });
|
|
292
|
+
const config = configSnapshot.value;
|
|
293
|
+
migrateLegacyOpenClawConfig(config);
|
|
294
|
+
assertExistingOpenClawNeutralConfig(config);
|
|
295
|
+
config.channels = config.channels && typeof config.channels === "object" && !Array.isArray(config.channels)
|
|
296
|
+
? config.channels
|
|
297
|
+
: {};
|
|
298
|
+
config.channels.hellolynk = buildOpenClawChannelConfig(config.channels.hellolynk?.nativeModel);
|
|
299
|
+
config.plugins = config.plugins && typeof config.plugins === "object" && !Array.isArray(config.plugins)
|
|
300
|
+
? config.plugins
|
|
301
|
+
: {};
|
|
302
|
+
config.plugins.load = config.plugins.load && typeof config.plugins.load === "object" && !Array.isArray(config.plugins.load)
|
|
303
|
+
? config.plugins.load
|
|
304
|
+
: {};
|
|
305
|
+
const existingPaths = Array.isArray(config.plugins.load.paths) ? config.plugins.load.paths : [];
|
|
306
|
+
const pluginId = pluginIdAt(pluginPath);
|
|
307
|
+
let exactPathCount = 0;
|
|
308
|
+
const unrelatedPaths = existingPaths.map((candidate) => {
|
|
309
|
+
const absoluteCandidate = path.resolve(String(candidate || ""));
|
|
310
|
+
if (samePath(absoluteCandidate, pluginPath)) {
|
|
311
|
+
exactPathCount += 1;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (pluginId && pluginIdAt(absoluteCandidate) === pluginId) {
|
|
315
|
+
if (isPriorStableOpenClawPluginPath(absoluteCandidate, pluginPath)) return null;
|
|
316
|
+
throw new Error("neutral_openclaw_plugin_conflict");
|
|
317
|
+
}
|
|
318
|
+
return candidate;
|
|
319
|
+
});
|
|
320
|
+
if (exactPathCount > 1) throw new Error("neutral_openclaw_plugin_conflict");
|
|
321
|
+
config.plugins.load.paths = [...new Set([...unrelatedPaths.filter(Boolean), pluginPath])];
|
|
322
|
+
config.plugins.entries = config.plugins.entries && typeof config.plugins.entries === "object" && !Array.isArray(config.plugins.entries)
|
|
323
|
+
? config.plugins.entries
|
|
324
|
+
: {};
|
|
325
|
+
const existingEntry = config.plugins.entries.hellolynk && typeof config.plugins.entries.hellolynk === "object" && !Array.isArray(config.plugins.entries.hellolynk)
|
|
326
|
+
? config.plugins.entries.hellolynk
|
|
327
|
+
: {};
|
|
328
|
+
config.plugins.entries.hellolynk = { ...existingEntry, enabled: true };
|
|
329
|
+
// The installed sidebar is a native plugin page. Enable the host's supported
|
|
330
|
+
// setting in the same guarded config transaction, preserving other settings.
|
|
331
|
+
let ui = config;
|
|
332
|
+
for (const key of ["gateway", "controlUi", "experimental"]) {
|
|
333
|
+
if (Object.hasOwn(ui, key) && !plainObject(ui[key])) throw new Error("agent_setup_config_invalid");
|
|
334
|
+
ui[key] ??= {};
|
|
335
|
+
ui = ui[key];
|
|
336
|
+
}
|
|
337
|
+
ui.customPlugins = true;
|
|
338
|
+
const replacementBytes = serializedJsonBytes(config);
|
|
339
|
+
const replacementSha256 = assertPreparedConfig(configSnapshot, replacementBytes, options);
|
|
340
|
+
if (options.apply !== false) await requireOpenClawNeutralBinding(options);
|
|
341
|
+
if (!samePath(resolveConfigPath(options), configPath)) throw new Error("agent_setup_config_path_changed");
|
|
342
|
+
return finishConfiguration({
|
|
343
|
+
snapshot: configSnapshot,
|
|
344
|
+
replacementBytes,
|
|
345
|
+
replacementSha256,
|
|
346
|
+
apply: options.apply !== false,
|
|
347
|
+
result: {
|
|
348
|
+
ok: true,
|
|
349
|
+
client: "openclaw",
|
|
350
|
+
mode: "neutral",
|
|
351
|
+
bindingRef: "protected-store:openclaw",
|
|
352
|
+
pluginRegistered: true,
|
|
353
|
+
pluginPath,
|
|
354
|
+
configPath,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function configureNeutralHostMcpClient(options) {
|
|
360
|
+
const client = String(options.client || "");
|
|
361
|
+
if (!["trae-work", "workbuddy"].includes(client)) throw new Error("agent_setup_client_unsupported");
|
|
362
|
+
const configPath = resolveConfigPath(options);
|
|
363
|
+
const nodePath = requireRegularFile(options.nodePath, "agent_setup_node_invalid");
|
|
364
|
+
const proxyPath = requireRegularFile(options.proxyPath, "agent_setup_proxy_invalid");
|
|
365
|
+
const entryArgs = normalizeMcpEntryArgs(options.entryArgs);
|
|
366
|
+
const storeRoot = normalizeNeutralHostStoreRoot(
|
|
367
|
+
options.storeRoot || defaultNeutralHostStoreRoot(process.platform, options.env || process.env),
|
|
368
|
+
);
|
|
369
|
+
const configSnapshot = readJsonConfig(configPath, { allowMissing: options.allowMissing === true });
|
|
370
|
+
const config = configSnapshot.value;
|
|
371
|
+
if (Object.hasOwn(config, "mcpServers")
|
|
372
|
+
&& (!config.mcpServers || typeof config.mcpServers !== "object" || Array.isArray(config.mcpServers))) {
|
|
373
|
+
throw new Error("agent_setup_config_invalid");
|
|
374
|
+
}
|
|
375
|
+
config.mcpServers = config.mcpServers || {};
|
|
376
|
+
const hadManagedEntry = Object.hasOwn(config.mcpServers, "hellolynk") || Object.hasOwn(config.mcpServers, "lynk");
|
|
377
|
+
migrateLegacyMcpServer(config.mcpServers, client);
|
|
378
|
+
config.mcpServers.hellolynk = {
|
|
379
|
+
command: nodePath,
|
|
380
|
+
args: [proxyPath, ...entryArgs],
|
|
381
|
+
env: { HELLOLYNK_NEUTRAL_STORE_ROOT: storeRoot },
|
|
382
|
+
};
|
|
383
|
+
const replacementBytes = serializedJsonBytes(config);
|
|
384
|
+
const replacementSha256 = assertPreparedConfig(configSnapshot, replacementBytes, options);
|
|
385
|
+
return finishConfiguration({
|
|
386
|
+
snapshot: configSnapshot,
|
|
387
|
+
replacementBytes,
|
|
388
|
+
replacementSha256,
|
|
389
|
+
apply: options.apply !== false,
|
|
390
|
+
result: { ok: true, client, mode: "neutral", configPath, proxyPath, entryArgCount: entryArgs.length, hadManagedEntry },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Uninstall removes the entry only while it starts a release under releasesRoot; an entry that
|
|
395
|
+
// starts anything else is not this installation's. Other servers and settings stay as they are.
|
|
396
|
+
export async function removeNeutralHostMcpClient(options) {
|
|
397
|
+
const client = String(options.client || "");
|
|
398
|
+
if (!["trae-work", "workbuddy"].includes(client)) throw new Error("agent_setup_client_unsupported");
|
|
399
|
+
const configPath = resolveConfigPath(options);
|
|
400
|
+
const configSnapshot = readJsonConfig(configPath, { allowMissing: true });
|
|
401
|
+
const config = configSnapshot.value;
|
|
402
|
+
if (Object.hasOwn(config, "mcpServers") && !plainObject(config.mcpServers)) throw new Error("agent_setup_config_invalid");
|
|
403
|
+
const server = config.mcpServers?.hellolynk;
|
|
404
|
+
if (server !== undefined && !inInstalledRelease(server?.args?.[0], options.releasesRoot, client)) {
|
|
405
|
+
throw new Error("agent_setup_uninstall_conflict");
|
|
406
|
+
}
|
|
407
|
+
if (server !== undefined) delete config.mcpServers.hellolynk;
|
|
408
|
+
return finishRemoval(configSnapshot, server !== undefined ? serializedJsonBytes(config) : configSnapshot.bytes, options,
|
|
409
|
+
{ ok: true, client, configPath });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// OpenClaw rejects a channel whose plugin is gone, so the channel and plugin entry go with the
|
|
413
|
+
// load path. A HelloLynk plugin loaded from anywhere else is not this installation's.
|
|
414
|
+
export async function removeOpenClaw(options) {
|
|
415
|
+
const configPath = resolveConfigPath(options);
|
|
416
|
+
const configSnapshot = readJsonConfig(configPath, { allowMissing: true });
|
|
417
|
+
const config = configSnapshot.value;
|
|
418
|
+
const paths = Array.isArray(config.plugins?.load?.paths) ? config.plugins.load.paths : [];
|
|
419
|
+
const installed = paths.filter((candidate) => isLegacyOpenClawPluginPath(candidate)
|
|
420
|
+
|| inInstalledRelease(candidate, options.releasesRoot, "openclaw", ["integrations", "openclaw-channel"]));
|
|
421
|
+
if (paths.some((candidate) => !installed.includes(candidate) && pluginIdAt(candidate) === "hellolynk")) {
|
|
422
|
+
throw new Error("agent_setup_uninstall_conflict");
|
|
423
|
+
}
|
|
424
|
+
if (installed.length) {
|
|
425
|
+
config.plugins.load.paths = paths.filter((candidate) => !installed.includes(candidate));
|
|
426
|
+
if (plainObject(config.channels)) delete config.channels.hellolynk;
|
|
427
|
+
if (plainObject(config.plugins.entries)) delete config.plugins.entries.hellolynk;
|
|
428
|
+
}
|
|
429
|
+
return finishRemoval(configSnapshot, installed.length ? serializedJsonBytes(config) : configSnapshot.bytes, options,
|
|
430
|
+
{ ok: true, client: "openclaw", configPath });
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function finishRemoval(snapshot, replacementBytes, options, result) {
|
|
434
|
+
if (!snapshot.exists) {
|
|
435
|
+
return Object.freeze({ ...result, planned: options.apply === false, changed: false, expectedConfigSha256: null, replacementSha256: null });
|
|
436
|
+
}
|
|
437
|
+
const replacementSha256 = assertPreparedConfig(snapshot, replacementBytes, options);
|
|
438
|
+
if (!samePath(resolveConfigPath(options), result.configPath)) throw new Error("agent_setup_config_path_changed");
|
|
439
|
+
return finishConfiguration({ snapshot, replacementBytes, replacementSha256, apply: options.apply !== false, result });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// A path inside <releasesRoot>\<client>-<sha256>, the releases the installer writes.
|
|
443
|
+
function inInstalledRelease(value, releasesRoot, client, tail = null) {
|
|
444
|
+
const candidate = String(value || "");
|
|
445
|
+
if (!path.isAbsolute(candidate) || !path.isAbsolute(String(releasesRoot || ""))) return false;
|
|
446
|
+
const relative = path.relative(path.resolve(releasesRoot), path.resolve(candidate));
|
|
447
|
+
const [release, ...rest] = relative.split(path.sep);
|
|
448
|
+
return !relative.startsWith("..") && !path.isAbsolute(relative)
|
|
449
|
+
&& new RegExp(`^${escapeRegExp(client)}-[a-f0-9]{64}$`, "i").test(release || "")
|
|
450
|
+
&& (tail === null ? rest.length > 0 : rest.join("/").toLowerCase() === tail.join("/").toLowerCase());
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// The merged TRAE client reads this one user startup argument. Only add a
|
|
454
|
+
// missing port; preserve existing settings, comments and user-chosen ports.
|
|
455
|
+
export async function configureTraeNativeStartup(options) {
|
|
456
|
+
const configPath = resolveConfigPath(options);
|
|
457
|
+
const snapshot = readJsonConfig(configPath, { allowMissing: true, jsonc: true });
|
|
458
|
+
const config = snapshot.value;
|
|
459
|
+
const current = config["remote-debugging-port"];
|
|
460
|
+
if (current !== undefined && (!/^[1-9]\d{0,4}$/.test(String(current)) || Number(current) > 65535)) {
|
|
461
|
+
throw new Error("trae_native_debug_startup_conflict");
|
|
462
|
+
}
|
|
463
|
+
const endpoint = new URL(options.debugEndpoint || `http://127.0.0.1:${current ?? 9338}`);
|
|
464
|
+
if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || !endpoint.port
|
|
465
|
+
|| endpoint.pathname !== "/" || endpoint.search || endpoint.hash || endpoint.username || endpoint.password) {
|
|
466
|
+
throw new Error("trae_native_debug_endpoint_invalid");
|
|
467
|
+
}
|
|
468
|
+
if (current !== undefined && String(current) !== endpoint.port
|
|
469
|
+
|| config["remote-debugging-address"] !== undefined && config["remote-debugging-address"] !== "127.0.0.1") {
|
|
470
|
+
throw new Error("trae_native_debug_startup_conflict");
|
|
471
|
+
}
|
|
472
|
+
let replacementBytes = snapshot.bytes;
|
|
473
|
+
if (current === undefined) {
|
|
474
|
+
const original = snapshot.bytes?.toString("utf8") || "{}\n";
|
|
475
|
+
const insertAt = jsoncWithoutComments(original).indexOf("{") + 1;
|
|
476
|
+
const eol = original.includes("\r\n") ? "\r\n" : "\n";
|
|
477
|
+
const setting = `${eol} "remote-debugging-port": ${JSON.stringify(endpoint.port)}${Object.keys(config).length ? "," : ""}${eol}`;
|
|
478
|
+
replacementBytes = Buffer.from(original.slice(0, insertAt) + setting + original.slice(insertAt));
|
|
479
|
+
}
|
|
480
|
+
const replacementSha256 = assertPreparedConfig(snapshot, replacementBytes, options);
|
|
481
|
+
return finishConfiguration({ snapshot: { ...snapshot, jsonc: true }, replacementBytes, replacementSha256,
|
|
482
|
+
apply: options.apply !== false, result: { ok: true, configPath, debugEndpoint: endpoint.origin, restartRequired: current === undefined } });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function migrateLegacyOpenClawConfig(config) {
|
|
486
|
+
const channels = plainObject(config.channels) ? config.channels : null;
|
|
487
|
+
const entries = plainObject(config.plugins?.entries) ? config.plugins.entries : null;
|
|
488
|
+
const paths = Array.isArray(config.plugins?.load?.paths) ? config.plugins.load.paths : null;
|
|
489
|
+
const channelPresent = Boolean(channels && Object.hasOwn(channels, "lynk"));
|
|
490
|
+
const entryPresent = Boolean(entries && Object.hasOwn(entries, "lynk"));
|
|
491
|
+
const legacyPaths = paths ? paths.filter(isLegacyOpenClawPluginPath) : [];
|
|
492
|
+
if (!channelPresent && !entryPresent && legacyPaths.length === 0) return;
|
|
493
|
+
const exactChannel = channelPresent && exactObject(channels.lynk, {
|
|
494
|
+
enabled: true,
|
|
495
|
+
hostMode: "neutral",
|
|
496
|
+
bindingRef: "protected-store:openclaw",
|
|
497
|
+
});
|
|
498
|
+
const exactEntry = entryPresent && exactObject(entries.lynk, { enabled: true });
|
|
499
|
+
if (!exactChannel || !exactEntry || !paths || legacyPaths.length !== 1) {
|
|
500
|
+
throw new Error("neutral_openclaw_legacy_config_conflict");
|
|
501
|
+
}
|
|
502
|
+
delete channels.lynk;
|
|
503
|
+
delete entries.lynk;
|
|
504
|
+
config.plugins.load.paths = paths.filter((candidate) => candidate !== legacyPaths[0]);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function migrateLegacyMcpServer(servers, client) {
|
|
508
|
+
if (!Object.hasOwn(servers, "lynk")) return;
|
|
509
|
+
if (!isLegacyMcpServer(servers.lynk, client)) throw new Error("neutral_mcp_legacy_config_conflict");
|
|
510
|
+
delete servers.lynk;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function isLegacyMcpServer(value, client) {
|
|
514
|
+
if (!plainObject(value)) return false;
|
|
515
|
+
const keys = Object.keys(value).sort().join(",");
|
|
516
|
+
if (keys !== "args,command" && keys !== "args,command,disabled") return false;
|
|
517
|
+
if (Object.hasOwn(value, "disabled") && value.disabled !== false) return false;
|
|
518
|
+
const command = String(value.command || "");
|
|
519
|
+
if (!path.isAbsolute(command) || !/^node(?:\.exe)?$/i.test(path.basename(command))) return false;
|
|
520
|
+
const args = Array.isArray(value.args) ? value.args.map((item) => String(item || "")) : [];
|
|
521
|
+
if (client === "trae-work") {
|
|
522
|
+
return args.length === 4
|
|
523
|
+
&& isLegacyReleaseFile(args[0], "trae-work", ["runtime", "trae-work-bootstrap.mjs"])
|
|
524
|
+
&& args[1] === "run"
|
|
525
|
+
&& args[2] === "--expected-manifest-sha256"
|
|
526
|
+
&& /^[a-f0-9]{64}$/.test(args[3]);
|
|
527
|
+
}
|
|
528
|
+
if (args.length !== 6
|
|
529
|
+
|| !isLegacyReleaseFile(args[0], "workbuddy", ["integrations", "workbuddy", "server", "package-bootstrap.mjs"])
|
|
530
|
+
|| args[1] !== "run" || args[2] !== "--root"
|
|
531
|
+
|| args[4] !== "--expected-manifest-sha256" || !/^[a-f0-9]{64}$/.test(args[5])) return false;
|
|
532
|
+
const releaseRoot = legacyReleaseRoot(args[0], ["integrations", "workbuddy", "server", "package-bootstrap.mjs"]);
|
|
533
|
+
return Boolean(releaseRoot) && samePath(args[3], releaseRoot);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function isLegacyOpenClawPluginPath(value) {
|
|
537
|
+
return Boolean(legacyReleaseRoot(value, ["integrations", "openclaw-channel"], "openclaw"));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function isPriorStableOpenClawPluginPath(candidate, current) {
|
|
541
|
+
const tail = ["integrations", "openclaw-channel"];
|
|
542
|
+
const currentPath = path.resolve(String(current || ""));
|
|
543
|
+
const candidatePath = path.resolve(String(candidate || ""));
|
|
544
|
+
const currentPackageRoot = path.dirname(path.dirname(currentPath));
|
|
545
|
+
const candidatePackageRoot = path.dirname(path.dirname(candidatePath));
|
|
546
|
+
const releasesRoot = path.dirname(currentPackageRoot);
|
|
547
|
+
if (!/^openclaw-[a-f0-9]{64}$/i.test(path.basename(currentPackageRoot))
|
|
548
|
+
|| !/^openclaw-[a-f0-9]{64}$/i.test(path.basename(candidatePackageRoot))
|
|
549
|
+
|| !samePath(path.dirname(candidatePackageRoot), releasesRoot)
|
|
550
|
+
|| !samePath(currentPath, path.join(currentPackageRoot, ...tail))
|
|
551
|
+
|| !samePath(candidatePath, path.join(candidatePackageRoot, ...tail))) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
return !samePath(candidatePackageRoot, currentPackageRoot);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isLegacyReleaseFile(value, client, tail) {
|
|
558
|
+
return Boolean(legacyReleaseRoot(value, tail, client));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function legacyReleaseRoot(value, tail, expectedClient = "workbuddy") {
|
|
562
|
+
const candidate = String(value || "");
|
|
563
|
+
if (!path.isAbsolute(candidate)) return null;
|
|
564
|
+
const normalized = path.resolve(candidate);
|
|
565
|
+
const segments = normalized.split(path.sep);
|
|
566
|
+
if (segments.length < tail.length + 3) return null;
|
|
567
|
+
const actualTail = segments.slice(-tail.length);
|
|
568
|
+
if (!actualTail.every((segment, index) => segment.toLowerCase() === tail[index].toLowerCase())) return null;
|
|
569
|
+
const releaseIndex = segments.length - tail.length - 1;
|
|
570
|
+
const releaseName = segments[releaseIndex];
|
|
571
|
+
if (!new RegExp(`^${escapeRegExp(expectedClient)}-[0-9]+\\.[0-9]+\\.[0-9]+-[A-Za-z0-9._-]+$`, "i").test(releaseName)) return null;
|
|
572
|
+
if (segments[releaseIndex - 1]?.toLowerCase() !== "releases"
|
|
573
|
+
|| segments[releaseIndex - 2]?.toLowerCase() !== "lynk") return null;
|
|
574
|
+
return segments.slice(0, releaseIndex + 1).join(path.sep);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function normalizeNeutralHostStoreRoot(value) {
|
|
578
|
+
const input = String(value || "").trim();
|
|
579
|
+
if (!path.isAbsolute(input)) throw new Error("neutral_host_store_root_invalid");
|
|
580
|
+
const root = path.resolve(input);
|
|
581
|
+
if (path.basename(root).toLowerCase() !== "host") throw new Error("neutral_host_store_root_invalid");
|
|
582
|
+
return root;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function exactObject(value, expected) {
|
|
586
|
+
if (!plainObject(value)) return false;
|
|
587
|
+
const keys = Object.keys(value).sort();
|
|
588
|
+
const expectedKeys = Object.keys(expected).sort();
|
|
589
|
+
return keys.length === expectedKeys.length
|
|
590
|
+
&& keys.every((key, index) => key === expectedKeys[index] && value[key] === expected[key]);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function plainObject(value) {
|
|
594
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function samePath(left, right) {
|
|
598
|
+
const a = path.resolve(String(left || ""));
|
|
599
|
+
const b = path.resolve(String(right || ""));
|
|
600
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function escapeRegExp(value) {
|
|
604
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function normalizeMcpEntryArgs(value) {
|
|
608
|
+
const args = value == null ? [] : value;
|
|
609
|
+
if (!Array.isArray(args) || args.length > 12) throw new Error("agent_setup_entry_args_invalid");
|
|
610
|
+
return args.map((item) => {
|
|
611
|
+
const text = String(item ?? "");
|
|
612
|
+
if (!text || text.length > 2048 || /[\0\r\n]/.test(text)
|
|
613
|
+
|| /hellolynk_(?:bind|device)_v1_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]{16,}/i.test(text)) {
|
|
614
|
+
throw new Error("agent_setup_entry_args_invalid");
|
|
615
|
+
}
|
|
616
|
+
return text;
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function parseMcpEntryArgs(value) {
|
|
621
|
+
if (value == null) return [];
|
|
622
|
+
let parsed;
|
|
623
|
+
try { parsed = JSON.parse(String(value)); } catch { throw new Error("agent_setup_entry_args_invalid"); }
|
|
624
|
+
return normalizeMcpEntryArgs(parsed);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function assertExistingOpenClawNeutralConfig(config) {
|
|
628
|
+
if (!Object.hasOwn(config, "channels")) return;
|
|
629
|
+
const channels = config.channels;
|
|
630
|
+
if (!channels || typeof channels !== "object" || Array.isArray(channels)) {
|
|
631
|
+
throw new Error("neutral_openclaw_legacy_config_rejected");
|
|
632
|
+
}
|
|
633
|
+
if (!Object.hasOwn(channels, "hellolynk")) return;
|
|
634
|
+
const hellolynk = channels.hellolynk;
|
|
635
|
+
const expected = buildOpenClawChannelConfig(hellolynk?.nativeModel);
|
|
636
|
+
if (!hellolynk || typeof hellolynk !== "object" || Array.isArray(hellolynk)
|
|
637
|
+
|| Object.keys(hellolynk).sort().join(",") !== Object.keys(expected).sort().join(",")
|
|
638
|
+
|| hellolynk.enabled !== true
|
|
639
|
+
|| hellolynk.hostMode !== "neutral"
|
|
640
|
+
|| hellolynk.bindingRef !== "protected-store:openclaw"
|
|
641
|
+
|| !["bindingRef", "enabled", "hostMode"].every((key) => Object.hasOwn(hellolynk, key))) {
|
|
642
|
+
throw new Error("neutral_openclaw_legacy_config_rejected");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function requireRegularFile(value, code) {
|
|
647
|
+
const absolute = path.resolve(String(value || ""));
|
|
648
|
+
let metadata;
|
|
649
|
+
try { metadata = fs.lstatSync(absolute); } catch { throw new Error(code); }
|
|
650
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(code);
|
|
651
|
+
return absolute;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function runAgentConfigurator(argv = process.argv.slice(2)) {
|
|
655
|
+
const { client, options } = parseArgs(argv);
|
|
656
|
+
if (client === "openclaw") return await configureOpenClaw({
|
|
657
|
+
configPath: required(options, "config-path"), pluginPath: required(options, "plugin-path"),
|
|
658
|
+
});
|
|
659
|
+
if (["trae-work", "workbuddy"].includes(client)) return await configureNeutralHostMcpClient({
|
|
660
|
+
client,
|
|
661
|
+
configPath: required(options, "config-path"),
|
|
662
|
+
nodePath: required(options, "node-path"),
|
|
663
|
+
proxyPath: required(options, "proxy-path"),
|
|
664
|
+
entryArgs: parseMcpEntryArgs(options["entry-args-json"]),
|
|
665
|
+
});
|
|
666
|
+
throw new Error("agent_setup_client_unsupported");
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async function requireOpenClawNeutralBinding(options = {}) {
|
|
670
|
+
const provider = options.bindingProvider || (() => createHostProtectedStore({
|
|
671
|
+
...(options.storeRoot ? { root: options.storeRoot } : {}),
|
|
672
|
+
}).resolveOpenClawNeutralBinding());
|
|
673
|
+
if (typeof provider !== "function") throw new Error("neutral_capability_provider_required");
|
|
674
|
+
let binding;
|
|
675
|
+
try {
|
|
676
|
+
binding = await provider();
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (options.allowUnconnected === true && ["host_installation_missing", "host_delegate_binding_missing", "neutral_openclaw_binding_configuration_required"].includes(error?.code)) return;
|
|
679
|
+
throw new Error("neutral_openclaw_binding_unavailable");
|
|
680
|
+
}
|
|
681
|
+
if (!binding || binding.adapterId !== "openclaw" || Number(binding.capability?.version) !== 2
|
|
682
|
+
|| binding.capability?.state !== "active") {
|
|
683
|
+
throw new Error("neutral_openclaw_binding_invalid");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function publicError(error) {
|
|
688
|
+
const code = String(error?.code || error?.message || "agent_setup_failed");
|
|
689
|
+
return /^[a-z][a-z0-9_]{0,127}$/.test(code) ? code : "agent_setup_failed";
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const directRun = process.argv[1] && path.basename(process.argv[1]) === path.basename(fileURLToPath(import.meta.url));
|
|
693
|
+
if (directRun) {
|
|
694
|
+
runAgentConfigurator().then(
|
|
695
|
+
(result) => process.stdout.write(`${JSON.stringify(result, null, 2)}\n`),
|
|
696
|
+
(error) => {
|
|
697
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: publicError(error) })}\n`);
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
}
|