@minhspark/codex-mcp-bridge 1.25.2 → 1.26.0
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +58 -0
- package/README.md +81 -32
- package/docs/new-macos-setup.md +47 -21
- package/docs/new-windows-setup.md +34 -9
- package/docs/project-onboarding.md +21 -8
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/background-relay-hidden.vbs +11 -0
- package/scripts/install-antigravity-bridge.mjs +65 -23
- package/scripts/install-background-relay.mjs +428 -0
- package/scripts/install-claude-code.mjs +2 -2
- package/scripts/install-claude-desktop.mjs +12 -7
- package/scripts/install-native-relay.mjs +2 -2
- package/scripts/setup-background-relay.mjs +58 -0
- package/scripts/setup.mjs +3 -3
- package/scripts/worker-relay.mjs +41 -0
- package/src/antigravity-bridge.mjs +96 -27
- package/src/antigravity-cli.mjs +13 -2
- package/src/antigravity-deliveries.mjs +32 -8
- package/src/antigravity-ipc.mjs +18 -9
- package/src/antigravity-outbound-bridge.mjs +71 -23
- package/src/antigravity-outbound-targets.mjs +100 -10
- package/src/background-antigravity-worker.mjs +192 -0
- package/src/background-claude-worker.mjs +240 -0
- package/src/background-codex-worker.mjs +344 -0
- package/src/background-relay-client.mjs +107 -0
- package/src/background-relay-config.mjs +98 -0
- package/src/background-relay-daemon.mjs +60 -0
- package/src/background-relay-mcp.mjs +45 -0
- package/src/background-relay-protocol.mjs +54 -0
- package/src/background-relay-registration.mjs +198 -0
- package/src/background-relay-service.mjs +311 -0
- package/src/background-relay-storage.mjs +203 -0
- package/src/background-relay-storage.ps1 +64 -0
- package/src/background-relay.mjs +373 -0
- package/src/bridge-readiness.mjs +2 -2
- package/src/claude-bridge.mjs +43 -25
- package/src/claude-code-registration.mjs +2 -1
- package/src/claude-session-router.mjs +8 -15
- package/src/index.mjs +39 -22
- package/src/mcp-supervisor.mjs +26 -16
- package/src/native-relay-companion.mjs +4 -1
- package/src/native-relay.mjs +5 -1
- package/src/peer-protocol.mjs +9 -6
- package/src/recipient-preflight.mjs +81 -0
- package/src/reload-control.mjs +2 -2
- package/src/setup.mjs +62 -25
- package/src/thread-delivery.mjs +10 -5
- package/src/windows-pipe-acl.ps1 +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { callBackgroundRelay } from "../src/background-relay-client.mjs";
|
|
9
|
+
import { readRelayConfiguration, writeRelayJson } from "../src/background-relay-config.mjs";
|
|
10
|
+
import { RELAY_SERVICE_OWNER, relayServiceRuntimeId } from "../src/background-relay-service.mjs";
|
|
11
|
+
import { isMainModule, stableNodeBin } from "../src/install-paths.mjs";
|
|
12
|
+
import { readRegularConfigFile } from "../src/project-policy.mjs";
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const TASK_SCRIPT = String.raw`$ErrorActionPreference = 'Stop'
|
|
16
|
+
$relayRequest = [Console]::In.ReadToEnd() | ConvertFrom-Json
|
|
17
|
+
try {
|
|
18
|
+
$relayScheduler = New-Object -ComObject Schedule.Service
|
|
19
|
+
$relayScheduler.Connect()
|
|
20
|
+
$relayFolder = $relayScheduler.GetFolder('\')
|
|
21
|
+
if ($relayRequest.action -eq 'register') {
|
|
22
|
+
$null = $relayFolder.RegisterTask($relayRequest.name, $relayRequest.xml, 6, $null, $null, 3, $null)
|
|
23
|
+
}
|
|
24
|
+
$relayTask = $null
|
|
25
|
+
try { $relayTask = $relayFolder.GetTask($relayRequest.name) }
|
|
26
|
+
catch {
|
|
27
|
+
$relayException = $_.Exception
|
|
28
|
+
$relayMissing = $false
|
|
29
|
+
while ($null -ne $relayException) {
|
|
30
|
+
if ($relayException.HResult -eq -2147024894) { $relayMissing = $true }
|
|
31
|
+
$relayException = $relayException.InnerException
|
|
32
|
+
}
|
|
33
|
+
if (-not $relayMissing) { throw }
|
|
34
|
+
}
|
|
35
|
+
if ($relayRequest.action -eq 'start') {
|
|
36
|
+
if ($null -eq $relayTask) { throw 'Missing task' }
|
|
37
|
+
$null = $relayTask.Run($null)
|
|
38
|
+
}
|
|
39
|
+
$relayUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
40
|
+
if ($null -eq $relayTask) {
|
|
41
|
+
@{ exists = $false; user = $relayUser } | ConvertTo-Json -Compress
|
|
42
|
+
} else {
|
|
43
|
+
$relayDefinition = $relayTask.Definition
|
|
44
|
+
$relayAction = $relayDefinition.Actions.Item(1)
|
|
45
|
+
$relayPrincipalId = [string]$relayDefinition.Principal.UserId
|
|
46
|
+
$relayPrincipalSid = $null
|
|
47
|
+
try {
|
|
48
|
+
if ($relayPrincipalId -match '^S-\d+(?:-\d+)+$') {
|
|
49
|
+
$relayPrincipalSid = [Security.Principal.SecurityIdentifier]::new($relayPrincipalId).Value
|
|
50
|
+
} else {
|
|
51
|
+
$relayPrincipalSid = [Security.Principal.NTAccount]::new($relayPrincipalId).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
@{ exists = $true; user = $relayUser; enabled = [bool]$relayTask.Enabled; state = [int]$relayTask.State;
|
|
55
|
+
description = [string]$relayDefinition.RegistrationInfo.Description;
|
|
56
|
+
command = [string]$relayAction.Path; arguments = [string]$relayAction.Arguments;
|
|
57
|
+
actions = [int]$relayDefinition.Actions.Count; logonType = [int]$relayDefinition.Principal.LogonType;
|
|
58
|
+
principalId = $relayPrincipalId; principalSid = $relayPrincipalSid; userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value;
|
|
59
|
+
runLevel = [int]$relayDefinition.Principal.RunLevel; hidden = [bool]$relayDefinition.Settings.Hidden;
|
|
60
|
+
executionTimeLimit = [string]$relayDefinition.Settings.ExecutionTimeLimit;
|
|
61
|
+
multipleInstances = [int]$relayDefinition.Settings.MultipleInstances } | ConvertTo-Json -Compress
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
@{ error = 'TASK_SCHEDULER_UNAVAILABLE' } | ConvertTo-Json -Compress
|
|
65
|
+
exit 1
|
|
66
|
+
}`;
|
|
67
|
+
|
|
68
|
+
function execute(command, args, options) {
|
|
69
|
+
try { return { status: 0, stdout: execFileSync(command, args, options), stderr: "" }; }
|
|
70
|
+
catch (error) { return { status: Number.isInteger(error.status) ? error.status : 1, stdout: error.stdout ?? "", stderr: "" }; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function installFailure(code) { return Object.assign(new Error(code), { code }); }
|
|
74
|
+
const escapeXml = (value) => String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
75
|
+
const quoteWindows = (value) => `"${String(value).replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/g, "$1$1")}"`;
|
|
76
|
+
const quoteSystemd = (value, expandDollar = true) => `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, () => expandDollar ? "$$" : "$")}"`;
|
|
77
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
78
|
+
|
|
79
|
+
function startupLocation(input, segments) {
|
|
80
|
+
if (typeof input !== "string" || !path.isAbsolute(input) || /[\u0000-\u001f\u007f]/.test(input)) throw installFailure("SERVICE_STORAGE_UNSAFE");
|
|
81
|
+
let ancestor = path.resolve(input);
|
|
82
|
+
const suffix = [];
|
|
83
|
+
while (true) {
|
|
84
|
+
try { fs.lstatSync(ancestor); break; }
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT" || ancestor === path.dirname(ancestor)) throw installFailure("SERVICE_STORAGE_UNSAFE");
|
|
87
|
+
suffix.unshift(path.basename(ancestor));
|
|
88
|
+
ancestor = path.dirname(ancestor);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
ancestor = fs.realpathSync.native(ancestor);
|
|
92
|
+
const root = path.resolve(ancestor, ...suffix);
|
|
93
|
+
const file = path.resolve(root, ...segments);
|
|
94
|
+
if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
|
|
95
|
+
const location = { root, file, ancestor, identities: new Map() };
|
|
96
|
+
startupDirectories(location);
|
|
97
|
+
return location;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function startupDirectories(location, create = false) {
|
|
101
|
+
const relative = path.relative(location.ancestor, path.dirname(location.file));
|
|
102
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
|
|
103
|
+
let current = location.ancestor;
|
|
104
|
+
for (const segment of ["", ...relative.split(path.sep).filter(Boolean)]) {
|
|
105
|
+
if (segment) current = path.join(current, segment);
|
|
106
|
+
let entry;
|
|
107
|
+
try { entry = fs.lstatSync(current, { bigint: true }); }
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code !== "ENOENT" || location.identities.has(current)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
|
|
110
|
+
if (!create) return;
|
|
111
|
+
fs.mkdirSync(current, { mode: 0o700 });
|
|
112
|
+
entry = fs.lstatSync(current, { bigint: true });
|
|
113
|
+
}
|
|
114
|
+
const identity = `${entry.dev}:${entry.ino}:${entry.birthtimeNs}`;
|
|
115
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || path.relative(current, fs.realpathSync.native(current))) throw installFailure("SERVICE_STORAGE_UNSAFE");
|
|
116
|
+
if (location.identities.has(current) && location.identities.get(current) !== identity) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
|
|
117
|
+
location.identities.set(current, identity);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function startupText(location) {
|
|
122
|
+
startupDirectories(location);
|
|
123
|
+
return readRegularConfigFile(location.file, { missing: true, maxBytes: 128 * 1024 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sameStartupFile(location, candidate) {
|
|
127
|
+
if (typeof candidate !== "string" || !path.isAbsolute(candidate) || path.basename(candidate) !== path.basename(location.file)) return false;
|
|
128
|
+
try {
|
|
129
|
+
const entry = fs.lstatSync(candidate);
|
|
130
|
+
return entry.isFile() && !entry.isSymbolicLink() && path.relative(location.file, fs.realpathSync.native(candidate)) === "";
|
|
131
|
+
} catch { return false; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function atomicText(location, content, previous) {
|
|
135
|
+
startupDirectories(location, true);
|
|
136
|
+
const file = location.file;
|
|
137
|
+
if (startupText(location) !== previous) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
|
|
138
|
+
const directory = path.dirname(file);
|
|
139
|
+
const temporary = path.resolve(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
140
|
+
if (!temporary.startsWith(directory.endsWith(path.sep) ? directory : `${directory}${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
|
|
141
|
+
let descriptor;
|
|
142
|
+
let identity;
|
|
143
|
+
const sameTemporary = (entry) => entry.isFile() && !entry.isSymbolicLink() && entry.nlink === 1n
|
|
144
|
+
&& ["dev", "ino", "birthtimeNs"].every((field) => entry[field] === identity[field]);
|
|
145
|
+
try {
|
|
146
|
+
descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
147
|
+
identity = fs.fstatSync(descriptor, { bigint: true });
|
|
148
|
+
fs.writeFileSync(descriptor, content);
|
|
149
|
+
fs.fsyncSync(descriptor);
|
|
150
|
+
fs.closeSync(descriptor);
|
|
151
|
+
descriptor = undefined;
|
|
152
|
+
if (startupText(location) !== previous || !sameTemporary(fs.lstatSync(temporary, { bigint: true }))) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
|
|
153
|
+
fs.renameSync(temporary, file);
|
|
154
|
+
} finally {
|
|
155
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
156
|
+
if (identity) {
|
|
157
|
+
startupDirectories(location);
|
|
158
|
+
let remaining;
|
|
159
|
+
try { remaining = fs.lstatSync(temporary, { bigint: true }); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
160
|
+
if (remaining) {
|
|
161
|
+
if (!sameTemporary(remaining)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
|
|
162
|
+
fs.unlinkSync(temporary);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function taskXml({ command, args, user, enabled }) {
|
|
169
|
+
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
170
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
171
|
+
<RegistrationInfo><Description>${escapeXml(RELAY_SERVICE_OWNER)}</Description></RegistrationInfo>
|
|
172
|
+
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${escapeXml(user)}</UserId></LogonTrigger></Triggers>
|
|
173
|
+
<Principals><Principal id="Author"><UserId>${escapeXml(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
174
|
+
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><AllowHardTerminate>false</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>${enabled}</Enabled><Hidden>true</Hidden><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure></Settings>
|
|
175
|
+
<Actions Context="Author"><Exec><Command>${escapeXml(command)}</Command><Arguments>${escapeXml(args)}</Arguments></Exec></Actions>
|
|
176
|
+
</Task>\n`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function savedEnvironment(env, config) {
|
|
180
|
+
const result = { AGENT_RELAY_DIRECTORY: config.paths.root, AGENT_RELAY_SERVICE_OWNER: RELAY_SERVICE_OWNER };
|
|
181
|
+
for (const name of ["HOME", "PATH", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME"]) {
|
|
182
|
+
if (typeof env[name] === "string" && env[name] && !/[\u0000-\u001f\u007f]/.test(env[name])) result[name] = env[name];
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function plist(label, node, service, environment, enabled) {
|
|
188
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
189
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
190
|
+
<plist version="1.0"><dict>
|
|
191
|
+
<key>Label</key><string>${escapeXml(label)}</string>
|
|
192
|
+
<key>ProgramArguments</key><array><string>${escapeXml(node)}</string><string>${escapeXml(service)}</string></array>
|
|
193
|
+
<key>EnvironmentVariables</key><dict>${Object.entries(environment).map(([key, value]) => `<key>${escapeXml(key)}</key><string>${escapeXml(value)}</string>`).join("")}</dict>
|
|
194
|
+
<key>RunAtLoad</key><${enabled ? "true" : "false"}/><key>Disabled</key><${enabled ? "false" : "true"}/>
|
|
195
|
+
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
196
|
+
<key>ThrottleInterval</key><integer>10</integer><key>ProcessType</key><string>Background</string>
|
|
197
|
+
<key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>/dev/null</string>
|
|
198
|
+
</dict></plist>\n`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function unit(node, service, environment) {
|
|
202
|
+
return `[Unit]
|
|
203
|
+
Description=${RELAY_SERVICE_OWNER}
|
|
204
|
+
StartLimitIntervalSec=0
|
|
205
|
+
|
|
206
|
+
[Service]
|
|
207
|
+
Type=simple
|
|
208
|
+
ExecStart=${quoteSystemd(node)} ${quoteSystemd(service)}
|
|
209
|
+
${Object.entries(environment).map(([name, value]) => `Environment=${quoteSystemd(`${name}=${value}`, false)}`).join("\n")}
|
|
210
|
+
Restart=on-failure
|
|
211
|
+
RestartSec=10
|
|
212
|
+
TimeoutStopSec=infinity
|
|
213
|
+
StandardOutput=null
|
|
214
|
+
StandardError=null
|
|
215
|
+
|
|
216
|
+
[Install]
|
|
217
|
+
WantedBy=default.target
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function installWindows(context) {
|
|
222
|
+
const { env, config, root, node, service, label, invoke, mutate } = context;
|
|
223
|
+
const command = path.win32.join(env.SystemRoot || env.WINDIR || "C:\\Windows", "System32", "wscript.exe");
|
|
224
|
+
const launcher = path.join(root, "scripts", "background-relay-hidden.vbs");
|
|
225
|
+
if (![node, service, launcher].every((value) => !/["%\r\n\0]/.test(value))) throw installFailure("SERVICE_LAUNCH_PATH_UNSUPPORTED");
|
|
226
|
+
if (!fs.existsSync(launcher)) throw installFailure("SERVICE_RUNTIME_MISSING");
|
|
227
|
+
if (context.nativeRun && !fs.existsSync(command)) throw installFailure("SERVICE_HIDDEN_LAUNCHER_UNAVAILABLE");
|
|
228
|
+
const args = ["//B", "//Nologo", quoteWindows(launcher), quoteWindows(node), quoteWindows(service), quoteWindows(config.paths.root)].join(" ");
|
|
229
|
+
const powershell = path.win32.join(env.SystemRoot || env.WINDIR || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
230
|
+
const task = async (action, extra = {}) => {
|
|
231
|
+
const response = await invoke(powershell, ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-EncodedCommand", Buffer.from(TASK_SCRIPT, "utf16le").toString("base64")], {
|
|
232
|
+
input: JSON.stringify({ action, name: label, ...extra }),
|
|
233
|
+
});
|
|
234
|
+
let value;
|
|
235
|
+
try { value = JSON.parse(response.stdout); } catch { throw installFailure("TASK_SCHEDULER_UNAVAILABLE"); }
|
|
236
|
+
if (response.status !== 0 || value.error || typeof value.exists !== "boolean") throw installFailure("TASK_SCHEDULER_UNAVAILABLE");
|
|
237
|
+
return value;
|
|
238
|
+
};
|
|
239
|
+
let current = await task("query");
|
|
240
|
+
if (current.exists && current.description !== RELAY_SERVICE_OWNER) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
|
|
241
|
+
const sameUser = (value) => typeof value.principalSid === "string" && /^S-\d+(?:-\d+)+$/i.test(value.principalSid)
|
|
242
|
+
&& typeof value.userSid === "string" && value.principalSid.toLowerCase() === value.userSid.toLowerCase();
|
|
243
|
+
if (current.exists && !sameUser(current)) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
|
|
244
|
+
const enabled = config.enabled && (!current.exists || current.enabled === true);
|
|
245
|
+
const matches = (value) => value.exists && value.description === RELAY_SERVICE_OWNER && value.command.toLowerCase() === command.toLowerCase()
|
|
246
|
+
&& value.arguments === args && value.actions === 1 && value.logonType === 3 && value.runLevel === 0
|
|
247
|
+
&& value.hidden === true && value.executionTimeLimit === "PT0S" && value.multipleInstances === 2 && value.enabled === enabled && sameUser(value);
|
|
248
|
+
const changed = !matches(current);
|
|
249
|
+
if (changed && mutate) {
|
|
250
|
+
if (typeof current.user !== "string" || !current.user || /[\r\n\0]/.test(current.user)) throw installFailure("TASK_SCHEDULER_UNAVAILABLE");
|
|
251
|
+
await task("register", { xml: taskXml({ command, args, user: current.user, enabled }) });
|
|
252
|
+
current = await task("query");
|
|
253
|
+
}
|
|
254
|
+
if (mutate && !matches(current)) throw installFailure("SERVICE_READBACK_FAILED");
|
|
255
|
+
if (mutate) await context.prepareRuntime();
|
|
256
|
+
let started = false;
|
|
257
|
+
if (enabled && mutate && current.state !== 4) { await task("start"); started = true; }
|
|
258
|
+
return { configured: matches(current), enabled, changed, started, registration: label, activationPending: changed && current.state === 4 };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function installMac(context) {
|
|
262
|
+
const { env, config, node, service, label, invoke, mutate, uid } = context;
|
|
263
|
+
if (!Number.isSafeInteger(uid) || uid < 0) throw installFailure("SERVICE_USER_UNAVAILABLE");
|
|
264
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
265
|
+
const location = startupLocation(home, ["Library", "LaunchAgents", `${label}.plist`]);
|
|
266
|
+
const file = location.file;
|
|
267
|
+
const previous = startupText(location);
|
|
268
|
+
if (previous !== null && !previous.includes(escapeXml(RELAY_SERVICE_OWNER))) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
|
|
269
|
+
const domain = `gui/${uid}`;
|
|
270
|
+
const disabled = await invoke("/bin/launchctl", ["print-disabled", domain]);
|
|
271
|
+
if (disabled.status !== 0) throw installFailure("LAUNCH_AGENT_UNAVAILABLE");
|
|
272
|
+
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
273
|
+
const userDisabled = new RegExp(`"${escaped}"\\s*=>\\s*true`).test(disabled.stdout)
|
|
274
|
+
|| /<key>Disabled<\/key>\s*<true\s*\/>/.test(previous ?? "");
|
|
275
|
+
const enabled = config.enabled && !userDisabled;
|
|
276
|
+
const wanted = plist(label, node, service, savedEnvironment(env, config), enabled);
|
|
277
|
+
const loaded = await invoke("/bin/launchctl", ["print", `${domain}/${label}`]);
|
|
278
|
+
if (loaded.status === 0 && previous === null) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
|
|
279
|
+
if (loaded.status !== 0 && loaded.status !== 113) throw installFailure("LAUNCH_AGENT_UNAVAILABLE");
|
|
280
|
+
const changed = previous !== wanted;
|
|
281
|
+
if (changed && mutate) atomicText(location, wanted, previous);
|
|
282
|
+
const configured = startupText(location) === wanted;
|
|
283
|
+
if (mutate && !configured) throw installFailure("SERVICE_READBACK_FAILED");
|
|
284
|
+
if (mutate) await context.prepareRuntime();
|
|
285
|
+
let started = false;
|
|
286
|
+
if (enabled && mutate) {
|
|
287
|
+
const args = loaded.status === 0 ? ["kickstart", `${domain}/${label}`] : ["bootstrap", domain, file];
|
|
288
|
+
if (loaded.status !== 0 || !/\bstate = running\b/.test(loaded.stdout)) {
|
|
289
|
+
if ((await invoke("/bin/launchctl", args)).status !== 0) throw installFailure("SERVICE_START_FAILED");
|
|
290
|
+
started = true;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return { configured, enabled, changed, started, registration: label, activationPending: changed && loaded.status === 0 };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function installLinux(context) {
|
|
297
|
+
const { env, config, node, service, label, invoke, mutate } = context;
|
|
298
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
299
|
+
const location = startupLocation(env.XDG_CONFIG_HOME || path.join(home, ".config"), ["systemd", "user", `${label}.service`]);
|
|
300
|
+
const file = location.file;
|
|
301
|
+
const query = async () => {
|
|
302
|
+
const response = await invoke("systemctl", ["--user", "show", `${label}.service`, "--property=LoadState,UnitFileState,ActiveState,FragmentPath", "--no-pager"]);
|
|
303
|
+
const values = Object.fromEntries(response.stdout.split(/\r?\n/).filter((line) => line.includes("=")).map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]));
|
|
304
|
+
if (!values.LoadState || response.status !== 0 && values.LoadState !== "not-found") throw installFailure("USER_SYSTEMD_UNAVAILABLE");
|
|
305
|
+
return values;
|
|
306
|
+
};
|
|
307
|
+
const values = await query();
|
|
308
|
+
if (values.UnitFileState === "masked" || values.UnitFileState === "masked-runtime") return { configured: false, enabled: false, changed: false, started: false, registration: label, code: "SERVICE_MASKED" };
|
|
309
|
+
const previous = startupText(location);
|
|
310
|
+
if (previous !== null && !previous.includes(`Description=${RELAY_SERVICE_OWNER}\n`)
|
|
311
|
+
|| values.FragmentPath && !sameStartupFile(location, values.FragmentPath)) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
|
|
312
|
+
const userDisabled = previous !== null && ["disabled", "disabled-runtime"].includes(values.UnitFileState);
|
|
313
|
+
const enabled = config.enabled && !userDisabled;
|
|
314
|
+
const wanted = unit(node, service, savedEnvironment(env, config));
|
|
315
|
+
const changed = previous !== wanted;
|
|
316
|
+
if (changed && mutate) {
|
|
317
|
+
atomicText(location, wanted, previous);
|
|
318
|
+
if ((await invoke("systemctl", ["--user", "daemon-reload"])).status !== 0) throw installFailure("USER_SYSTEMD_UNAVAILABLE");
|
|
319
|
+
}
|
|
320
|
+
if (enabled && mutate && !["enabled", "enabled-runtime"].includes(values.UnitFileState)) {
|
|
321
|
+
if ((await invoke("systemctl", ["--user", "enable", `${label}.service`])).status !== 0) throw installFailure("SERVICE_REGISTRATION_FAILED");
|
|
322
|
+
}
|
|
323
|
+
if (mutate) await context.prepareRuntime();
|
|
324
|
+
let started = false;
|
|
325
|
+
if (enabled && mutate && values.ActiveState !== "active") {
|
|
326
|
+
if ((await invoke("systemctl", ["--user", "start", `${label}.service`])).status !== 0) throw installFailure("SERVICE_START_FAILED");
|
|
327
|
+
started = true;
|
|
328
|
+
}
|
|
329
|
+
const verified = mutate ? await query() : values;
|
|
330
|
+
const configured = startupText(location) === wanted
|
|
331
|
+
&& verified.LoadState === "loaded" && sameStartupFile(location, verified.FragmentPath)
|
|
332
|
+
&& (!enabled || ["enabled", "enabled-runtime"].includes(verified.UnitFileState));
|
|
333
|
+
if (mutate && !configured) throw installFailure("SERVICE_READBACK_FAILED");
|
|
334
|
+
return { configured, enabled, changed, started, registration: label };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export async function installBackgroundRelay({ env = process.env, platform = process.platform, root = ROOT, run = execute,
|
|
338
|
+
dryRun = false, check = false, health = callBackgroundRelay, sleep = pause, pollAttempts = 15, uid = process.getuid?.() } = {}) {
|
|
339
|
+
const base = { configured: false, enabled: false, started: false, liveVerified: false, ready: false };
|
|
340
|
+
let config;
|
|
341
|
+
try {
|
|
342
|
+
config = readRelayConfiguration({ env, platform, missing: true });
|
|
343
|
+
if (!config) return { ...base, code: "WORKER_RELAY_NOT_CONFIGURED", reason: "Configure the background relay before installing its user service" };
|
|
344
|
+
if (!["win32", "darwin", "linux"].includes(platform)) return { ...base, code: "SERVICE_PLATFORM_UNSUPPORTED", reason: "Background relay user startup is unsupported on this platform" };
|
|
345
|
+
if (!path.isAbsolute(root)) throw installFailure("SERVICE_RUNTIME_MISSING");
|
|
346
|
+
root = fs.realpathSync.native(root);
|
|
347
|
+
const node = stableNodeBin(env);
|
|
348
|
+
const service = path.join(root, "src", "background-relay-service.mjs");
|
|
349
|
+
if (!path.isAbsolute(root) || !fs.existsSync(service) || !fs.existsSync(path.join(root, "src", "background-relay-daemon.mjs"))) throw installFailure("SERVICE_RUNTIME_MISSING");
|
|
350
|
+
let packageVersion;
|
|
351
|
+
try { packageVersion = JSON.parse(readRegularConfigFile(path.join(root, "package.json"), { maxBytes: 128 * 1024 })).version; }
|
|
352
|
+
catch { throw installFailure("SERVICE_RUNTIME_INVALID"); }
|
|
353
|
+
if (typeof packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.+-]+)?$/.test(packageVersion) || packageVersion.length > 96) throw installFailure("SERVICE_RUNTIME_INVALID");
|
|
354
|
+
if ([root, config.paths.root, node].some((value) => /[\u0000-\u001f\u007f]/.test(value))) throw installFailure("SERVICE_LAUNCH_PATH_UNSUPPORTED");
|
|
355
|
+
const hash = createHash("sha256").update(platform === "win32" ? config.paths.root.toLowerCase() : config.paths.root).digest("hex").slice(0, 12);
|
|
356
|
+
const label = platform === "win32" ? `MinhSpark Agent Relay ${hash}` : `com.minhspark.agent-relay.${hash}`;
|
|
357
|
+
const invoke = async (command, args, extra = {}) => {
|
|
358
|
+
const result = await run(command, args, { env, encoding: "utf8", windowsHide: true, shell: false, timeout: 15000, maxBuffer: 256 * 1024, stdio: ["pipe", "pipe", "pipe"], ...extra });
|
|
359
|
+
return { status: result?.status ?? result?.exitCode ?? 0, stdout: String(result?.stdout ?? "") };
|
|
360
|
+
};
|
|
361
|
+
const runtimeFile = path.join(config.paths.root, "service-runtime.json");
|
|
362
|
+
const desiredRuntime = { schemaVersion: 1, owner: RELAY_SERVICE_OWNER, packageRoot: root, nodeExecutable: node, packageVersion,
|
|
363
|
+
environment: Object.fromEntries(Object.entries(savedEnvironment(env, config)).filter(([name]) => !name.startsWith("AGENT_RELAY_"))) };
|
|
364
|
+
const expectedRuntimeId = relayServiceRuntimeId(desiredRuntime);
|
|
365
|
+
const readRuntime = () => {
|
|
366
|
+
const raw = readRegularConfigFile(runtimeFile, { missing: true, maxBytes: 64 * 1024 });
|
|
367
|
+
if (raw === null) return null;
|
|
368
|
+
let value;
|
|
369
|
+
try { value = JSON.parse(raw); } catch { throw installFailure("SERVICE_RUNTIME_INVALID"); }
|
|
370
|
+
if (value?.schemaVersion !== 1 || value.owner !== RELAY_SERVICE_OWNER) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
|
|
371
|
+
return value;
|
|
372
|
+
};
|
|
373
|
+
const previousRuntime = readRuntime();
|
|
374
|
+
const runtimeChanged = JSON.stringify(previousRuntime) !== JSON.stringify(desiredRuntime);
|
|
375
|
+
const prepareRuntime = async () => {
|
|
376
|
+
if (JSON.stringify(readRuntime()) !== JSON.stringify(previousRuntime)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
|
|
377
|
+
if (runtimeChanged) writeRelayJson(runtimeFile, desiredRuntime);
|
|
378
|
+
if (JSON.stringify(readRuntime()) !== JSON.stringify(desiredRuntime)) throw installFailure("SERVICE_READBACK_FAILED");
|
|
379
|
+
};
|
|
380
|
+
const context = { env, config, root, node, service, label, invoke, mutate: !dryRun && !check, uid, nativeRun: run === execute, prepareRuntime };
|
|
381
|
+
const installed = await ({ win32: installWindows, darwin: installMac, linux: installLinux })[platform](context);
|
|
382
|
+
installed.configured = installed.configured && JSON.stringify(readRuntime()) === JSON.stringify(desiredRuntime);
|
|
383
|
+
const result = { ...base, ...installed, dryRun, deliveryEnabled: installed.enabled, liveVerified: false,
|
|
384
|
+
expectedVersion: packageVersion, runtimeChanged, activationPending: installed.activationPending === true || runtimeChanged && previousRuntime !== null };
|
|
385
|
+
if (!installed.enabled) {
|
|
386
|
+
result.notice = config.enabled ? "User startup is intentionally disabled; the installer preserved that choice" : "Background relay is intentionally disabled in its configuration";
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
if (!installed.configured || dryRun) return result;
|
|
390
|
+
const attempts = check ? 1 : Math.max(1, Math.min(30, pollAttempts));
|
|
391
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
392
|
+
try {
|
|
393
|
+
const status = await health("status", {}, { env, connection: { endpoint: config.endpoint, token: config.token, workerId: null }, timeoutMs: 1000 });
|
|
394
|
+
const serviceState = JSON.parse(readRegularConfigFile(path.join(config.paths.root, "service-status.json"), { maxBytes: 64 * 1024 }));
|
|
395
|
+
const owned = status?.schemaVersion === 1 && status.transportReady === true && status.desktopRequired === false
|
|
396
|
+
&& Number.isSafeInteger(status.pid) && status.pid > 0 && serviceState?.owner === RELAY_SERVICE_OWNER
|
|
397
|
+
&& serviceState.status === "running" && serviceState.daemonPid === status.pid;
|
|
398
|
+
if (owned && typeof status.version === "string" && /^[A-Za-z0-9.+_-]{1,96}$/.test(status.version)) result.version = status.version;
|
|
399
|
+
if (owned && (serviceState.runtimeId !== expectedRuntimeId || status.runtimeId !== expectedRuntimeId || status.version !== packageVersion)) result.activationPending = true;
|
|
400
|
+
if (owned && serviceState.runtimeId === expectedRuntimeId && status.runtimeId === expectedRuntimeId && status.version === packageVersion) {
|
|
401
|
+
return { ...result, activationPending: false, liveVerified: true, ready: status.accepting === true, pid: status.pid,
|
|
402
|
+
version: typeof status.version === "string" && /^[A-Za-z0-9.+_-]{1,96}$/.test(status.version) ? status.version : null,
|
|
403
|
+
...(status.accepting === true ? {} : { code: "SERVICE_DRAINING", notice: "The relay is finishing existing work before accepting new jobs" }) };
|
|
404
|
+
}
|
|
405
|
+
} catch {}
|
|
406
|
+
if (attempt + 1 < attempts) await sleep(250);
|
|
407
|
+
}
|
|
408
|
+
return { ...result, code: result.activationPending ? "SERVICE_UPDATE_PENDING" : "SERVICE_NOT_READY",
|
|
409
|
+
reason: result.activationPending ? "User startup is configured; the running service has not yet verified the desired idle runtime update"
|
|
410
|
+
: "User startup is configured, but its owned daemon did not report healthy transport" };
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const allowed = /^(?:SERVICE_[A-Z_]+|TASK_SCHEDULER_UNAVAILABLE|LAUNCH_AGENT_UNAVAILABLE|USER_SYSTEMD_UNAVAILABLE|WORKER_CONFIG_INVALID|WORKER_STORAGE_UNSAFE)$/;
|
|
413
|
+
const code = typeof error.code === "string" && allowed.test(error.code) ? error.code : "SERVICE_INSTALL_FAILED";
|
|
414
|
+
return { ...base, code, reason: "The background relay user service could not be safely configured or verified" };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
419
|
+
const args = process.argv.slice(2);
|
|
420
|
+
if (args.some((arg) => !["--check", "--dry-run", "--json"].includes(arg)) || args.includes("--check") && args.includes("--dry-run")) {
|
|
421
|
+
process.stderr.write("Usage: install-background-relay.mjs [--check|--dry-run] [--json]\n");
|
|
422
|
+
process.exitCode = 2;
|
|
423
|
+
} else {
|
|
424
|
+
const result = await installBackgroundRelay({ check: args.includes("--check"), dryRun: args.includes("--dry-run") });
|
|
425
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
426
|
+
if (!result.configured && !args.includes("--dry-run") && result.code !== "SERVICE_MASKED") process.exitCode = 1;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -68,7 +68,7 @@ try {
|
|
|
68
68
|
if (args.includes("--install")) {
|
|
69
69
|
if (args.includes("--check") || args.includes("--apply")) throw new Error("Use --install separately from --check or --apply");
|
|
70
70
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
71
|
-
const plan = planInitialCodeRegistration({ code, desktop, root, node: stableNodeBin(), environment: process.env });
|
|
71
|
+
const plan = planInitialCodeRegistration({ code, desktop, root, node: stableNodeBin(), environment: process.env, setup: args.includes("--setup") });
|
|
72
72
|
const dryRun = args.includes("--dry-run");
|
|
73
73
|
const writtenFiles = dryRun ? 0 : apply(plan);
|
|
74
74
|
console.log(JSON.stringify({ action: dryRun ? "plan" : "install", configured: !dryRun, liveVerified: false,
|
|
@@ -86,7 +86,7 @@ try {
|
|
|
86
86
|
if (args.includes("--apply")) {
|
|
87
87
|
if (!args.includes("--clients-stopped")) throw new Error("Exit Claude and the configuration manager first, then pass --clients-stopped to avoid their overwriting the migration");
|
|
88
88
|
apply(plan);
|
|
89
|
-
console.log("
|
|
89
|
+
console.log("Shared policy migration applied. Loaded Code registrations have not been verified; codex_bridge_status reports the active configuration. The configuration manager's matching entry must use the same policy file.");
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
} catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
@@ -20,10 +20,11 @@ const entryOption = process.argv.indexOf("--entry-name");
|
|
|
20
20
|
const requestedEntryName = entryOption < 0 ? undefined : process.argv[entryOption + 1];
|
|
21
21
|
if (entryOption >= 0 && !["codex-bridge", "codex-bridge-desktop"].includes(requestedEntryName)) throw new Error("--entry-name must be codex-bridge or codex-bridge-desktop");
|
|
22
22
|
const quiet = process.argv.includes("--quiet");
|
|
23
|
+
const setup = process.argv.includes("--setup");
|
|
23
24
|
const log = (...values) => { if (!quiet) console.log(...values); };
|
|
24
25
|
const reset = process.argv.includes("--reset");
|
|
25
26
|
const forceDesktopTasks = process.argv.includes("--desktop-tasks");
|
|
26
|
-
const fullAccess = process.argv.includes("--full-access") || requestedEntryName === undefined && fullAccessEnabled();
|
|
27
|
+
const fullAccess = process.argv.includes("--full-access") || !setup && requestedEntryName === undefined && fullAccessEnabled();
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* This bridge is intentionally the local hand-off point between Claude and
|
|
@@ -80,6 +81,7 @@ const original = readConfig();
|
|
|
80
81
|
const cfg = original ? JSON.parse(original.data.toString("utf8")) : {};
|
|
81
82
|
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) throw new Error("Desktop configuration must be a JSON object");
|
|
82
83
|
if (cfg.mcpServers !== undefined && (!cfg.mcpServers || typeof cfg.mcpServers !== "object" || Array.isArray(cfg.mcpServers))) throw new Error("Desktop mcpServers must be a JSON object");
|
|
84
|
+
if (setup && cfg.preferences !== undefined && (!cfg.preferences || typeof cfg.preferences !== "object" || Array.isArray(cfg.preferences))) throw new Error("Desktop preferences must be a JSON object");
|
|
83
85
|
cfg.mcpServers = cfg.mcpServers ?? {};
|
|
84
86
|
|
|
85
87
|
/**
|
|
@@ -108,7 +110,7 @@ if (bridgeEntries.length > 1 || requestedEntryName && bridgeEntries.some((name)
|
|
|
108
110
|
throw new Error(`Claude Desktop already has several bridge entries (${bridgeEntries.join(", ")}); remove all but one and re-run. No configuration was changed.`);
|
|
109
111
|
}
|
|
110
112
|
const entryName = requestedEntryName ?? bridgeEntries[0] ?? "codex-bridge";
|
|
111
|
-
if (requestedEntryName && cfg.mcpServers[entryName] !== undefined) {
|
|
113
|
+
if ((requestedEntryName || setup) && cfg.mcpServers[entryName] !== undefined) {
|
|
112
114
|
const existing = cfg.mcpServers[entryName];
|
|
113
115
|
const script = existing?.args?.[0];
|
|
114
116
|
const source = typeof script === "string" && path.isAbsolute(script) ? path.dirname(script) : null;
|
|
@@ -133,6 +135,7 @@ const desktopEnabled = desktopMode === undefined ? desktopTasksConfigured() : de
|
|
|
133
135
|
|
|
134
136
|
const kept = Object.keys(previousEnv).filter(
|
|
135
137
|
(name) => process.env[name] === undefined && name !== "CODEX_BIN" &&
|
|
138
|
+
!(setup && name === "CODEX_BRIDGE_OPEN_IN_APP") &&
|
|
136
139
|
!(forceDesktopTasks && name === "CODEX_BRIDGE_DESKTOP_TASKS") &&
|
|
137
140
|
!(desktopEnabled && name === "CODEX_BRIDGE_AUTOSTART"),
|
|
138
141
|
);
|
|
@@ -168,7 +171,7 @@ cfg.mcpServers[entryName] = {
|
|
|
168
171
|
*/
|
|
169
172
|
CODEX_BRIDGE_THREAD_POLICY: settled("CODEX_BRIDGE_THREAD_POLICY", "roots"),
|
|
170
173
|
CODEX_BRIDGE_SANDBOX: fullAccess ? "danger-full-access" : settled("CODEX_BRIDGE_SANDBOX", "workspace-write"),
|
|
171
|
-
CODEX_BRIDGE_OPEN_IN_APP:
|
|
174
|
+
CODEX_BRIDGE_OPEN_IN_APP: setup ? "0" : settled("CODEX_BRIDGE_OPEN_IN_APP", "0"),
|
|
172
175
|
CODEX_BRIDGE_RELEASE_AFTER_TURN: settled("CODEX_BRIDGE_RELEASE_AFTER_TURN", IS_WINDOWS ? "1" : "0"),
|
|
173
176
|
...(desktopMode !== undefined ? { CODEX_BRIDGE_DESKTOP_TASKS: desktopMode } : {}),
|
|
174
177
|
CODEX_BRIDGE_AUTOSTART: desktopEnabled ? "0" : settled("CODEX_BRIDGE_AUTOSTART", "1"),
|
|
@@ -185,6 +188,7 @@ cfg.mcpServers[entryName] = {
|
|
|
185
188
|
: {}),
|
|
186
189
|
},
|
|
187
190
|
};
|
|
191
|
+
if (setup) cfg.preferences = { ...cfg.preferences, menuBarEnabled: true };
|
|
188
192
|
|
|
189
193
|
createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...cfg.mcpServers[entryName].env }) });
|
|
190
194
|
|
|
@@ -255,7 +259,8 @@ if (entryName === "codex-bridge") {
|
|
|
255
259
|
}
|
|
256
260
|
}
|
|
257
261
|
|
|
258
|
-
log(`\
|
|
259
|
-
log("
|
|
260
|
-
if (desktopEnabled) log("
|
|
261
|
-
if (quiet) console.log(JSON.stringify({ action: "install", entry: entryName, configured: true, liveVerified: false, changed, restartRequired: changed, desktopTasks: desktopEnabled
|
|
262
|
+
log(`\nConfiguration saved for ${entryName}. Running MCP processes were not restarted and may still hold earlier settings.`);
|
|
263
|
+
log("An existing compatible supervisor reloads installed source when the worker is safely idle. codex_bridge_status reports the loaded process; a separate diagnostic process cannot verify it.");
|
|
264
|
+
if (desktopEnabled) log("Native delivery requires a loaded Desktop-owned relay. This installer does not start apps or open conversations, and saved configuration does not prove live readiness.");
|
|
265
|
+
if (quiet) console.log(JSON.stringify({ action: "install", entry: entryName, configured: true, liveVerified: false, changed, restartRequired: changed, desktopTasks: desktopEnabled,
|
|
266
|
+
...(setup ? { backgroundPreference: { setting: "preferences.menuBarEnabled", status: "configured", liveVerified: false } } : {}) }));
|
|
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
|
|
|
25
25
|
|
|
26
26
|
exitForVersionRequest(import.meta.url);
|
|
27
27
|
|
|
28
|
-
const VERSION = "1.
|
|
28
|
+
const VERSION = "1.26.0";
|
|
29
29
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
30
30
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
31
31
|
const entryArgs = ["native-relay-companion.mjs"];
|
|
@@ -136,5 +136,5 @@ if (process.argv.includes("--desktop-tasks")) {
|
|
|
136
136
|
writeRelayConfig({ ...readRelayConfig(), desktopTasks: true });
|
|
137
137
|
console.log("Desktop task creation enabled: exact saved local projects, immediate visibility, Codex Desktop permissions.");
|
|
138
138
|
}
|
|
139
|
-
console.log("
|
|
139
|
+
console.log("Native relay configuration saved. Running MCP processes were not restarted; native_relay_status reports whether a Desktop-owned relay is loaded and current. Existing compatible supervisors reload installed source when idle, but saved configuration does not prove the running process has loaded new settings. This installer does not start Codex Desktop or open conversations.");
|
|
140
140
|
console.log("remove: node scripts/install-native-relay.mjs --remove");
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { isMainModule } from "../src/install-paths.mjs";
|
|
6
|
+
import { backgroundAdapters } from "../src/background-relay.mjs";
|
|
7
|
+
import { BACKGROUND_AGENTS, canStartBackgroundWorker, configureRelay, readRelayConfiguration, relayFailure } from "../src/background-relay-config.mjs";
|
|
8
|
+
import { configureBackgroundWorkerMcp } from "../src/background-relay-registration.mjs";
|
|
9
|
+
import { installBackgroundRelay } from "./install-background-relay.mjs";
|
|
10
|
+
import { diagnosticText, withBridgeDiagnostic } from "../src/recipient-preflight.mjs";
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
|
|
14
|
+
export async function setupBackgroundRelay({ env = process.env, platform = process.platform, root = ROOT, check = false, dryRun = false,
|
|
15
|
+
adapters, readConfig = readRelayConfiguration, configure = configureRelay, registration = configureBackgroundWorkerMcp, service = installBackgroundRelay } = {}) {
|
|
16
|
+
if (dryRun) return { configured: false, dryRun: true, liveVerified: false,
|
|
17
|
+
planned: ["Independent relay configuration", "Scoped worker MCP communication", "Per-user background service"], opensApps: false, opensChats: false };
|
|
18
|
+
const workers = adapters ?? await backgroundAdapters();
|
|
19
|
+
const saved = readConfig({ env, platform, missing: true });
|
|
20
|
+
const readiness = await Promise.all(BACKGROUND_AGENTS.map(async agent => {
|
|
21
|
+
try { return { agent, ...await workers[agent].inspect({ env, platform, executable: saved?.providers[agent]?.executable ?? undefined }) }; }
|
|
22
|
+
catch (error) { return { agent, available: false, authenticated: null, ready: false, code: error.code ?? "WORKER_INSPECTION_FAILED" }; }
|
|
23
|
+
}));
|
|
24
|
+
const config = check ? saved
|
|
25
|
+
: configure({ env, platform });
|
|
26
|
+
if (!config) throw relayFailure("WORKER_RELAY_NOT_CONFIGURED", "The independent relay has not been configured; rerun the package setup command");
|
|
27
|
+
const mcp = registration({ env, platform, root, check });
|
|
28
|
+
const installed = await service({ env, platform, root, check });
|
|
29
|
+
const notices = [
|
|
30
|
+
"Background tasks have separate CLI identities and receipts; they never resume Desktop chats.",
|
|
31
|
+
...(!fs.existsSync(config.projectPolicy) ? ["No shared project policy is saved yet. Explicitly authorize a selected project with bridge-projects allow-project before sending background work."] : []),
|
|
32
|
+
...(mcp.blocked ? [mcp.reason ?? "An existing permission or disabled-server choice blocks worker MCP communication; that choice was preserved."] : []),
|
|
33
|
+
...readiness.filter(item => !item.available || item.authenticated === false).map(item => `${item.agent}: ${item.code ?? "WORKER_NOT_READY"}; CLI authentication and installation remain separate from Desktop sign-in.`),
|
|
34
|
+
...(installed.code ? [diagnosticText(`${installed.code}: ${installed.notice ?? installed.reason ?? "Inspect worker-relay status for service readiness."}`)]
|
|
35
|
+
: installed.notice ? [diagnosticText(installed.notice)] : []),
|
|
36
|
+
];
|
|
37
|
+
return { configured: mcp.configured === true && installed.configured === true, enabled: config.enabled,
|
|
38
|
+
liveVerified: installed.liveVerified === true, transportReady: installed.ready === true,
|
|
39
|
+
desktopRequired: false, opensApps: false, opensChats: false, notices,
|
|
40
|
+
workerReadiness: readiness.map(({ agent, available, authenticated, ready, version, code }) => ({ agent, available, authenticated, ready, version, code,
|
|
41
|
+
canAcceptWork: config.enabled && config.providers[agent].enabled && canStartBackgroundWorker(agent, { available, authenticated, ready, code }) })),
|
|
42
|
+
permissions: { blocked: mcp.blocked === true, ...(mcp.code ? { code: mcp.code } : {}) },
|
|
43
|
+
service: { configured: installed.configured, started: installed.started, activationPending: installed.activationPending, code: installed.code, version: installed.version } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
47
|
+
const args = process.argv.slice(2);
|
|
48
|
+
try {
|
|
49
|
+
if (args.some(arg => !["--check", "--dry-run", "--json"].includes(arg))) throw relayFailure("WORKER_SETUP_ARGUMENT_INVALID", "Unsupported background relay setup argument");
|
|
50
|
+
const result = await setupBackgroundRelay({ check: args.includes("--check"), dryRun: args.includes("--dry-run") });
|
|
51
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
52
|
+
if (!result.configured && !result.dryRun) process.exitCode = 1;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const result = withBridgeDiagnostic({ isError: true, content: [] }, error, { component: "background-relay", operation: "setup" });
|
|
55
|
+
process.stderr.write(result.content.map(item => item.text ?? "").join("\n") + "\n");
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/scripts/setup.mjs
CHANGED
|
@@ -6,9 +6,9 @@ exitForVersionRequest(import.meta.url);
|
|
|
6
6
|
try {
|
|
7
7
|
const result = runSetup({ args: process.argv.slice(2) });
|
|
8
8
|
printSetup(result, { json: process.argv.includes("--json") });
|
|
9
|
-
if (result.configured === false && !result.dryRun
|
|
9
|
+
if (result.configured === false && !result.dryRun) process.exitCode = 1;
|
|
10
10
|
} catch (error) {
|
|
11
11
|
process.stderr.write(`Bridge setup: ${error.message}\n`);
|
|
12
|
-
process.stderr.write("Run codex-bridge-setup to retry
|
|
13
|
-
|
|
12
|
+
process.stderr.write("Bridge configuration was not completed. Run codex-bridge-setup to retry after resolving the reported error.\n");
|
|
13
|
+
process.exitCode = 1;
|
|
14
14
|
}
|