History

Fix root executor config resolution

Rhys Sullivan committed 7 months ago

Commit 3c67ec01 parent 05d78bc2 files · +128 −7Browse files

Unified · Split

2 files
  1. apps/server/src/services/executor.ts +128 −7
  2. executor.jsonc +0 −0
apps/server/src/services/executor.ts+128−7
@@ -11,7 +11,7 @@ import { · 10 unmodified lines
11 makeScopedKv,11 makeScopedKv,
12 migrate,12 migrate,
13} from "@executor/storage-file";13} from "@executor/storage-file";
14import { withConfigFile } from "@executor/config";14import { withConfigFile, loadConfig, SECRET_REF_PREFIX, type ConfigHeaderValue } from "@executor/config";
15import {15import {
16 openApiPlugin,16 openApiPlugin,
17 makeKvOperationStore,17 makeKvOperationStore,
@@ -69,7 +69,7 @@ export class ExecutorService extends Context.Tag("ExecutorService")< · 51 unmodified lines
69// ---------------------------------------------------------------------------69// ---------------------------------------------------------------------------
70 70
71import { homedir } from "node:os";71import { homedir } from "node:os";
72import { join } from "node:path";72import { dirname, join, resolve } from "node:path";
73 73
74const resolveDataDir = (): string => {74const resolveDataDir = (): string => {
75 if (process.env.EXECUTOR_DATA_DIR) return process.env.EXECUTOR_DATA_DIR;75 if (process.env.EXECUTOR_DATA_DIR) return process.env.EXECUTOR_DATA_DIR;
@@ -82,6 +82,68 @@ fs.mkdirSync(DATA_DIR, { recursive: true }); · 6 unmodified lines
82 82
83const DB_PATH = `${DATA_DIR}/data.db`;83const DB_PATH = `${DATA_DIR}/data.db`;
84 84
85const hasWorkspacePackage = (dir: string): boolean => {
86 try {
87 const path = join(dir, "package.json");
88 if (!fs.existsSync(path)) return false;
89 const parsed = JSON.parse(fs.readFileSync(path, "utf8")) as { workspaces?: unknown };
90 return Array.isArray(parsed.workspaces);
91 } catch {
92 return false;
93 }
94};
95
96const resolveExecutorRoot = (startDir: string): string => {
97 let current = resolve(startDir);
98 let fallback = current;
99
100 for (;;) {
101 if (fs.existsSync(join(current, "executor.jsonc"))) {
102 return current;
103 }
104
105 if (fs.existsSync(join(current, ".git")) || hasWorkspacePackage(current)) {
106 fallback = current;
107 }
108
109 const parent = dirname(current);
110 if (parent === current) {
111 return fallback;
112 }
113 current = parent;
114 }
115};
116
117const toInvocationHeaders = (
118 headers: Record<string, ConfigHeaderValue> | undefined,
119): Record<string, string | { readonly secretId: string; readonly prefix?: string }> | undefined => {
120 if (!headers) return undefined;
121
122 return Object.fromEntries(
123 Object.entries(headers).map(([key, value]) => {
124 if (typeof value === "string") {
125 return [
126 key,
127 value.startsWith(SECRET_REF_PREFIX)
128 ? { secretId: value.slice(SECRET_REF_PREFIX.length) }
129 : value,
130 ];
131 }
132
133 const rawValue = value.value;
134 return [
135 key,
136 rawValue.startsWith(SECRET_REF_PREFIX)
137 ? {
138 secretId: rawValue.slice(SECRET_REF_PREFIX.length),
139 ...(value.prefix ? { prefix: value.prefix } : {}),
140 }
141 : rawValue,
142 ];
143 }),
144 );
145};
146
85// ---------------------------------------------------------------------------147// ---------------------------------------------------------------------------
86// Executor Layer — SQLite-backed, scoped to ManagedRuntime lifetime148// Executor Layer — SQLite-backed, scoped to ManagedRuntime lifetime
87// ---------------------------------------------------------------------------149// ---------------------------------------------------------------------------
@@ -93,15 +155,15 @@ const ExecutorLayer = Layer.effect( · 5 unmodified lines
93 155
94 yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e)));156 yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e)));
95 157
96 const cwd = process.cwd();158 const executorRoot = resolveExecutorRoot(process.cwd());
97 const kv = makeSqliteKv(sql);159 const kv = makeSqliteKv(sql);
98 const config = makeKvConfig(kv, { cwd });160 const config = makeKvConfig(kv, { cwd: executorRoot });
99 const scopedKv = makeScopedKv(kv, cwd);161 const scopedKv = makeScopedKv(kv, executorRoot);
100 162
101 const configPath = join(process.cwd(), "executor.jsonc");163 const configPath = join(executorRoot, "executor.jsonc");
102 const fsLayer = NodeFileSystem.layer;164 const fsLayer = NodeFileSystem.layer;
103 165
104 return yield* createExecutor({166 const executor = yield* createExecutor({
105 ...config,167 ...config,
106 plugins: [168 plugins: [
107 openApiPlugin({169 openApiPlugin({
@@ -138,6 +200,65 @@ const ExecutorLayer = Layer.effect( · 30 unmodified lines
138 }),200 }),
139 ] as const,201 ] as const,
140 });202 });
203
204 const fileConfig = yield* loadConfig(configPath).pipe(
205 Effect.provide(fsLayer),
206 Effect.catchAll((e) => Effect.die(e)),
207 );
208
209 if (fileConfig?.sources) {
210 const existingSources = new Set((yield* executor.sources.list()).map((source) => source.id));
211
212 for (const source of fileConfig.sources) {
213 if (source.namespace && existingSources.has(source.namespace)) {
214 continue;
215 }
216
217 if (source.kind === "openapi") {
218 yield* executor.openapi.addSpec({
219 spec: source.spec,
220 ...(source.baseUrl ? { baseUrl: source.baseUrl } : {}),
221 ...(source.namespace ? { namespace: source.namespace } : {}),
222 ...(source.headers ? { headers: toInvocationHeaders(source.headers) } : {}),
223 }).pipe(Effect.catchAll((e) => Effect.die(e)));
224 continue;
225 }
226
227 if (source.kind === "graphql") {
228 yield* executor.graphql.addSource({
229 endpoint: source.endpoint,
230 ...(source.introspectionJson ? { introspectionJson: source.introspectionJson } : {}),
231 ...(source.namespace ? { namespace: source.namespace } : {}),
232 ...(source.headers ? { headers: toInvocationHeaders(source.headers) } : {}),
233 }).pipe(Effect.catchAll((e) => Effect.die(e)));
234 continue;
235 }
236
237 yield* executor.mcp.addSource(
238 source.transport === "stdio"
239 ? {
240 transport: "stdio",
241 name: source.name,
242 command: source.command,
243 ...(source.args ? { args: [...source.args] } : {}),
244 ...(source.env ? { env: source.env } : {}),
245 ...(source.cwd ? { cwd: source.cwd } : {}),
246 ...(source.namespace ? { namespace: source.namespace } : {}),
247 }
248 : {
249 transport: "remote",
250 name: source.name,
251 endpoint: source.endpoint,
252 ...(source.remoteTransport ? { remoteTransport: source.remoteTransport } : {}),
253 ...(source.queryParams ? { queryParams: source.queryParams } : {}),
254 ...(source.headers ? { headers: source.headers } : {}),
255 ...(source.namespace ? { namespace: source.namespace } : {}),
256 },
257 ).pipe(Effect.catchAll((e) => Effect.die(e)));
258 }
259 }
260
261 return executor;
141 }),262 }),
142).pipe(Layer.provide(SqliteClient.layer({ filename: DB_PATH })));263).pipe(Layer.provide(SqliteClient.layer({ filename: DB_PATH })));
143 264
executor.jsoncrenamed from apps/web/executor.jsonc+0−0

File renamed without changes.