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
1111 makeScopedKv,
1212 migrate,
1313 } from "@executor/storage-file";
14−import { withConfigFile } from "@executor/config";
14+import { withConfigFile, loadConfig, SECRET_REF_PREFIX, type ConfigHeaderValue } from "@executor/config";
1515 import {
1616 openApiPlugin,
1717 makeKvOperationStore,
@@ -69,7 +69,7 @@ export class ExecutorService extends Context.Tag("ExecutorService")< · 51 unmodified lines
6969 // ---------------------------------------------------------------------------
7070
7171 import { homedir } from "node:os";
72−import { join } from "node:path";
72+import { dirname, join, resolve } from "node:path";
7373
7474 const resolveDataDir = (): string => {
7575 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
8282
8383 const DB_PATH = `${DATA_DIR}/data.db`;
8484
85+const 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+
96+const 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+
117+const 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+
85147 // ---------------------------------------------------------------------------
86148 // Executor Layer — SQLite-backed, scoped to ManagedRuntime lifetime
87149 // ---------------------------------------------------------------------------
@@ -93,15 +155,15 @@ const ExecutorLayer = Layer.effect( · 5 unmodified lines
93155
94156 yield* migrate.pipe(Effect.catchAll((e) => Effect.die(e)));
95157
96− const cwd = process.cwd();
158+ const executorRoot = resolveExecutorRoot(process.cwd());
97159 const kv = makeSqliteKv(sql);
98− const config = makeKvConfig(kv, { cwd });
99− const scopedKv = makeScopedKv(kv, cwd);
160+ const config = makeKvConfig(kv, { cwd: executorRoot });
161+ const scopedKv = makeScopedKv(kv, executorRoot);
100162
101− const configPath = join(process.cwd(), "executor.jsonc");
163+ const configPath = join(executorRoot, "executor.jsonc");
102164 const fsLayer = NodeFileSystem.layer;
103165
104− return yield* createExecutor({
166+ const executor = yield* createExecutor({
105167 ...config,
106168 plugins: [
107169 openApiPlugin({
@@ -138,6 +200,65 @@ const ExecutorLayer = Layer.effect( · 30 unmodified lines
138200 }),
139201 ] as const,
140202 });
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;
141262 }),
142263 ).pipe(Layer.provide(SqliteClient.layer({ filename: DB_PATH })));
143264
executor.jsoncrenamed from apps/web/executor.jsonc+0−0

File renamed without changes.