@electric-ax/agents 0.3.0 → 0.4.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/dist/entrypoint.js +227 -205
- package/dist/index.cjs +225 -201
- package/dist/index.d.cts +17 -11
- package/dist/index.d.ts +18 -12
- package/dist/index.js +226 -202
- package/package.json +4 -4
package/dist/entrypoint.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import pino from "pino";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { completeWithLowCostModel, createEntityRegistry, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
6
|
+
import { appendPathToUrl, completeWithLowCostModel, createEntityRegistry, createPullWakeRunner, createRuntimeHandler, db, detectAvailableProviders, readCodexAccessToken, registerToolProvider, unregisterToolProvider } from "@electric-ax/agents-runtime";
|
|
8
7
|
import { eq, not, queryOnce } from "@durable-streams/state";
|
|
9
8
|
import { z } from "zod";
|
|
10
9
|
import { createHash } from "node:crypto";
|
|
@@ -22,8 +21,12 @@ const LOG_DIR = process.env.ELECTRIC_AGENTS_LOG_DIR ?? path.resolve(process.cwd(
|
|
|
22
21
|
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
22
|
const LOG_FILE = path.join(LOG_DIR, `builtin-agents-${Date.now()}.jsonl`);
|
|
24
23
|
const LOG_LEVEL = process.env.ELECTRIC_AGENTS_LOG_LEVEL ?? `info`;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
24
|
+
const IS_ELECTRON_MAIN = Boolean(process.versions.electron);
|
|
25
|
+
const USE_PRETTY_LOGS = LOG_LEVEL !== `silent` && !process.env.VITEST && !IS_ELECTRON_MAIN;
|
|
26
|
+
const streams = [{ stream: pino.destination({
|
|
27
|
+
dest: LOG_FILE,
|
|
28
|
+
sync: IS_ELECTRON_MAIN
|
|
29
|
+
}) }];
|
|
27
30
|
if (USE_PRETTY_LOGS) streams.push({ stream: pino.transport({
|
|
28
31
|
target: `pino-pretty`,
|
|
29
32
|
options: {
|
|
@@ -1898,9 +1901,8 @@ function truncate(str, max) {
|
|
|
1898
1901
|
|
|
1899
1902
|
//#endregion
|
|
1900
1903
|
//#region src/bootstrap.ts
|
|
1901
|
-
const DEFAULT_BUILTIN_AGENT_HANDLER_PATH = `/_electric/builtin-agent-handler`;
|
|
1902
1904
|
async function createBuiltinAgentHandler(options) {
|
|
1903
|
-
const { agentServerUrl, serveEndpoint
|
|
1905
|
+
const { agentServerUrl, serveEndpoint, workingDirectory, streamFn, createElectricTools, publicUrl, runtimeName, serverHeaders, defaultDispatchPolicyForType } = options;
|
|
1904
1906
|
const modelCatalog = await createBuiltinModelCatalog({ allowMockFallback: Boolean(streamFn) });
|
|
1905
1907
|
if (!modelCatalog) {
|
|
1906
1908
|
serverLog.warn(`[builtin-agents] no supported model provider API key found — set ANTHROPIC_API_KEY or OPENAI_API_KEY`);
|
|
@@ -1938,6 +1940,8 @@ async function createBuiltinAgentHandler(options) {
|
|
|
1938
1940
|
serveEndpoint,
|
|
1939
1941
|
registry,
|
|
1940
1942
|
subscriptionPathForType: (name) => `/${name}/*/main`,
|
|
1943
|
+
defaultDispatchPolicyForType,
|
|
1944
|
+
serverHeaders,
|
|
1941
1945
|
idleTimeout: 5e3,
|
|
1942
1946
|
createElectricTools,
|
|
1943
1947
|
publicUrl,
|
|
@@ -1959,15 +1963,13 @@ async function registerBuiltinAgentTypes(bootstrap) {
|
|
|
1959
1963
|
//#endregion
|
|
1960
1964
|
//#region src/server.ts
|
|
1961
1965
|
var BuiltinAgentsServer = class {
|
|
1962
|
-
server = null;
|
|
1963
1966
|
bootstrap = null;
|
|
1964
|
-
_url = null;
|
|
1965
|
-
publicBaseUrl = null;
|
|
1966
1967
|
_mcpRegistry = null;
|
|
1967
1968
|
mcpWatcherCloser = null;
|
|
1968
1969
|
mcpToolProviderName = null;
|
|
1969
1970
|
mcpApplyInFlight = new Set();
|
|
1970
1971
|
mcpStopping = false;
|
|
1972
|
+
pullWakeRunner = null;
|
|
1971
1973
|
options;
|
|
1972
1974
|
constructor(options) {
|
|
1973
1975
|
this.options = options;
|
|
@@ -1976,171 +1978,166 @@ var BuiltinAgentsServer = class {
|
|
|
1976
1978
|
get mcpRegistry() {
|
|
1977
1979
|
return this._mcpRegistry;
|
|
1978
1980
|
}
|
|
1979
|
-
get url() {
|
|
1980
|
-
if (!this._url) throw new Error(`Builtin agents server not started`);
|
|
1981
|
-
return this._url;
|
|
1982
|
-
}
|
|
1983
|
-
get registeredBaseUrl() {
|
|
1984
|
-
if (!this.publicBaseUrl) throw new Error(`Builtin agents server not started`);
|
|
1985
|
-
return this.publicBaseUrl;
|
|
1986
|
-
}
|
|
1987
1981
|
async start() {
|
|
1988
|
-
if (this.
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
}
|
|
1997
|
-
});
|
|
1982
|
+
if (this.bootstrap || this.pullWakeRunner) throw new Error(`Builtin agents runtime already started`);
|
|
1983
|
+
const pullWake = this.options.pullWake;
|
|
1984
|
+
if (!pullWake?.runnerId) throw new Error(`Builtin agents require a pull-wake runner id`);
|
|
1985
|
+
try {
|
|
1986
|
+
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.options.agentServerUrl;
|
|
1987
|
+
const mcpRegistry = createRegistry({
|
|
1988
|
+
publicUrl,
|
|
1989
|
+
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
1998
1990
|
});
|
|
1999
|
-
this.
|
|
2000
|
-
const
|
|
2001
|
-
this.
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
const serveEndpoint = new URL(webhookPath, this.publicBaseUrl.endsWith(`/`) ? this.publicBaseUrl : `${this.publicBaseUrl}/`).toString();
|
|
2012
|
-
const publicUrl = this.options.mcpOAuthRedirectBase ?? this.publicBaseUrl;
|
|
2013
|
-
const mcpRegistry = createRegistry({
|
|
2014
|
-
publicUrl,
|
|
2015
|
-
openAuthorizeUrl: this.options.openAuthorizeUrl
|
|
2016
|
-
});
|
|
2017
|
-
this._mcpRegistry = mcpRegistry;
|
|
2018
|
-
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
2019
|
-
const extras = this.options.extraMcpServers ?? [];
|
|
2020
|
-
const wirePersistence = async (cfg) => {
|
|
2021
|
-
const servers = [];
|
|
2022
|
-
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
2023
|
-
const persist = await keychainPersistence({ server: s.name });
|
|
2024
|
-
servers.push({
|
|
2025
|
-
...s,
|
|
2026
|
-
auth: {
|
|
2027
|
-
...s.auth,
|
|
2028
|
-
...persist
|
|
2029
|
-
}
|
|
2030
|
-
});
|
|
2031
|
-
} else servers.push(s);
|
|
2032
|
-
return {
|
|
2033
|
-
...cfg,
|
|
2034
|
-
servers
|
|
2035
|
-
};
|
|
2036
|
-
};
|
|
2037
|
-
const merge = (jsonCfg) => {
|
|
2038
|
-
const jsonServers = jsonCfg?.servers ?? [];
|
|
2039
|
-
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2040
|
-
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2041
|
-
return {
|
|
2042
|
-
servers: [...filteredExtras, ...jsonServers],
|
|
2043
|
-
raw: jsonCfg?.raw
|
|
2044
|
-
};
|
|
2045
|
-
};
|
|
2046
|
-
const onConfigError = this.options.onConfigError;
|
|
2047
|
-
const runApply = async (jsonCfg) => {
|
|
2048
|
-
if (this.mcpStopping) return;
|
|
2049
|
-
try {
|
|
2050
|
-
const wired = await wirePersistence(merge(jsonCfg));
|
|
2051
|
-
if (this.mcpStopping) return;
|
|
2052
|
-
await mcpRegistry.applyConfig(wired);
|
|
2053
|
-
} catch (e) {
|
|
2054
|
-
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2055
|
-
try {
|
|
2056
|
-
onConfigError?.(e);
|
|
2057
|
-
} catch (cbErr) {
|
|
2058
|
-
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
const applyMerged = (jsonCfg) => {
|
|
2063
|
-
const p = runApply(jsonCfg);
|
|
2064
|
-
this.mcpApplyInFlight.add(p);
|
|
2065
|
-
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2066
|
-
return p;
|
|
2067
|
-
};
|
|
2068
|
-
if (mcpConfigPath) {
|
|
2069
|
-
try {
|
|
2070
|
-
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2071
|
-
applyMerged(cfg);
|
|
2072
|
-
} catch (err) {
|
|
2073
|
-
if (err.code !== `ENOENT`) throw err;
|
|
2074
|
-
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2075
|
-
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2076
|
-
applyMerged(null);
|
|
2077
|
-
}
|
|
2078
|
-
try {
|
|
2079
|
-
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2080
|
-
onChange: (cfg) => void applyMerged(cfg),
|
|
2081
|
-
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2082
|
-
});
|
|
2083
|
-
} catch (e) {
|
|
2084
|
-
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2085
|
-
}
|
|
2086
|
-
} else {
|
|
2087
|
-
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2088
|
-
applyMerged(null);
|
|
2089
|
-
}
|
|
2090
|
-
this.mcpToolProviderName = `mcp`;
|
|
2091
|
-
registerToolProvider({
|
|
2092
|
-
name: `mcp`,
|
|
2093
|
-
tools: () => {
|
|
2094
|
-
const tools = [];
|
|
2095
|
-
for (const entry of mcpRegistry.list()) {
|
|
2096
|
-
if (entry.status !== `ready`) continue;
|
|
2097
|
-
const live = mcpRegistry.get(entry.name);
|
|
2098
|
-
if (!live?.transport) continue;
|
|
2099
|
-
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2100
|
-
server: entry.name,
|
|
2101
|
-
tool: t,
|
|
2102
|
-
client: live.transport.client,
|
|
2103
|
-
timeoutMs: live.config.timeoutMs
|
|
2104
|
-
}));
|
|
2105
|
-
const caps = live.transport.client.getServerCapabilities?.();
|
|
2106
|
-
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2107
|
-
server: entry.name,
|
|
2108
|
-
client: live.transport.client,
|
|
2109
|
-
timeoutMs: live.config.timeoutMs
|
|
2110
|
-
}));
|
|
2111
|
-
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2112
|
-
server: entry.name,
|
|
2113
|
-
client: live.transport.client,
|
|
2114
|
-
timeoutMs: live.config.timeoutMs
|
|
2115
|
-
}));
|
|
2116
|
-
}
|
|
2117
|
-
return tools;
|
|
1991
|
+
this._mcpRegistry = mcpRegistry;
|
|
1992
|
+
const mcpConfigPath = this.options.loadProjectMcpConfig ? path.resolve(this.options.workingDirectory ?? process.cwd(), `mcp.json`) : null;
|
|
1993
|
+
const extras = this.options.extraMcpServers ?? [];
|
|
1994
|
+
const wirePersistence = async (cfg) => {
|
|
1995
|
+
const servers = [];
|
|
1996
|
+
for (const s of cfg.servers) if (s.transport === `http` && s.auth?.mode === `authorizationCode`) {
|
|
1997
|
+
const persist = await keychainPersistence({ server: s.name });
|
|
1998
|
+
servers.push({
|
|
1999
|
+
...s,
|
|
2000
|
+
auth: {
|
|
2001
|
+
...s.auth,
|
|
2002
|
+
...persist
|
|
2118
2003
|
}
|
|
2119
2004
|
});
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2005
|
+
} else servers.push(s);
|
|
2006
|
+
return {
|
|
2007
|
+
...cfg,
|
|
2008
|
+
servers
|
|
2009
|
+
};
|
|
2010
|
+
};
|
|
2011
|
+
const merge = (jsonCfg) => {
|
|
2012
|
+
const jsonServers = jsonCfg?.servers ?? [];
|
|
2013
|
+
const jsonNames = new Set(jsonServers.map((s) => s.name));
|
|
2014
|
+
const filteredExtras = extras.filter((s) => !jsonNames.has(s.name));
|
|
2015
|
+
return {
|
|
2016
|
+
servers: [...filteredExtras, ...jsonServers],
|
|
2017
|
+
raw: jsonCfg?.raw
|
|
2018
|
+
};
|
|
2019
|
+
};
|
|
2020
|
+
const onConfigError = this.options.onConfigError;
|
|
2021
|
+
const runApply = async (jsonCfg) => {
|
|
2022
|
+
if (this.mcpStopping) return;
|
|
2023
|
+
try {
|
|
2024
|
+
const wired = await wirePersistence(merge(jsonCfg));
|
|
2025
|
+
if (this.mcpStopping) return;
|
|
2026
|
+
await mcpRegistry.applyConfig(wired);
|
|
2027
|
+
} catch (e) {
|
|
2028
|
+
serverLog.error(`[mcp] applyConfig:`, e);
|
|
2029
|
+
try {
|
|
2030
|
+
onConfigError?.(e);
|
|
2031
|
+
} catch (cbErr) {
|
|
2032
|
+
serverLog.error(`[mcp] onConfigError callback failed:`, cbErr);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
const applyMerged = (jsonCfg) => {
|
|
2037
|
+
const p = runApply(jsonCfg);
|
|
2038
|
+
this.mcpApplyInFlight.add(p);
|
|
2039
|
+
p.finally(() => this.mcpApplyInFlight.delete(p));
|
|
2040
|
+
return p;
|
|
2041
|
+
};
|
|
2042
|
+
if (mcpConfigPath) {
|
|
2043
|
+
try {
|
|
2044
|
+
const cfg = await loadConfig(mcpConfigPath, process.env);
|
|
2045
|
+
applyMerged(cfg);
|
|
2046
|
+
} catch (err) {
|
|
2047
|
+
if (err.code !== `ENOENT`) throw err;
|
|
2048
|
+
if (extras.length === 0) serverLog.info(`[mcp] no ${mcpConfigPath} — starting with no servers`);
|
|
2049
|
+
else serverLog.info(`[mcp] no ${mcpConfigPath} — starting with ${extras.length} server(s) from extras`);
|
|
2050
|
+
applyMerged(null);
|
|
2051
|
+
}
|
|
2052
|
+
try {
|
|
2053
|
+
this.mcpWatcherCloser = await watchConfig(mcpConfigPath, {
|
|
2054
|
+
onChange: (cfg) => void applyMerged(cfg),
|
|
2055
|
+
onError: (e) => serverLog.error(`[mcp] config error:`, e)
|
|
2128
2056
|
});
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2057
|
+
} catch (e) {
|
|
2058
|
+
serverLog.error(`[mcp] config watcher failed to start:`, e);
|
|
2059
|
+
}
|
|
2060
|
+
} else {
|
|
2061
|
+
if (extras.length > 0) serverLog.info(`[mcp] starting with ${extras.length} server(s) from extras`);
|
|
2062
|
+
applyMerged(null);
|
|
2063
|
+
}
|
|
2064
|
+
this.mcpToolProviderName = `mcp`;
|
|
2065
|
+
registerToolProvider({
|
|
2066
|
+
name: `mcp`,
|
|
2067
|
+
tools: () => {
|
|
2068
|
+
const tools = [];
|
|
2069
|
+
for (const entry of mcpRegistry.list()) {
|
|
2070
|
+
if (entry.status !== `ready`) continue;
|
|
2071
|
+
const live = mcpRegistry.get(entry.name);
|
|
2072
|
+
if (!live?.transport) continue;
|
|
2073
|
+
for (const t of entry.tools) tools.push(bridgeMcpTool({
|
|
2074
|
+
server: entry.name,
|
|
2075
|
+
tool: t,
|
|
2076
|
+
client: live.transport.client,
|
|
2077
|
+
timeoutMs: live.config.timeoutMs
|
|
2078
|
+
}));
|
|
2079
|
+
const caps = live.transport.client.getServerCapabilities?.();
|
|
2080
|
+
if (caps?.resources) tools.push(...buildResourceTools({
|
|
2081
|
+
server: entry.name,
|
|
2082
|
+
client: live.transport.client,
|
|
2083
|
+
timeoutMs: live.config.timeoutMs
|
|
2084
|
+
}));
|
|
2085
|
+
if (caps?.prompts) tools.push(...buildPromptTools({
|
|
2086
|
+
server: entry.name,
|
|
2087
|
+
client: live.transport.client,
|
|
2088
|
+
timeoutMs: live.config.timeoutMs
|
|
2089
|
+
}));
|
|
2090
|
+
}
|
|
2091
|
+
return tools;
|
|
2136
2092
|
}
|
|
2137
2093
|
});
|
|
2138
|
-
|
|
2094
|
+
this.bootstrap = await createBuiltinAgentHandler({
|
|
2095
|
+
agentServerUrl: this.options.agentServerUrl,
|
|
2096
|
+
workingDirectory: this.options.workingDirectory,
|
|
2097
|
+
streamFn: this.options.mockStreamFn,
|
|
2098
|
+
createElectricTools: this.options.createElectricTools,
|
|
2099
|
+
publicUrl,
|
|
2100
|
+
runtimeName: `builtin-agents`,
|
|
2101
|
+
serverHeaders: pullWake.headers
|
|
2102
|
+
});
|
|
2103
|
+
if (!this.bootstrap) throw new Error(`ANTHROPIC_API_KEY or OPENAI_API_KEY must be set before starting builtin agents`);
|
|
2104
|
+
await registerBuiltinAgentTypes(this.bootstrap);
|
|
2105
|
+
const registeredRunner = pullWake.registerRunner ? await this.registerPullWakeRunner(pullWake) : null;
|
|
2106
|
+
this.pullWakeRunner = createPullWakeRunner({
|
|
2107
|
+
baseUrl: this.options.agentServerUrl,
|
|
2108
|
+
runnerId: pullWake.runnerId,
|
|
2109
|
+
runtime: this.bootstrap.runtime,
|
|
2110
|
+
headers: pullWake.headers,
|
|
2111
|
+
claimHeaders: pullWake.claimHeaders,
|
|
2112
|
+
claimTokenHeader: pullWake.claimTokenHeader,
|
|
2113
|
+
heartbeatIntervalMs: pullWake.heartbeatIntervalMs,
|
|
2114
|
+
leaseMs: pullWake.leaseMs,
|
|
2115
|
+
offset: registeredRunner?.wake_stream_offset,
|
|
2116
|
+
onError: (error) => {
|
|
2117
|
+
serverLog.error(`[builtin-agents] pull-wake runner failed`, error);
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
this.pullWakeRunner.start();
|
|
2122
|
+
serverLog.info(`[builtin-agents] pull-wake runner started: ${pullWake.runnerId}`);
|
|
2123
|
+
return `pull-wake:${pullWake.runnerId}`;
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
await this.stop().catch(() => {});
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2139
2128
|
}
|
|
2140
2129
|
async stop() {
|
|
2130
|
+
if (this.pullWakeRunner) {
|
|
2131
|
+
await this.pullWakeRunner.stop().catch((e) => {
|
|
2132
|
+
serverLog.error(`[builtin-agents] pull-wake runner stop failed`, e);
|
|
2133
|
+
});
|
|
2134
|
+
this.pullWakeRunner = null;
|
|
2135
|
+
}
|
|
2141
2136
|
if (this.bootstrap) {
|
|
2142
2137
|
this.bootstrap.runtime.abortWakes();
|
|
2143
|
-
await Promise.race([this.bootstrap.runtime.drainWakes().catch(() => {
|
|
2138
|
+
await Promise.race([this.bootstrap.runtime.drainWakes().catch((err) => {
|
|
2139
|
+
serverLog.error(`[builtin-agents] drainWakes failed during shutdown:`, err);
|
|
2140
|
+
}), new Promise((resolve) => setTimeout(resolve, 5e3))]);
|
|
2144
2141
|
this.bootstrap = null;
|
|
2145
2142
|
}
|
|
2146
2143
|
this.mcpStopping = true;
|
|
@@ -2163,39 +2160,29 @@ var BuiltinAgentsServer = class {
|
|
|
2163
2160
|
});
|
|
2164
2161
|
this._mcpRegistry = null;
|
|
2165
2162
|
}
|
|
2166
|
-
if (this.server) {
|
|
2167
|
-
const server = this.server;
|
|
2168
|
-
await new Promise((resolve) => {
|
|
2169
|
-
server.close(() => resolve());
|
|
2170
|
-
});
|
|
2171
|
-
this.server = null;
|
|
2172
|
-
}
|
|
2173
2163
|
this.mcpStopping = false;
|
|
2174
|
-
this._url = null;
|
|
2175
|
-
this.publicBaseUrl = null;
|
|
2176
2164
|
}
|
|
2177
|
-
async
|
|
2178
|
-
const
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2165
|
+
async registerPullWakeRunner(pullWake) {
|
|
2166
|
+
const headers = new Headers(typeof pullWake.headers === `function` ? await pullWake.headers() : pullWake.headers);
|
|
2167
|
+
headers.set(`content-type`, `application/json`);
|
|
2168
|
+
const response = await fetch(appendPathToUrl(this.options.agentServerUrl, `/_electric/runners`), {
|
|
2169
|
+
method: `POST`,
|
|
2170
|
+
headers,
|
|
2171
|
+
body: JSON.stringify({
|
|
2172
|
+
id: pullWake.runnerId,
|
|
2173
|
+
owner_user_id: pullWake.ownerUserId,
|
|
2174
|
+
label: pullWake.label ?? `Built-in agents`,
|
|
2175
|
+
kind: `local`,
|
|
2176
|
+
admin_status: `enabled`
|
|
2177
|
+
})
|
|
2178
|
+
});
|
|
2179
|
+
if (!response.ok) throw new Error(`Failed to register pull-wake runner ${pullWake.runnerId}: ${response.status} ${await response.text()}`);
|
|
2180
|
+
return await response.json();
|
|
2192
2181
|
}
|
|
2193
2182
|
};
|
|
2194
2183
|
|
|
2195
2184
|
//#endregion
|
|
2196
2185
|
//#region src/entrypoint-lib.ts
|
|
2197
|
-
const DEFAULT_HOST = `127.0.0.1`;
|
|
2198
|
-
const DEFAULT_PORT = 4448;
|
|
2199
2186
|
function readEnv(env, names) {
|
|
2200
2187
|
for (const name of names) {
|
|
2201
2188
|
const value = env[name]?.trim();
|
|
@@ -2208,13 +2195,6 @@ function readRequiredEnv(env, names, description) {
|
|
|
2208
2195
|
if (value) return value;
|
|
2209
2196
|
throw new Error(`Missing ${description}. Set one of: ${names.map((name) => `"${name}"`).join(`, `)}`);
|
|
2210
2197
|
}
|
|
2211
|
-
function readPort(env) {
|
|
2212
|
-
const raw = readEnv(env, [`ELECTRIC_AGENTS_BUILTIN_PORT`, `PORT`]);
|
|
2213
|
-
if (!raw) return DEFAULT_PORT;
|
|
2214
|
-
const port = Number(raw);
|
|
2215
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid builtin agents port "${raw}". Expected an integer between 1 and 65535.`);
|
|
2216
|
-
return port;
|
|
2217
|
-
}
|
|
2218
2198
|
function validateUrl(name, value) {
|
|
2219
2199
|
try {
|
|
2220
2200
|
new URL(value);
|
|
@@ -2223,20 +2203,63 @@ function validateUrl(name, value) {
|
|
|
2223
2203
|
throw new Error(`Invalid ${name}: "${value}"`);
|
|
2224
2204
|
}
|
|
2225
2205
|
}
|
|
2206
|
+
function buildAssertedAuthHeaders(env) {
|
|
2207
|
+
const headers = {};
|
|
2208
|
+
const email = readEnv(env, [`ELECTRIC_ASSERTED_AUTH_EMAIL`]);
|
|
2209
|
+
const name = readEnv(env, [`ELECTRIC_ASSERTED_AUTH_NAME`]);
|
|
2210
|
+
if (email) headers[`X-Electric-Asserted-Email`] = email;
|
|
2211
|
+
if (name) headers[`X-Electric-Asserted-Name`] = name;
|
|
2212
|
+
return Object.keys(headers).length > 0 ? headers : void 0;
|
|
2213
|
+
}
|
|
2214
|
+
function parseAdditionalServerHeaders(env) {
|
|
2215
|
+
const raw = readEnv(env, [`ELECTRIC_AGENTS_SERVER_HEADERS`]);
|
|
2216
|
+
if (!raw) return void 0;
|
|
2217
|
+
let parsed;
|
|
2218
|
+
try {
|
|
2219
|
+
parsed = JSON.parse(raw);
|
|
2220
|
+
} catch {
|
|
2221
|
+
throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected JSON`);
|
|
2222
|
+
}
|
|
2223
|
+
if (!parsed || typeof parsed !== `object` || Array.isArray(parsed)) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: expected a JSON object`);
|
|
2224
|
+
const headers = new Headers();
|
|
2225
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
2226
|
+
if (typeof value !== `string`) throw new Error(`Invalid ELECTRIC_AGENTS_SERVER_HEADERS: header "${name}" must be a string`);
|
|
2227
|
+
headers.set(name, value);
|
|
2228
|
+
}
|
|
2229
|
+
const normalized = Object.fromEntries(headers.entries());
|
|
2230
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
2231
|
+
}
|
|
2232
|
+
function mergeHeaders(...sources) {
|
|
2233
|
+
const headers = new Headers();
|
|
2234
|
+
for (const source of sources) {
|
|
2235
|
+
if (!source) continue;
|
|
2236
|
+
new Headers(source).forEach((value, key) => headers.set(key, value));
|
|
2237
|
+
}
|
|
2238
|
+
const merged = Object.fromEntries(headers.entries());
|
|
2239
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2240
|
+
}
|
|
2241
|
+
function hasHeader(headers, name) {
|
|
2242
|
+
return headers ? new Headers(headers).has(name) : false;
|
|
2243
|
+
}
|
|
2226
2244
|
function resolveBuiltinAgentsEntrypointOptions(env = process.env, cwd = process.cwd()) {
|
|
2227
2245
|
const agentServerUrl = validateUrl(`agent server URL`, readRequiredEnv(env, [`ELECTRIC_AGENTS_SERVER_URL`, `ELECTRIC_AGENTS_BASE_URL`], `agent server base URL`));
|
|
2228
|
-
const
|
|
2246
|
+
const runnerId = readRequiredEnv(env, [`ELECTRIC_AGENTS_PULL_WAKE_RUNNER_ID`, `PULL_WAKE_RUNNER_ID`], `pull-wake runner id`);
|
|
2247
|
+
const serverHeaders = mergeHeaders(buildAssertedAuthHeaders(env), parseAdditionalServerHeaders(env));
|
|
2229
2248
|
return {
|
|
2230
2249
|
agentServerUrl,
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2250
|
+
workingDirectory: readEnv(env, [`ELECTRIC_AGENTS_WORKING_DIRECTORY`, `WORKING_DIRECTORY`]) ?? cwd,
|
|
2251
|
+
pullWake: {
|
|
2252
|
+
runnerId,
|
|
2253
|
+
registerRunner: readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `true` || readEnv(env, [`ELECTRIC_AGENTS_REGISTER_PULL_WAKE_RUNNER`]) === `1`,
|
|
2254
|
+
headers: serverHeaders,
|
|
2255
|
+
claimHeaders: serverHeaders,
|
|
2256
|
+
claimTokenHeader: hasHeader(serverHeaders, `authorization`) ? `electric-claim-token` : void 0
|
|
2257
|
+
}
|
|
2235
2258
|
};
|
|
2236
2259
|
}
|
|
2237
|
-
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer
|
|
2260
|
+
async function runBuiltinAgentsEntrypoint({ env = process.env, cwd = process.cwd(), createServer = (options$1) => new BuiltinAgentsServer(options$1) } = {}) {
|
|
2238
2261
|
const options = resolveBuiltinAgentsEntrypointOptions(env, cwd);
|
|
2239
|
-
const server = createServer
|
|
2262
|
+
const server = createServer(options);
|
|
2240
2263
|
const url = await server.start();
|
|
2241
2264
|
return {
|
|
2242
2265
|
options,
|
|
@@ -2260,10 +2283,9 @@ async function main() {
|
|
|
2260
2283
|
try {
|
|
2261
2284
|
const started = await runBuiltinAgentsEntrypoint();
|
|
2262
2285
|
server = started.server;
|
|
2263
|
-
console.log(`Builtin agents
|
|
2286
|
+
console.log(`Builtin agents pull-wake runner started at ${started.url}`);
|
|
2264
2287
|
console.log(`Registering against: ${started.options.agentServerUrl}`);
|
|
2265
2288
|
console.log(`Working directory: ${started.options.workingDirectory}`);
|
|
2266
|
-
if (started.options.baseUrl) console.log(`Public webhook base URL: ${started.options.baseUrl}`);
|
|
2267
2289
|
process.on(`SIGINT`, () => {
|
|
2268
2290
|
stop(0);
|
|
2269
2291
|
});
|