@minhspark/codex-mcp-bridge 1.25.3 → 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 +31 -0
- package/README.md +45 -20
- 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 +26 -14
- package/scripts/install-background-relay.mjs +428 -0
- package/scripts/install-native-relay.mjs +1 -1
- package/scripts/setup-background-relay.mjs +58 -0
- package/scripts/worker-relay.mjs +41 -0
- package/src/antigravity-bridge.mjs +18 -3
- package/src/antigravity-cli.mjs +13 -2
- package/src/antigravity-outbound-bridge.mjs +27 -6
- package/src/antigravity-outbound-targets.mjs +59 -4
- 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/claude-bridge.mjs +6 -3
- package/src/index.mjs +7 -2
- package/src/mcp-supervisor.mjs +17 -11
- package/src/native-relay-companion.mjs +1 -1
- package/src/reload-control.mjs +2 -2
- package/src/setup.mjs +26 -9
- package/src/windows-pipe-acl.ps1 +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import { readRelayConfiguration, relayFailure } from "./background-relay-config.mjs";
|
|
3
|
+
import { RELAY_FRAME_LIMIT, RELAY_REPLY_LIMIT, relayHello, validRelayHello, verifyRelayChallenge, relayOperation, verifyRelayResponse } from "./background-relay-protocol.mjs";
|
|
4
|
+
|
|
5
|
+
export function relayConnection(env = process.env) {
|
|
6
|
+
if (env.AGENT_RELAY_TOKEN || env.AGENT_RELAY_WORKER_ID || env.AGENT_RELAY_ENDPOINT) {
|
|
7
|
+
if (!env.AGENT_RELAY_TOKEN || !env.AGENT_RELAY_WORKER_ID || !env.AGENT_RELAY_ENDPOINT) throw relayFailure("WORKER_IDENTITY_INCOMPLETE", "The worker relay capability is incomplete");
|
|
8
|
+
return { endpoint: env.AGENT_RELAY_ENDPOINT, token: env.AGENT_RELAY_TOKEN, workerId: env.AGENT_RELAY_WORKER_ID };
|
|
9
|
+
}
|
|
10
|
+
const config = readRelayConfiguration({ env, missing: true });
|
|
11
|
+
if (!config) throw relayFailure("WORKER_RELAY_NOT_CONFIGURED", "Run the package update or setup command to configure the independent background relay");
|
|
12
|
+
if (!config.enabled) throw relayFailure("WORKER_RELAY_DISABLED", "Background relay is intentionally disabled in its configuration");
|
|
13
|
+
return { endpoint: config.endpoint, token: config.token, workerId: null };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function callBackgroundRelay(method, params = {}, { env = process.env, connection = relayConnection(env), timeoutMs = 10000, connect = net.createConnection } = {}) {
|
|
17
|
+
const initial = relayHello(connection.workerId ?? null);
|
|
18
|
+
if (!validRelayHello(initial) || typeof connection.token !== "string" || !connection.token || typeof connection.endpoint !== "string" || !connection.endpoint
|
|
19
|
+
|| !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000) throw relayFailure("WORKER_CONNECTION_INVALID", "Background relay connection options are invalid");
|
|
20
|
+
const operation = JSON.parse(JSON.stringify({ schemaVersion: 1, id: initial.id, workerId: initial.workerId, clientPid: process.pid, method, params }));
|
|
21
|
+
if (Buffer.byteLength(JSON.stringify(operation)) + 256 > RELAY_FRAME_LIMIT) throw relayFailure("WORKER_REQUEST_TOO_LARGE", "Background relay request is too large");
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let socket;
|
|
24
|
+
let sent = false;
|
|
25
|
+
let complete = false;
|
|
26
|
+
let attempts = 0;
|
|
27
|
+
let retryTimer;
|
|
28
|
+
const deadline = Date.now() + timeoutMs;
|
|
29
|
+
const state = () => sent && method === "send" ? "unknown" : "not_sent";
|
|
30
|
+
const failed = (code, message) => relayFailure(code, message, state(), "relay_transport");
|
|
31
|
+
const finish = (error, result) => {
|
|
32
|
+
if (complete) return;
|
|
33
|
+
complete = true;
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
clearTimeout(retryTimer);
|
|
36
|
+
socket?.destroy();
|
|
37
|
+
error ? reject(error) : resolve(result);
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => finish(failed("WORKER_RELAY_TIMEOUT", "The independent relay did not answer before the deadline")), timeoutMs);
|
|
40
|
+
const attempt = () => {
|
|
41
|
+
if (complete) return;
|
|
42
|
+
if (Date.now() >= deadline) { finish(failed("WORKER_RELAY_TIMEOUT", "The independent relay did not answer before the deadline")); return; }
|
|
43
|
+
attempts++;
|
|
44
|
+
let current;
|
|
45
|
+
let connected = false;
|
|
46
|
+
let received = false;
|
|
47
|
+
let retired = false;
|
|
48
|
+
let buffer = "";
|
|
49
|
+
let challenge;
|
|
50
|
+
const hello = relayHello(initial.workerId, initial.id);
|
|
51
|
+
const disconnect = (error, code = "WORKER_RELAY_UNAVAILABLE") => {
|
|
52
|
+
if (complete || retired) return;
|
|
53
|
+
retired = true;
|
|
54
|
+
current?.destroy();
|
|
55
|
+
const transient = ["ENOENT", "ECONNREFUSED", "ECONNRESET", "EBUSY", "EPIPE"].includes(error?.code)
|
|
56
|
+
|| connected && code === "WORKER_RELAY_INTERRUPTED";
|
|
57
|
+
if (!sent && !received && attempts < 12 && transient) {
|
|
58
|
+
const remaining = deadline - Date.now();
|
|
59
|
+
if (remaining <= 0) { finish(failed("WORKER_RELAY_TIMEOUT", "The independent relay did not answer before the deadline")); return; }
|
|
60
|
+
retryTimer = setTimeout(attempt, Math.min(250 * 2 ** (attempts - 1), 1000, remaining));
|
|
61
|
+
} else finish(failed(code, "The independent relay connection ended before a verified receipt was received"));
|
|
62
|
+
};
|
|
63
|
+
try { current = socket = connect(connection.endpoint); }
|
|
64
|
+
catch (error) { disconnect(error); return; }
|
|
65
|
+
current.setEncoding("utf8");
|
|
66
|
+
current.on("connect", () => {
|
|
67
|
+
if (complete || retired) return;
|
|
68
|
+
if (Date.now() >= deadline) { finish(failed("WORKER_RELAY_TIMEOUT", "The independent relay did not answer before the deadline")); return; }
|
|
69
|
+
connected = true;
|
|
70
|
+
try { current.write(JSON.stringify(hello) + "\n"); }
|
|
71
|
+
catch (error) { disconnect(error); }
|
|
72
|
+
});
|
|
73
|
+
current.on("error", error => disconnect(error));
|
|
74
|
+
current.on("close", () => disconnect(null, "WORKER_RELAY_INTERRUPTED"));
|
|
75
|
+
current.on("data", data => {
|
|
76
|
+
if (complete || retired) return;
|
|
77
|
+
if (Buffer.byteLength(data)) received = true;
|
|
78
|
+
buffer += data;
|
|
79
|
+
if (Buffer.byteLength(buffer) > (challenge ? RELAY_REPLY_LIMIT : 4096)) { finish(failed("WORKER_REPLY_TOO_LARGE", "The relay response exceeded its limit")); return; }
|
|
80
|
+
const end = buffer.indexOf("\n");
|
|
81
|
+
if (end < 0) return;
|
|
82
|
+
if (buffer.slice(end + 1).length) { finish(failed("WORKER_REPLY_INVALID", "The relay sent multiple or trailing protocol frames")); return; }
|
|
83
|
+
let message;
|
|
84
|
+
try { message = JSON.parse(buffer.slice(0, end)); }
|
|
85
|
+
catch { finish(failed("WORKER_REPLY_INVALID", "The relay response is not valid JSON")); return; }
|
|
86
|
+
buffer = "";
|
|
87
|
+
if (!challenge) {
|
|
88
|
+
if (!verifyRelayChallenge(connection.token, hello, message)) { finish(failed("WORKER_RELAY_AUTH_FAILED", "The local endpoint could not prove its relay identity; no operation was sent")); return; }
|
|
89
|
+
if (Date.now() >= deadline) { finish(failed("WORKER_RELAY_TIMEOUT", "The independent relay did not answer before the deadline")); return; }
|
|
90
|
+
challenge = message;
|
|
91
|
+
const wire = JSON.stringify(relayOperation(connection.token, hello, challenge, operation)) + "\n";
|
|
92
|
+
sent = true;
|
|
93
|
+
try { current.write(wire); } catch (error) { disconnect(error); }
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!verifyRelayResponse(connection.token, hello, challenge, message)) { finish(failed("WORKER_REPLY_INVALID", "The relay response failed authentication or request correlation")); return; }
|
|
97
|
+
const response = message.response;
|
|
98
|
+
if (response.error) {
|
|
99
|
+
const error = relayFailure(response.error.code ?? "WORKER_OPERATION_FAILED", response.error.message ?? "Background relay operation failed", response.error.deliveryState ?? "unknown", response.error.phase ?? "relay");
|
|
100
|
+
if (response.error.diagnostic) error.diagnostic = response.error.diagnostic;
|
|
101
|
+
finish(error);
|
|
102
|
+
} else finish(null, response.result);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
attempt();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
|
+
import { readRegularConfigFile } from "./project-policy.mjs";
|
|
6
|
+
import { protectPrivateRelayDirectory, assertPrivateRelayDirectory, assertPrivateRelayFile,
|
|
7
|
+
beginPrivateRelayWrite, bindPrivateRelayWrite, finishPrivateRelayWrite, abortPrivateRelayWrite } from "./background-relay-storage.mjs";
|
|
8
|
+
|
|
9
|
+
export const BACKGROUND_AGENTS = Object.freeze(["codex", "claude", "antigravity"]);
|
|
10
|
+
export const BACKGROUND_TOOLS = Object.freeze(["worker_status", "list_workers", "send_to_worker", "read_worker_delivery"]);
|
|
11
|
+
export const RELAY_UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
12
|
+
|
|
13
|
+
export function canStartBackgroundWorker(agent, readiness) {
|
|
14
|
+
return readiness.available === true && readiness.authenticated !== false && (readiness.ready === true
|
|
15
|
+
|| agent === "antigravity" && readiness.authenticated === null && /AUTH_UNVERIFIED$/.test(readiness.code ?? ""));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function relayFailure(code, message, deliveryState = "not_sent", phase = "preflight") {
|
|
19
|
+
return Object.assign(new Error(message), { code, phase, deliveryState, sent: deliveryState === "not_sent" ? false : deliveryState === "sent" ? true : null });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function relayPaths(env = process.env, platform = process.platform) {
|
|
23
|
+
const home = env.USERPROFILE || env.HOME || os.homedir();
|
|
24
|
+
const directory = env.AGENT_RELAY_DIRECTORY || path.join(env.XDG_CONFIG_HOME || path.join(home, ".config"), "GptClaudeBridge", "agent-relay");
|
|
25
|
+
if (!path.isAbsolute(directory)) throw relayFailure("WORKER_CONFIG_INVALID", "The background relay directory must be absolute");
|
|
26
|
+
const root = path.resolve(directory);
|
|
27
|
+
const hash = createHash("sha256").update(platform === "win32" ? root.toLowerCase() : root).digest("hex").slice(0, 24);
|
|
28
|
+
return { root, config: path.join(root, "config.json"), jobs: path.join(root, "jobs"),
|
|
29
|
+
endpoint: platform === "win32" ? `\\\\.\\pipe\\LOCAL\\minhspark-agent-relay-${hash}` : path.join(root, "relay.sock"),
|
|
30
|
+
defaultPolicy: env.CODEX_BRIDGE_PROJECT_POLICY || path.join(home, ".config", "GptClaudeBridge", "projects.json") };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function privateRelayDirectory(directory) {
|
|
34
|
+
return protectPrivateRelayDirectory(directory);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeRelayJson(file, value) {
|
|
38
|
+
const directory = path.dirname(file);
|
|
39
|
+
const temporary = path.join(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
|
|
40
|
+
const encoded = JSON.stringify(value, null, 2) + "\n";
|
|
41
|
+
if (Buffer.byteLength(encoded) > 4 * 1024 * 1024) throw relayFailure("WORKER_STORAGE_LIMIT", "Relay state exceeds its storage limit");
|
|
42
|
+
const ticket = beginPrivateRelayWrite(file);
|
|
43
|
+
let descriptor;
|
|
44
|
+
try {
|
|
45
|
+
const existing = readRegularConfigFile(file, { missing: true, maxBytes: 4 * 1024 * 1024, label: "Relay state" });
|
|
46
|
+
descriptor = fs.openSync(temporary, "wx", 0o600);
|
|
47
|
+
fs.writeFileSync(descriptor, encoded);
|
|
48
|
+
fs.fsyncSync(descriptor);
|
|
49
|
+
bindPrivateRelayWrite(ticket, temporary, descriptor);
|
|
50
|
+
fs.closeSync(descriptor);
|
|
51
|
+
descriptor = undefined;
|
|
52
|
+
if (readRegularConfigFile(file, { missing: true, maxBytes: 4 * 1024 * 1024, label: "Relay state" }) !== existing) throw relayFailure("WORKER_CONFIG_CHANGED", "Relay state changed concurrently");
|
|
53
|
+
fs.renameSync(temporary, file);
|
|
54
|
+
finishPrivateRelayWrite(ticket);
|
|
55
|
+
} finally {
|
|
56
|
+
abortPrivateRelayWrite(ticket);
|
|
57
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
58
|
+
try { fs.unlinkSync(temporary); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function readRelayConfiguration({ env = process.env, platform = process.platform, missing = false } = {}) {
|
|
63
|
+
const paths = relayPaths(env, platform);
|
|
64
|
+
try { fs.lstatSync(paths.root); }
|
|
65
|
+
catch (error) { if (missing && error.code === "ENOENT") return null; throw error; }
|
|
66
|
+
assertPrivateRelayDirectory(paths.root);
|
|
67
|
+
if (!assertPrivateRelayFile(paths.config, { missing })) return null;
|
|
68
|
+
const raw = readRegularConfigFile(paths.config, { missing, maxBytes: 64 * 1024, label: "Relay configuration" });
|
|
69
|
+
if (raw === null) return null;
|
|
70
|
+
let config;
|
|
71
|
+
try { config = JSON.parse(raw); } catch { throw relayFailure("WORKER_CONFIG_INVALID", "Relay configuration is not valid JSON"); }
|
|
72
|
+
if (!config || config.schemaVersion !== 1 || typeof config.enabled !== "boolean" || config.endpoint !== paths.endpoint
|
|
73
|
+
|| typeof config.token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(config.token)
|
|
74
|
+
|| typeof config.projectPolicy !== "string" || !path.isAbsolute(config.projectPolicy)
|
|
75
|
+
|| !config.providers || typeof config.providers !== "object" || Array.isArray(config.providers)) {
|
|
76
|
+
throw relayFailure("WORKER_CONFIG_INVALID", "Relay configuration has an unsupported schema");
|
|
77
|
+
}
|
|
78
|
+
for (const name of BACKGROUND_AGENTS) {
|
|
79
|
+
const provider = config.providers[name];
|
|
80
|
+
if (!provider || typeof provider.enabled !== "boolean" || provider.executable !== null && (typeof provider.executable !== "string" || !path.isAbsolute(provider.executable))) {
|
|
81
|
+
throw relayFailure("WORKER_CONFIG_INVALID", "Worker executable configuration is invalid");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { ...config, paths };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function configureRelay({ env = process.env, platform = process.platform, providers = {}, projectPolicy } = {}) {
|
|
88
|
+
const paths = relayPaths(env, platform);
|
|
89
|
+
const previous = readRelayConfiguration({ env, platform, missing: true });
|
|
90
|
+
const config = { schemaVersion: 1, enabled: previous?.enabled ?? true, endpoint: paths.endpoint,
|
|
91
|
+
token: previous?.token ?? randomBytes(32).toString("base64url"),
|
|
92
|
+
projectPolicy: projectPolicy ?? previous?.projectPolicy ?? paths.defaultPolicy,
|
|
93
|
+
providers: Object.fromEntries(BACKGROUND_AGENTS.map(name => [name, { enabled: previous?.providers[name]?.enabled ?? true,
|
|
94
|
+
executable: providers[name]?.executable ?? previous?.providers[name]?.executable ?? null }])) };
|
|
95
|
+
if (!path.isAbsolute(config.projectPolicy)) throw relayFailure("WORKER_CONFIG_INVALID", "Project policy must use an absolute path");
|
|
96
|
+
writeRelayJson(paths.config, config);
|
|
97
|
+
return readRelayConfiguration({ env, platform });
|
|
98
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { sourceRevision } from "./release-snapshot.mjs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { isMainModule } from "./install-paths.mjs";
|
|
5
|
+
import { readRelayConfiguration, relayFailure, writeRelayJson } from "./background-relay-config.mjs";
|
|
6
|
+
import { BackgroundRelay, backgroundAdapters, serveBackgroundRelay } from "./background-relay.mjs";
|
|
7
|
+
import { bridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
export async function runBackgroundDaemon({ env = process.env, exit = code => process.exit(code) } = {}) {
|
|
11
|
+
const config = readRelayConfiguration({ env });
|
|
12
|
+
if (!config.enabled) throw relayFailure("WORKER_RELAY_DISABLED", "The independent background relay is disabled");
|
|
13
|
+
const configuration = JSON.stringify(config);
|
|
14
|
+
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
15
|
+
const revision = sourceRevision(root);
|
|
16
|
+
const relay = new BackgroundRelay({ config, env, adapters: await backgroundAdapters() });
|
|
17
|
+
let transport;
|
|
18
|
+
try { transport = await serveBackgroundRelay(relay); }
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error.code === "EADDRINUSE") throw relayFailure("WORKER_RELAY_ALREADY_RUNNING", "A relay already owns this endpoint; another daemon was not started");
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
try { await relay.initialize(); }
|
|
24
|
+
catch (error) { await transport.close(); throw error; }
|
|
25
|
+
let stopping = false;
|
|
26
|
+
let requestedExit = null;
|
|
27
|
+
const stop = async code => {
|
|
28
|
+
if (stopping) return;
|
|
29
|
+
stopping = true;
|
|
30
|
+
clearInterval(poll);
|
|
31
|
+
clearInterval(health);
|
|
32
|
+
await transport.close();
|
|
33
|
+
exit(code);
|
|
34
|
+
};
|
|
35
|
+
const requestStop = code => { requestedExit = code; relay.accepting = false; };
|
|
36
|
+
const poll = setInterval(() => {
|
|
37
|
+
if (stopping) return;
|
|
38
|
+
try {
|
|
39
|
+
if (requestedExit === null && (sourceRevision(root) !== revision || JSON.stringify(readRelayConfiguration({ env })) !== configuration)) requestStop(75);
|
|
40
|
+
if (!relay.accepting && requestedExit === null) requestedExit = 75;
|
|
41
|
+
if (requestedExit !== null && relay.active.size === 0) void stop(requestedExit);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const diagnostic = bridgeDiagnostic(error, { component: "background-relay", operation: "watch_update", readOnly: true });
|
|
44
|
+
try { writeRelayJson(path.join(config.paths.root, "last-service-error.json"), diagnostic); } catch {}
|
|
45
|
+
}
|
|
46
|
+
}, 2000);
|
|
47
|
+
const health = setInterval(() => { void relay.refreshHealth(); }, 60000);
|
|
48
|
+
process.once("SIGTERM", () => requestStop(0));
|
|
49
|
+
process.once("SIGINT", () => requestStop(0));
|
|
50
|
+
process.stdout.write(JSON.stringify({ event: "relay_ready", pid: process.pid, version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version, desktopRequired: false }) + "\n");
|
|
51
|
+
return { relay, transport, stop: () => requestStop(0) };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
55
|
+
runBackgroundDaemon().catch(error => {
|
|
56
|
+
const diagnostic = bridgeDiagnostic(error, { component: "background-relay", operation: "startup" });
|
|
57
|
+
process.stderr.write(JSON.stringify({ diagnostic }) + "\n");
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { isMainModule } from "./install-paths.mjs";
|
|
6
|
+
import { callBackgroundRelay } from "./background-relay-client.mjs";
|
|
7
|
+
import { withBridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
8
|
+
|
|
9
|
+
export const BACKGROUND_WORKER_GUIDANCE = "For explicitly authorized work that must run with Desktop apps closed or frozen, use worker_status, send_to_worker and read_worker_delivery for separate background CLI workers. These tools have independent identities and project authorization; they never open, resume or impersonate a Desktop chat. Keep native task IDs on native tools. Never replay an uncertain native send through a worker. Show diagnostic codes and report blocks in the current chat when a relay call fails. ";
|
|
10
|
+
|
|
11
|
+
export function registerBackgroundTools(register, { env = process.env, call = callBackgroundRelay } = {}) {
|
|
12
|
+
const titles = { worker_status: "Inspect background relay", list_workers: "List background workers", send_to_worker: "Send work to a background agent", read_worker_delivery: "Read background delivery" };
|
|
13
|
+
const tool = (name, description, inputSchema, readOnly, method) => register(name, { title: titles[name], description, inputSchema,
|
|
14
|
+
annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async args => {
|
|
15
|
+
try {
|
|
16
|
+
const result = await call(method, args, { env, timeoutMs: method === "read" ? ((args.waitSec ?? 0) + 10) * 1000 : 15000 });
|
|
17
|
+
const failed = result.status === "failed" || result.status === "unknown";
|
|
18
|
+
const response = { ...(failed ? { isError: true } : {}), content: [{ type: "text", text: JSON.stringify(result, null, 2) }], structuredContent: result };
|
|
19
|
+
return failed ? withBridgeDiagnostic(response, { diagnostic: result.diagnostic, message: "The independent background worker did not complete successfully" },
|
|
20
|
+
{ component: "background-relay", operation: name, requestId: result.requestId, deliveryState: result.deliveryState }) : response;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return withBridgeDiagnostic({ isError: true, content: [] }, error, { component: "background-relay", operation: name,
|
|
23
|
+
requestId: args.requestId, readOnly: readOnly && method !== "read" });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
tool("worker_status", "Inspect the independent background relay, CLI readiness and recent diagnostic reports. Desktop apps may be closed or frozen. Transport readiness is separate from each CLI's authentication/model readiness. authentication-unverified with canAcceptWork=true is informational: that CLI lacks an auth probe and may run authorized work. Do not call it an authentication failure; report actual job failures from their receipts.", {}, true, "status");
|
|
27
|
+
tool("list_workers", "List independently identified CLI workers and this caller's background task receipts. These are separate from Desktop chats; never use a native chat ID as a worker destination.", {}, true, "list");
|
|
28
|
+
tool("send_to_worker", "Queue explicitly authorized work to a separate background CLI worker in an explicitly selected, authorized project. This does not address, open or resume any Desktop chat. Generate requestId once and keep it for retries; an uncertain result must be inspected with read_worker_delivery, never resent under another ID. Use worker-relay tools for subsequent cross-agent delegation. Work continues independently when Desktop apps close or freeze.", {
|
|
29
|
+
agent: z.enum(["codex", "claude", "antigravity"]).describe("The separate CLI agent that will perform this authorized task."),
|
|
30
|
+
cwd: z.string().min(1).describe("Absolute path to the explicitly selected and authorized project."),
|
|
31
|
+
prompt: z.string().min(1).max(262144).describe("Complete authorized task brief, including context, scope, constraints and completion criteria. Never request a Desktop chat substitution."),
|
|
32
|
+
requestId: z.string().uuid().describe("Generate once for this independent task and reuse for retries; inspect this ID after any uncertain delivery."),
|
|
33
|
+
}, false, "send");
|
|
34
|
+
tool("read_worker_delivery", "Read or wait for a durable background task receipt using its original requestId. A queued receipt is not a completed response. Failed and unknown receipts include the report block to show directly in this chat. Unknown work is never automatically replayed.", {
|
|
35
|
+
requestId: z.string().uuid().describe("The original request ID returned by send_to_worker."),
|
|
36
|
+
waitSec: z.number().int().min(0).max(60).optional().describe("Maximum seconds to wait for a terminal receipt; zero returns immediately."),
|
|
37
|
+
}, true, "read");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
41
|
+
const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
42
|
+
const server = new McpServer({ name: "worker-relay", version }, { instructions: BACKGROUND_WORKER_GUIDANCE + "Preserve request IDs, poll receipts until a terminal result, and never replay uncertain work. Delegate only authorized work in the same project. Native account logins and CLI logins are separate readiness checks; never copy credentials or open apps/chats to recover the relay." });
|
|
43
|
+
registerBackgroundTools(server.registerTool.bind(server));
|
|
44
|
+
await server.connect(new StdioServerTransport());
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const RELAY_FRAME_LIMIT = 1024 * 1024;
|
|
4
|
+
export const RELAY_REPLY_LIMIT = 4 * 1024 * 1024;
|
|
5
|
+
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
6
|
+
const NONCE = /^[A-Za-z0-9_-]{43}$/;
|
|
7
|
+
const object = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
const keys = (value, expected) => object(value) && Object.keys(value).length === expected.length && expected.every(key => Object.hasOwn(value, key));
|
|
9
|
+
const sign = (secret, domain, ...values) => createHmac("sha256", secret).update(JSON.stringify(["minhspark-background-relay", 1, domain, ...values])).digest("base64url");
|
|
10
|
+
const equal = (left, right) => typeof left === "string" && NONCE.test(left) && timingSafeEqual(Buffer.from(left), Buffer.from(right));
|
|
11
|
+
|
|
12
|
+
export function relayHello(workerId = null, id = randomUUID()) {
|
|
13
|
+
return { schemaVersion: 1, type: "hello", id, workerId, clientNonce: randomBytes(32).toString("base64url") };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validRelayHello(value) {
|
|
17
|
+
return keys(value, ["schemaVersion", "type", "id", "workerId", "clientNonce"]) && value.schemaVersion === 1 && value.type === "hello"
|
|
18
|
+
&& UUID.test(value.id) && (value.workerId === null || UUID.test(value.workerId)) && NONCE.test(value.clientNonce);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function relayChallenge(secret, hello) {
|
|
22
|
+
const serverNonce = randomBytes(32).toString("base64url");
|
|
23
|
+
return { schemaVersion: 1, type: "challenge", id: hello.id, serverNonce, proof: sign(secret, "server-proof", hello, serverNonce) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function verifyRelayChallenge(secret, hello, value) {
|
|
27
|
+
return keys(value, ["schemaVersion", "type", "id", "serverNonce", "proof"]) && value.schemaVersion === 1 && value.type === "challenge"
|
|
28
|
+
&& value.id === hello.id && NONCE.test(value.serverNonce) && equal(value.proof, sign(secret, "server-proof", hello, value.serverNonce));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function relayOperation(secret, hello, challenge, operation) {
|
|
32
|
+
return { schemaVersion: 1, type: "operation", operation, proof: sign(secret, "client-operation", hello, challenge.serverNonce, operation) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function verifyRelayOperation(secret, hello, challenge, value) {
|
|
36
|
+
const operation = value?.operation;
|
|
37
|
+
return keys(value, ["schemaVersion", "type", "operation", "proof"]) && value.schemaVersion === 1 && value.type === "operation"
|
|
38
|
+
&& keys(operation, ["schemaVersion", "id", "workerId", "clientPid", "method", "params"]) && operation.schemaVersion === 1
|
|
39
|
+
&& operation.id === hello.id && operation.workerId === hello.workerId && Number.isSafeInteger(operation.clientPid) && operation.clientPid > 0
|
|
40
|
+
&& ["status", "list", "read", "send", "drain"].includes(operation.method) && object(operation.params)
|
|
41
|
+
&& equal(value.proof, sign(secret, "client-operation", hello, challenge.serverNonce, operation));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function relayResponse(secret, hello, challenge, response) {
|
|
45
|
+
return { schemaVersion: 1, type: "response", response, proof: sign(secret, "server-response", hello, challenge.serverNonce, response) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function verifyRelayResponse(secret, hello, challenge, value) {
|
|
49
|
+
const response = value?.response;
|
|
50
|
+
return keys(value, ["schemaVersion", "type", "response", "proof"]) && value.schemaVersion === 1 && value.type === "response"
|
|
51
|
+
&& object(response) && response.schemaVersion === 1 && response.id === hello.id
|
|
52
|
+
&& (keys(response, ["schemaVersion", "id", "result"]) || keys(response, ["schemaVersion", "id", "error"]) && object(response.error))
|
|
53
|
+
&& equal(value.proof, sign(secret, "server-response", hello, challenge.serverNonce, response));
|
|
54
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { BACKGROUND_TOOLS, relayFailure } from "./background-relay-config.mjs";
|
|
7
|
+
import { readRegularConfigFile } from "./project-policy.mjs";
|
|
8
|
+
import { stableInstallPath, stableNodeBin } from "./install-paths.mjs";
|
|
9
|
+
|
|
10
|
+
const MAX_BYTES = 1024 * 1024;
|
|
11
|
+
const NAME = "worker-relay";
|
|
12
|
+
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
const invalid = () => relayFailure("WORKER_MCP_CONFIG_INVALID", "Antigravity worker configuration must contain valid JSON objects and permission lists.", "not_sent", "setup");
|
|
15
|
+
const unsafe = () => relayFailure("WORKER_MCP_CONFIG_UNSAFE", "Antigravity worker configuration must use bounded regular files and unlinked directories.", "not_sent", "setup");
|
|
16
|
+
const changed = () => relayFailure("WORKER_MCP_CONFIG_CHANGED", "Antigravity worker configuration changed during setup; concurrent changes were preserved.", "not_sent", "setup");
|
|
17
|
+
const sameFile = (left, right) => left === null || right === null ? left === right
|
|
18
|
+
: ["dev", "ino", "size", "mode", "mtimeNs", "ctimeNs", "nlink"].every((field) => left[field] === right[field]);
|
|
19
|
+
|
|
20
|
+
function stat(file) {
|
|
21
|
+
try { return fs.lstatSync(file, { bigint: true }); }
|
|
22
|
+
catch (error) { if (error.code === "ENOENT") return null; throw unsafe(); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function directories(home, file, create = false) {
|
|
26
|
+
const relative = path.relative(home, path.dirname(file));
|
|
27
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) throw unsafe();
|
|
28
|
+
let current = home;
|
|
29
|
+
for (const segment of ["", ...relative.split(path.sep).filter(Boolean)]) {
|
|
30
|
+
if (segment) current = path.join(current, segment);
|
|
31
|
+
let entry = stat(current);
|
|
32
|
+
if (entry === null && create) {
|
|
33
|
+
try { fs.mkdirSync(current, { mode: 0o700 }); }
|
|
34
|
+
catch (error) { if (error.code !== "EEXIST") throw unsafe(); }
|
|
35
|
+
entry = stat(current);
|
|
36
|
+
}
|
|
37
|
+
if (entry === null) return;
|
|
38
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw unsafe();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function snapshot(home, file) {
|
|
43
|
+
directories(home, file);
|
|
44
|
+
const before = stat(file);
|
|
45
|
+
let text;
|
|
46
|
+
try { text = readRegularConfigFile(file, { missing: true, maxBytes: MAX_BYTES, label: "Antigravity worker configuration" }); }
|
|
47
|
+
catch { throw unsafe(); }
|
|
48
|
+
const after = stat(file);
|
|
49
|
+
if (!sameFile(before, after)) throw changed();
|
|
50
|
+
let value;
|
|
51
|
+
try { value = text === null ? {} : JSON.parse(text); }
|
|
52
|
+
catch { throw invalid(); }
|
|
53
|
+
if (!object(value)) throw invalid();
|
|
54
|
+
return { file, text, value, stat: after, mode: after ? Number(after.mode & 0o777n) : 0o600 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertCurrent(home, expected) {
|
|
58
|
+
const current = snapshot(home, expected.file);
|
|
59
|
+
if (current.text !== expected.text || !sameFile(current.stat, expected.stat)) throw changed();
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function replace(home, original, text, mode, applied) {
|
|
64
|
+
if (Buffer.byteLength(text) > MAX_BYTES) throw unsafe();
|
|
65
|
+
directories(home, original.file, true);
|
|
66
|
+
assertCurrent(home, original);
|
|
67
|
+
const temporary = path.join(path.dirname(original.file), `.${path.basename(original.file)}.${randomUUID()}.tmp`);
|
|
68
|
+
let descriptor;
|
|
69
|
+
let identity;
|
|
70
|
+
try {
|
|
71
|
+
descriptor = fs.openSync(temporary, "wx", mode);
|
|
72
|
+
identity = fs.fstatSync(descriptor, { bigint: true });
|
|
73
|
+
fs.writeFileSync(descriptor, text, "utf8");
|
|
74
|
+
if (process.platform !== "win32") fs.fchmodSync(descriptor, mode);
|
|
75
|
+
fs.fsyncSync(descriptor);
|
|
76
|
+
fs.closeSync(descriptor);
|
|
77
|
+
descriptor = undefined;
|
|
78
|
+
assertCurrent(home, original);
|
|
79
|
+
fs.renameSync(temporary, original.file);
|
|
80
|
+
applied?.push({ original, text, identity });
|
|
81
|
+
const current = snapshot(home, original.file);
|
|
82
|
+
if (current.text !== text || current.stat?.dev !== identity.dev || current.stat?.ino !== identity.ino) throw changed();
|
|
83
|
+
return current;
|
|
84
|
+
} finally {
|
|
85
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
86
|
+
const remaining = stat(temporary);
|
|
87
|
+
if (remaining && identity && remaining.dev === identity.dev && remaining.ino === identity.ino) fs.unlinkSync(temporary);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function apply(home, edits) {
|
|
92
|
+
const applied = [];
|
|
93
|
+
try {
|
|
94
|
+
for (const edit of edits) assertCurrent(home, edit);
|
|
95
|
+
for (const edit of edits) replace(home, edit, edit.next, edit.mode, applied);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
let incomplete = false;
|
|
98
|
+
for (const record of applied.reverse()) {
|
|
99
|
+
try {
|
|
100
|
+
const current = snapshot(home, record.original.file);
|
|
101
|
+
if (current.text !== record.text || current.stat?.dev !== record.identity.dev || current.stat?.ino !== record.identity.ino) throw changed();
|
|
102
|
+
if (record.original.text === null) {
|
|
103
|
+
assertCurrent(home, current);
|
|
104
|
+
fs.unlinkSync(current.file);
|
|
105
|
+
if (snapshot(home, current.file).text !== null) throw changed();
|
|
106
|
+
} else replace(home, current, record.original.text, record.original.mode);
|
|
107
|
+
} catch { incomplete = true; }
|
|
108
|
+
}
|
|
109
|
+
if (incomplete) throw relayFailure("WORKER_MCP_REGISTRATION_INCOMPLETE", "Antigravity worker setup stopped after a partial update. Concurrent changes prevented safe rollback; inspect both configuration files.", "not_sent", "setup");
|
|
110
|
+
if (/^WORKER_MCP_/.test(error?.code ?? "")) throw error;
|
|
111
|
+
throw relayFailure("WORKER_MCP_WRITE_FAILED", "Antigravity worker configuration could not be saved. Previously completed updates were restored.", "not_sent", "setup");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function samePath(left, right, platform) {
|
|
116
|
+
if (typeof left !== "string" || !path.isAbsolute(left) || left.includes("\0")) return false;
|
|
117
|
+
const normalize = (value) => platform === "win32" ? path.resolve(value).toLowerCase() : path.resolve(value);
|
|
118
|
+
if (normalize(left) === normalize(right)) return true;
|
|
119
|
+
try { return normalize(fs.realpathSync.native(left)) === normalize(fs.realpathSync.native(right)); }
|
|
120
|
+
catch { return false; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function ownedEntry(entry, expected, platform) {
|
|
124
|
+
if (entry === undefined) return;
|
|
125
|
+
if (!object(entry) || !samePath(entry.command, expected.command, platform)
|
|
126
|
+
|| !Array.isArray(entry.args) || entry.args.length !== 1 || !samePath(entry.args[0], expected.args[0], platform)
|
|
127
|
+
|| entry.type !== undefined && entry.type !== "stdio"
|
|
128
|
+
|| ["url", "serverUrl", "serverURL", "httpUrl", "headers", "oauth", "authProviderType"].some((key) => Object.hasOwn(entry, key))
|
|
129
|
+
|| entry.env !== undefined && (!object(entry.env) || Object.keys(entry.env).length !== 0)) {
|
|
130
|
+
throw relayFailure("WORKER_MCP_COLLISION", "The canonical worker-relay MCP name is already used by a different configuration. It was not overwritten.", "not_sent", "setup");
|
|
131
|
+
}
|
|
132
|
+
if (entry.disabled !== undefined && typeof entry.disabled !== "boolean") throw invalid();
|
|
133
|
+
for (const key of ["enabledTools", "disabledTools"]) {
|
|
134
|
+
if (entry[key] !== undefined && (!Array.isArray(entry[key]) || entry[key].length > 4096 || entry[key].some((value) => typeof value !== "string" || value.length > 1024))) throw invalid();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function permissionState(settings, entry) {
|
|
139
|
+
if (settings.permissions !== undefined && !object(settings.permissions)) throw invalid();
|
|
140
|
+
const permissions = settings.permissions ?? {};
|
|
141
|
+
for (const key of ["allow", "ask", "deny"]) {
|
|
142
|
+
if (permissions[key] !== undefined && (!Array.isArray(permissions[key]) || permissions[key].length > 4096 || permissions[key].some((rule) => typeof rule !== "string" || rule.length > 1024))) throw invalid();
|
|
143
|
+
}
|
|
144
|
+
if (settings.toolPermission !== undefined && typeof settings.toolPermission !== "string") throw invalid();
|
|
145
|
+
const policyVerified = settings.toolPermission === undefined || ["request-review", "proceed-in-sandbox", "always-proceed", "strict"].includes(settings.toolPermission);
|
|
146
|
+
const matches = (rule, tool) => {
|
|
147
|
+
const match = /^mcp\((.*)\)$/.exec(rule.trim());
|
|
148
|
+
return match && ["*", `${NAME}/*`, `${NAME}/${tool}`].includes(match[1].trim());
|
|
149
|
+
};
|
|
150
|
+
const blockedTools = BACKGROUND_TOOLS.filter((tool) => !policyVerified || settings.toolPermission === "strict" || entry.disabled === true
|
|
151
|
+
|| entry.enabledTools !== undefined && !entry.enabledTools.includes(tool)
|
|
152
|
+
|| entry.disabledTools?.includes(tool)
|
|
153
|
+
|| [...(permissions.deny ?? []), ...(permissions.ask ?? [])].some((rule) => matches(rule, tool)));
|
|
154
|
+
return { blockedTools, policyVerified };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function configureBackgroundWorkerMcp({ env = process.env, platform = process.platform, root = DEFAULT_ROOT, dryRun = false, check = false } = {}) {
|
|
158
|
+
const selectedHome = platform === "win32" ? env.USERPROFILE || env.HOME || os.homedir() : env.HOME || env.USERPROFILE || os.homedir();
|
|
159
|
+
if (typeof selectedHome !== "string" || !path.isAbsolute(selectedHome) || typeof root !== "string" || !path.isAbsolute(root)) throw invalid();
|
|
160
|
+
let home;
|
|
161
|
+
let entry;
|
|
162
|
+
try {
|
|
163
|
+
home = fs.realpathSync.native(selectedHome);
|
|
164
|
+
if (!fs.statSync(home).isDirectory()) throw unsafe();
|
|
165
|
+
entry = path.join(stableInstallPath(fs.realpathSync.native(root), env), "src", "background-relay-mcp.mjs");
|
|
166
|
+
if (!fs.statSync(entry).isFile()) throw invalid();
|
|
167
|
+
} catch { throw relayFailure("WORKER_MCP_PATH_INVALID", "The user home or installed background relay entrypoint is unavailable.", "not_sent", "setup"); }
|
|
168
|
+
const files = { mcp: path.join(home, ".gemini", "config", "mcp_config.json"), settings: path.join(home, ".gemini", "antigravity-cli", "settings.json") };
|
|
169
|
+
const originals = { mcp: snapshot(home, files.mcp), settings: snapshot(home, files.settings) };
|
|
170
|
+
const mcp = structuredClone(originals.mcp.value);
|
|
171
|
+
const settings = structuredClone(originals.settings.value);
|
|
172
|
+
if (mcp.mcpServers !== undefined && !object(mcp.mcpServers)) throw invalid();
|
|
173
|
+
const expected = { command: stableNodeBin(env), args: [entry] };
|
|
174
|
+
const existing = mcp.mcpServers?.[NAME];
|
|
175
|
+
ownedEntry(existing, expected, platform);
|
|
176
|
+
const policy = permissionState(settings, existing ?? expected);
|
|
177
|
+
mcp.mcpServers ??= {};
|
|
178
|
+
if (existing === undefined) mcp.mcpServers[NAME] = expected;
|
|
179
|
+
settings.permissions ??= {};
|
|
180
|
+
settings.permissions.allow ??= [];
|
|
181
|
+
const grants = BACKGROUND_TOOLS.map((tool) => `mcp(${NAME}/${tool})`);
|
|
182
|
+
const added = grants.filter((rule) => !settings.permissions.allow.includes(rule));
|
|
183
|
+
settings.permissions.allow.push(...added);
|
|
184
|
+
const edits = Object.entries({ mcp, settings }).flatMap(([key, value]) => {
|
|
185
|
+
const original = originals[key];
|
|
186
|
+
if (JSON.stringify(value) === JSON.stringify(original.value)) return [];
|
|
187
|
+
const next = JSON.stringify(value, null, 2) + "\n";
|
|
188
|
+
if (Buffer.byteLength(next) > MAX_BYTES) throw unsafe();
|
|
189
|
+
return [{ ...original, next }];
|
|
190
|
+
});
|
|
191
|
+
if (!dryRun && !check) apply(home, edits);
|
|
192
|
+
const configured = !edits.length || !dryRun && !check;
|
|
193
|
+
const blocked = policy.blockedTools.length > 0;
|
|
194
|
+
return { configured, changed: Boolean(edits.length && !dryRun && !check), wouldChange: edits.length > 0, blocked, files,
|
|
195
|
+
permissions: { ...policy, required: grants, added }, isolatedMcpInventory: false, nativeFallback: false,
|
|
196
|
+
...(blocked ? { code: policy.policyVerified ? "WORKER_MCP_POLICY_BLOCKED" : "WORKER_MCP_POLICY_UNVERIFIED", reason: "Existing Antigravity permissions or MCP tool restrictions prevent unattended relay access. Those restrictions were preserved." }
|
|
197
|
+
: !configured ? { code: "WORKER_MCP_NOT_CONFIGURED", reason: "The Antigravity worker MCP registration or scoped permission grants require configuration." } : {}) };
|
|
198
|
+
}
|